Merge branch 'dev' into live-se-red
This commit is contained in:
commit
6c42bb0952
16 changed files with 440 additions and 116 deletions
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-alpine
|
||||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
ARG GIT_SHA
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-alpine
|
||||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
ARG GIT_SHA
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@ def __transform_journey(rows, reverse_path=False):
|
|||
break
|
||||
number_of_step1 += 1
|
||||
total_100p += r["sessions_count"]
|
||||
# for i in range(number_of_step1):
|
||||
# rows[i]["value"] = 100 / number_of_step1
|
||||
|
||||
# for i in range(number_of_step1, len(rows)):
|
||||
for i in range(len(rows)):
|
||||
rows[i]["value"] = rows[i]["sessions_count"] * 100 / total_100p
|
||||
|
||||
|
|
@ -32,22 +29,17 @@ def __transform_journey(rows, reverse_path=False):
|
|||
source = f"{r['event_number_in_session']}_{r['event_type']}_{r['e_value']}"
|
||||
if source not in nodes:
|
||||
nodes.append(source)
|
||||
nodes_values.append({"name": r['e_value'], "eventType": r['event_type'],
|
||||
"avgTimeFromPrevious": 0, "sessionsCount": 0})
|
||||
nodes_values.append({"name": r['e_value'], "eventType": r['event_type']})
|
||||
if r['next_value']:
|
||||
target = f"{r['event_number_in_session'] + 1}_{r['next_type']}_{r['next_value']}"
|
||||
if target not in nodes:
|
||||
nodes.append(target)
|
||||
nodes_values.append({"name": r['next_value'], "eventType": r['next_type'],
|
||||
"avgTimeFromPrevious": 0, "sessionsCount": 0})
|
||||
nodes_values.append({"name": r['next_value'], "eventType": r['next_type']})
|
||||
|
||||
sr_idx = nodes.index(source)
|
||||
tg_idx = nodes.index(target)
|
||||
if r["avg_time_from_previous"] is not None:
|
||||
nodes_values[tg_idx]["avgTimeFromPrevious"] += r["avg_time_from_previous"] * r["sessions_count"]
|
||||
nodes_values[tg_idx]["sessionsCount"] += r["sessions_count"]
|
||||
link = {"eventType": r['event_type'], "sessionsCount": r["sessions_count"],
|
||||
"value": r["value"], "avgTimeFromPrevious": r["avg_time_from_previous"]}
|
||||
|
||||
link = {"eventType": r['event_type'], "sessionsCount": r["sessions_count"],"value": r["value"]}
|
||||
if not reverse_path:
|
||||
link["source"] = sr_idx
|
||||
link["target"] = tg_idx
|
||||
|
|
@ -55,12 +47,6 @@ def __transform_journey(rows, reverse_path=False):
|
|||
link["source"] = tg_idx
|
||||
link["target"] = sr_idx
|
||||
links.append(link)
|
||||
for n in nodes_values:
|
||||
if n["sessionsCount"] > 0:
|
||||
n["avgTimeFromPrevious"] = n["avgTimeFromPrevious"] / n["sessionsCount"]
|
||||
else:
|
||||
n["avgTimeFromPrevious"] = None
|
||||
n.pop("sessionsCount")
|
||||
|
||||
return {"nodes": nodes_values,
|
||||
"links": sorted(links, key=lambda x: (x["source"], x["target"]), reverse=False)}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ JOURNEY_TYPES = {
|
|||
# query: Q5, the result is correct,
|
||||
# startPoints are computed before ranked_events to reduce the number of window functions over rows
|
||||
# replaced time_to_target by time_from_previous
|
||||
# compute avg_time_from_previous at the same level as sessions_count
|
||||
# compute avg_time_from_previous at the same level as sessions_count (this was removed in v1.22)
|
||||
# sort by top 5 according to sessions_count at the CTE level
|
||||
# final part project data without grouping
|
||||
# if start-point is selected, the selected event is ranked n°1
|
||||
|
|
@ -35,15 +35,29 @@ def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
|||
sub_events = []
|
||||
start_points_conditions = []
|
||||
step_0_conditions = []
|
||||
step_1_post_conditions = ["event_number_in_session <= %(density)s"]
|
||||
|
||||
if len(data.metric_value) == 0:
|
||||
data.metric_value.append(schemas.ProductAnalyticsSelectedEventType.LOCATION)
|
||||
sub_events.append({"column": JOURNEY_TYPES[schemas.ProductAnalyticsSelectedEventType.LOCATION]["column"],
|
||||
"eventType": schemas.ProductAnalyticsSelectedEventType.LOCATION.value})
|
||||
else:
|
||||
if len(data.start_point) > 0:
|
||||
extra_metric_values = []
|
||||
for s in data.start_point:
|
||||
if s.type not in data.metric_value:
|
||||
sub_events.append({"column": JOURNEY_TYPES[s.type]["column"],
|
||||
"eventType": JOURNEY_TYPES[s.type]["eventType"]})
|
||||
step_1_post_conditions.append(
|
||||
f"(event_type!='{JOURNEY_TYPES[s.type]["eventType"]}' OR event_number_in_session = 1)")
|
||||
extra_metric_values.append(s.type)
|
||||
data.metric_value += extra_metric_values
|
||||
|
||||
for v in data.metric_value:
|
||||
if JOURNEY_TYPES.get(v):
|
||||
sub_events.append({"column": JOURNEY_TYPES[v]["column"],
|
||||
"eventType": JOURNEY_TYPES[v]["eventType"]})
|
||||
|
||||
if len(sub_events) == 1:
|
||||
main_column = sub_events[0]['column']
|
||||
else:
|
||||
|
|
@ -317,7 +331,6 @@ def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
|||
e_value,
|
||||
next_type,
|
||||
next_value,
|
||||
AVG(time_from_previous) AS avg_time_from_previous,
|
||||
COUNT(1) AS sessions_count
|
||||
FROM ranked_events
|
||||
WHERE event_number_in_session = 1
|
||||
|
|
@ -330,8 +343,7 @@ def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
|||
e_value,
|
||||
next_type,
|
||||
next_value,
|
||||
sessions_count,
|
||||
avg_time_from_previous
|
||||
sessions_count
|
||||
FROM n1"""]
|
||||
for i in range(2, data.density + 1):
|
||||
steps_query.append(f"""n{i} AS (SELECT *
|
||||
|
|
@ -340,7 +352,6 @@ def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
|||
re.e_value AS e_value,
|
||||
re.next_type AS next_type,
|
||||
re.next_value AS next_value,
|
||||
AVG(re.time_from_previous) AS avg_time_from_previous,
|
||||
COUNT(1) AS sessions_count
|
||||
FROM n{i - 1} INNER JOIN ranked_events AS re
|
||||
ON (n{i - 1}.next_value = re.e_value AND n{i - 1}.next_type = re.event_type)
|
||||
|
|
@ -353,8 +364,7 @@ def path_analysis(project_id: int, data: schemas.CardPathAnalysis):
|
|||
e_value,
|
||||
next_type,
|
||||
next_value,
|
||||
sessions_count,
|
||||
avg_time_from_previous
|
||||
sessions_count
|
||||
FROM n{i}""")
|
||||
|
||||
with ch_client.ClickHouseClient(database="experimental") as ch:
|
||||
|
|
@ -382,7 +392,7 @@ WITH {initial_sessions_cte}
|
|||
FROM {main_events_table} {"INNER JOIN sub_sessions ON (sub_sessions.session_id = events.session_id)" if len(sessions_conditions) > 0 else ""}
|
||||
WHERE {" AND ".join(ch_sub_query)}
|
||||
) AS full_ranked_events
|
||||
WHERE event_number_in_session <= %(density)s)
|
||||
WHERE {" AND ".join(step_1_post_conditions)})
|
||||
SELECT *
|
||||
FROM pre_ranked_events;"""
|
||||
logger.debug("---------Q1-----------")
|
||||
|
|
@ -404,11 +414,7 @@ WITH pre_ranked_events AS (SELECT *
|
|||
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_value,
|
||||
leadInFrame(toNullable(event_type))
|
||||
OVER (PARTITION BY session_id ORDER BY datetime {path_direction}
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_type,
|
||||
abs(lagInFrame(toNullable(datetime))
|
||||
OVER (PARTITION BY session_id ORDER BY datetime {path_direction}
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
|
||||
- pre_ranked_events.datetime) AS time_from_previous
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS next_type
|
||||
FROM start_points INNER JOIN pre_ranked_events USING (session_id))
|
||||
SELECT *
|
||||
FROM ranked_events;"""
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from queue import Queue, Empty
|
|||
|
||||
import clickhouse_connect
|
||||
from clickhouse_connect.driver.query import QueryContext
|
||||
from clickhouse_connect.driver.exceptions import DatabaseError
|
||||
from decouple import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -32,9 +31,10 @@ if config("CH_COMPRESSION", cast=bool, default=True):
|
|||
extra_args["compression"] = "lz4"
|
||||
|
||||
|
||||
def transform_result(original_function):
|
||||
def transform_result(self, original_function):
|
||||
@wraps(original_function)
|
||||
def wrapper(*args, **kwargs):
|
||||
logger.debug(self.format(query=kwargs.get("query"), parameters=kwargs.get("parameters")))
|
||||
result = original_function(*args, **kwargs)
|
||||
if isinstance(result, clickhouse_connect.driver.query.QueryResult):
|
||||
column_names = result.column_names
|
||||
|
|
@ -140,7 +140,7 @@ class ClickHouseClient:
|
|||
else:
|
||||
self.__client = CH_pool.get_connection()
|
||||
|
||||
self.__client.execute = transform_result(self.__client.query)
|
||||
self.__client.execute = transform_result(self, self.__client.query)
|
||||
self.__client.format = self.format
|
||||
|
||||
def __enter__(self):
|
||||
|
|
|
|||
|
|
@ -1209,10 +1209,10 @@ class CardPathAnalysis(__CardSchema):
|
|||
if len(s.value) == 0:
|
||||
continue
|
||||
start_point.append(s)
|
||||
self.metric_value.append(s.type)
|
||||
# self.metric_value.append(s.type)
|
||||
|
||||
self.start_point = start_point
|
||||
self.metric_value = remove_duplicate_values(self.metric_value)
|
||||
# self.metric_value = remove_duplicate_values(self.metric_value)
|
||||
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-alpine
|
||||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk add --no-cache build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec tini
|
||||
|
||||
|
|
@ -13,10 +13,11 @@ ENV SOURCE_MAP_VERSION=0.7.4 \
|
|||
WORKDIR /work
|
||||
COPY requirements.txt ./requirements.txt
|
||||
# Caching the source build
|
||||
RUN pip install --no-cache-dir --upgrade uv
|
||||
RUN uv pip install --no-cache-dir --upgrade pip setuptools wheel --system
|
||||
RUN uv pip install --no-cache-dir --upgrade python3-saml==1.16.0 --no-binary=lxml --system
|
||||
RUN uv pip install --no-cache-dir --upgrade -r requirements.txt --system
|
||||
#RUN pip install --no-cache-dir --upgrade uv
|
||||
#RUN uv pip install --no-cache-dir --upgrade pip setuptools wheel --system
|
||||
#RUN uv pip install --no-cache-dir --upgrade python3-saml==1.16.0 --no-binary=lxml --system
|
||||
#RUN uv pip install --no-cache-dir --upgrade -r requirements.txt --system
|
||||
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
RUN mv env.default .env
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-alpine
|
||||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk add --no-cache build-base tini
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-alpine
|
||||
FROM python:3.12-alpine
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apk add --no-cache build-base tini
|
||||
|
|
|
|||
|
|
@ -11,5 +11,3 @@ if config("EXP_ERRORS_SEARCH", cast=bool, default=False):
|
|||
from . import errors_details_exp as errors_details
|
||||
else:
|
||||
from . import errors
|
||||
|
||||
from . import errors_viewed_ee as errors_viewed
|
||||
|
|
|
|||
175
frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx
Normal file
175
frontend/app/components/Client/Projects/ProjectCodeSnippet.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Checkbox, Loader, Toggler } from 'UI';
|
||||
import GDPR from 'App/mstore/types/gdpr';
|
||||
import cn from 'classnames';
|
||||
import stl from './projectCodeSnippet.module.css';
|
||||
import Select from 'Shared/Select';
|
||||
import CodeSnippet from 'Shared/CodeSnippet';
|
||||
import CircleNumber from 'Components/Onboarding/components/CircleNumber';
|
||||
import Project from '@/mstore/types/project';
|
||||
|
||||
interface InputModeOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const inputModeOptions: InputModeOption[] = [
|
||||
{ label: 'Record all inputs', value: 'plain' },
|
||||
{ label: 'Ignore all inputs', value: 'obscured' },
|
||||
{ label: 'Obscure all inputs', value: 'hidden' }
|
||||
];
|
||||
|
||||
const inputModeOptionsMap: Record<string, number> = {};
|
||||
inputModeOptions.forEach((o, i) => (inputModeOptionsMap[o.value] = i));
|
||||
|
||||
interface Props {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
const ProjectCodeSnippet: React.FC = (props: Props) => {
|
||||
const { projectsStore } = useStore();
|
||||
const siteId = projectsStore.siteId;
|
||||
const site = props.project;
|
||||
const gdpr = site.gdpr as GDPR;
|
||||
const sites = projectsStore.list;
|
||||
const editGDPR = projectsStore.editGDPR;
|
||||
const onSaveGDPR = projectsStore.saveGDPR;
|
||||
const init = projectsStore.initProject;
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [isAssistEnabled, setAssistEnabled] = useState(false);
|
||||
const [showLoader, setShowLoader] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSite = sites.find((s) => s.id === siteId);
|
||||
if (currentSite) {
|
||||
init(currentSite);
|
||||
}
|
||||
}, [init, siteId, sites]);
|
||||
|
||||
const saveGDPR = () => {
|
||||
setChanged(true);
|
||||
void onSaveGDPR(site.id);
|
||||
};
|
||||
|
||||
const onChangeSelect = (data: { name: string; value: string }) => {
|
||||
editGDPR({ [data.name]: data.value });
|
||||
saveGDPR();
|
||||
};
|
||||
|
||||
const onChangeOption = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, checked } = event.target;
|
||||
editGDPR({ [name]: checked });
|
||||
saveGDPR();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoader(true);
|
||||
const timer = setTimeout(() => {
|
||||
setShowLoader(false);
|
||||
}, 200);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isAssistEnabled]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="font-semibold mb-2 flex items-center">
|
||||
<CircleNumber text="1" /> Choose data recording options
|
||||
</div>
|
||||
|
||||
<div className="ml-10 mb-4" style={{ maxWidth: '50%' }}>
|
||||
<Select
|
||||
name="defaultInputMode"
|
||||
options={inputModeOptions}
|
||||
onChange={({ value }) =>
|
||||
onChangeSelect({ name: 'defaultInputMode', value: value.value })
|
||||
}
|
||||
placeholder="Default Input Mode"
|
||||
defaultValue={gdpr.defaultInputMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx-4" />
|
||||
<div className="flex items-center ml-10">
|
||||
<Checkbox
|
||||
name="maskNumbers"
|
||||
type="checkbox"
|
||||
checked={gdpr.maskNumbers}
|
||||
onChange={onChangeOption}
|
||||
className="mr-2"
|
||||
label="Do not record any numeric text"
|
||||
/>
|
||||
|
||||
<div className="mx-4" />
|
||||
|
||||
<Checkbox
|
||||
name="maskEmails"
|
||||
type="checkbox"
|
||||
checked={gdpr.maskEmails}
|
||||
onChange={onChangeOption}
|
||||
className="mr-2"
|
||||
label="Do not record email addresses"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(stl.info, 'rounded bg-gray mt-2 mb-4 ml-10', { hidden: !changed })}>
|
||||
Below code snippet changes depending on the data recording options chosen.
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.instructions, 'mt-8')}>
|
||||
<div className="font-semibold flex items-center">
|
||||
<CircleNumber text="2" />
|
||||
<span>Enable Assist (Optional)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-10">
|
||||
<p>
|
||||
OpenReplay Assist allows you to support your users by seeing their live screen and
|
||||
instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen
|
||||
sharing software.
|
||||
</p>
|
||||
<Toggler
|
||||
label="Yes"
|
||||
checked={isAssistEnabled}
|
||||
name="test"
|
||||
className="font-medium mr-2"
|
||||
onChange={() => setAssistEnabled(!isAssistEnabled)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn(stl.instructions, 'mt-8')}>
|
||||
<div className="font-semibold flex items-center">
|
||||
<CircleNumber text="3" />
|
||||
<span>Install SDK</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-10 mb-2">
|
||||
Paste this snippet <span>{'before the '}</span>
|
||||
<span className={stl.highLight}> {'</head>'} </span>
|
||||
<span>{' tag of your page.'}</span>
|
||||
</div>
|
||||
<div className={cn(stl.snippetsWrapper, 'ml-10')}>
|
||||
{showLoader ? (
|
||||
<div style={{ height: '474px' }}>
|
||||
<Loader loading={true} />
|
||||
</div>
|
||||
) : (
|
||||
<CodeSnippet
|
||||
isAssistEnabled={isAssistEnabled}
|
||||
host={site?.host}
|
||||
projectKey={site?.projectKey!}
|
||||
ingestPoint={`"https://${window.location.hostname}/ingest"`}
|
||||
defaultInputMode={gdpr.defaultInputMode}
|
||||
obscureTextNumbers={gdpr.maskNumbers}
|
||||
obscureTextEmails={gdpr.maskEmails}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(ProjectCodeSnippet);
|
||||
|
|
@ -15,37 +15,29 @@ function ProjectForm(props: Props) {
|
|||
const [form] = Form.useForm();
|
||||
const { onClose } = props;
|
||||
const { projectsStore } = useStore();
|
||||
const project = projectsStore.instance as Project;
|
||||
const [project, setProject] = React.useState<Project>(new Project(props.project || {}));
|
||||
const loading = projectsStore.loading;
|
||||
const canDelete = projectsStore.list.length > 1;
|
||||
const pathname = window.location.pathname;
|
||||
// const pathname = window.location.pathname;
|
||||
const mstore = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.project && props.project.id) {
|
||||
projectsStore.initProject(props.project);
|
||||
} else {
|
||||
projectsStore.initProject({});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEdit = ({ target: { name, value } }: ChangeEvent<HTMLInputElement>) => {
|
||||
projectsStore.editInstance({ [name]: value });
|
||||
setProject((prev: Project) => (new Project({ ...prev, [name]: value })));
|
||||
};
|
||||
|
||||
const onSubmit = (e: FormEvent) => {
|
||||
if (!projectsStore.instance) return;
|
||||
if (projectsStore.instance.id && projectsStore.instance.exists()) {
|
||||
if (!project) return;
|
||||
if (project.id && project.exists()) {
|
||||
projectsStore
|
||||
.updateProject(projectsStore.instance.id, project)
|
||||
.updateProject(project.id, project)
|
||||
.then((response: any) => {
|
||||
if (!response || !response.errors || response.errors.size === 0) {
|
||||
if (onClose) {
|
||||
onClose(null);
|
||||
}
|
||||
if (!pathname.includes('onboarding')) {
|
||||
void projectsStore.fetchList();
|
||||
}
|
||||
// if (!pathname.includes('onboarding')) {
|
||||
// void projectsStore.fetchList();
|
||||
// }
|
||||
toast.success('Project updated successfully');
|
||||
} else {
|
||||
toast.error(response.errors[0]);
|
||||
|
|
@ -53,14 +45,16 @@ function ProjectForm(props: Props) {
|
|||
});
|
||||
} else {
|
||||
projectsStore
|
||||
.save(projectsStore.instance!)
|
||||
.then(() => {
|
||||
.save(project!)
|
||||
.then((resp: Project) => {
|
||||
toast.success('Project created successfully');
|
||||
onClose?.(null);
|
||||
|
||||
mstore.searchStore.clearSearch();
|
||||
mstore.searchStoreLive.clearSearch();
|
||||
mstore.initClient();
|
||||
|
||||
projectsStore.setConfigProject(parseInt(resp.id!));
|
||||
})
|
||||
.catch((error: string) => {
|
||||
toast.error(error || 'An error occurred while creating the project');
|
||||
|
|
@ -77,6 +71,7 @@ function ProjectForm(props: Props) {
|
|||
if (onClose) {
|
||||
onClose(null);
|
||||
}
|
||||
projectsStore.setConfigProject(parseInt(projectStore.list[0].id!));
|
||||
if (project.id === projectsStore.active?.id) {
|
||||
projectsStore.setSiteId(projectStore.list[0].id!);
|
||||
}
|
||||
|
|
@ -85,14 +80,16 @@ function ProjectForm(props: Props) {
|
|||
});
|
||||
};
|
||||
|
||||
console.log('ProjectForm', project);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
onFinish={onSubmit}
|
||||
initialValues={{ ...project }}
|
||||
>
|
||||
|
||||
<Form.Item
|
||||
label="Name"
|
||||
name="name"
|
||||
|
|
@ -121,7 +118,8 @@ function ProjectForm(props: Props) {
|
|||
]}
|
||||
value={project.platform}
|
||||
onChange={(value) => {
|
||||
projectsStore.editInstance({ platform: value });
|
||||
// projectsStore.editInstance({ platform: value });
|
||||
setProject((prev: Project) => (new Project({ ...prev, platform: value })));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Avatar, Input, Menu, MenuProps, Progress } from 'antd';
|
||||
import { Avatar, Button, Input, Menu, MenuProps, Progress, Typography } from 'antd';
|
||||
import { useStore } from '@/mstore';
|
||||
import Project from '@/mstore/types/project';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { AppWindowMac, Smartphone } from 'lucide-react';
|
||||
import { AppWindowMac, EditIcon, Smartphone } from 'lucide-react';
|
||||
import { PencilIcon } from '.store/lucide-react-virtual-3cff663764/package';
|
||||
import ProjectForm from 'Components/Client/Projects/ProjectForm';
|
||||
import { useModal } from 'Components/ModalContext';
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
|
||||
const ProjectList: React.FC = () => {
|
||||
const { projectsStore } = useStore();
|
||||
const [search, setSearch] = React.useState('');
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
const filteredProjects = projectsStore.list.filter((project: Project) =>
|
||||
project.name.toLowerCase().includes(search.toLowerCase())
|
||||
|
|
@ -22,9 +26,23 @@ const ProjectList: React.FC = () => {
|
|||
projectsStore.setConfigProject(pid);
|
||||
};
|
||||
|
||||
const projectEditHandler = (e: React.MouseEvent, project: Project) => {
|
||||
// e.stopPropagation();
|
||||
|
||||
projectsStore.initProject(project);
|
||||
|
||||
openModal(<ProjectForm onClose={closeModal} project={project} />, {
|
||||
title: 'Edit Project'
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const menuItems: MenuItem[] = filteredProjects.map((project) => ({
|
||||
key: project.id + '',
|
||||
label: project.name,
|
||||
label: <Typography.Text style={{ color: 'inherit' }} ellipsis={true}>{project.name}</Typography.Text>,
|
||||
extra: <Button onClick={(e) => projectEditHandler(e, project)} className="flex opacity-0 group-hover:!opacity-100"
|
||||
size="small" type="link" icon={<PencilIcon size={14} />} />,
|
||||
className: 'group',
|
||||
icon: (
|
||||
<ProjectIconWithProgress
|
||||
platform={project.platform}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import React from 'react';
|
||||
import Project from '@/mstore/types/project';
|
||||
import InstallMobileDocs from 'Shared/TrackingCodeModal/InstallIosDocs';
|
||||
import { Tabs } from 'UI';
|
||||
import ProjectCodeSnippet from 'Shared/TrackingCodeModal/ProjectCodeSnippet/ProjectCodeSnippet';
|
||||
import InstallDocs from 'Shared/TrackingCodeModal/InstallDocs';
|
||||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
|
||||
const PROJECT = 'Using Script';
|
||||
const DOCUMENTATION = 'Using NPM';
|
||||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
import InstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs';
|
||||
import ProjectCodeSnippet from 'Components/Client/Projects/ProjectCodeSnippet';
|
||||
import MobileInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/MobileInstallDocs';
|
||||
import { Segmented } from 'antd';
|
||||
import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs';
|
||||
|
||||
const JAVASCRIPT = 'Using Script';
|
||||
const NPM = 'Using NPM';
|
||||
const TABS = [
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION },
|
||||
{ key: PROJECT, text: PROJECT }
|
||||
{ key: NPM, text: NPM },
|
||||
{ key: JAVASCRIPT, text: JAVASCRIPT }
|
||||
];
|
||||
|
||||
interface Props {
|
||||
|
|
@ -20,35 +23,70 @@ interface Props {
|
|||
function ProjectTabTracking(props: Props) {
|
||||
usePageTitle('Installation - OpenReplay Preferences');
|
||||
const { project } = props;
|
||||
const [activeTab, setActiveTab] = React.useState(PROJECT);
|
||||
const ingestPoint = `https://${window.location.hostname}/ingest`;
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case PROJECT:
|
||||
return <ProjectCodeSnippet site={project} />;
|
||||
case DOCUMENTATION:
|
||||
return <InstallDocs site={project} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{project.platform === 'ios' ? (
|
||||
<InstallMobileDocs site={project} ingestPoint={ingestPoint} />
|
||||
{project.platform !== 'web' ? (
|
||||
<MobileSnippet project={project} />
|
||||
) : (
|
||||
<div>
|
||||
<Tabs
|
||||
tabs={TABS}
|
||||
active={activeTab}
|
||||
onClick={(tab: string) => setActiveTab(tab)}
|
||||
/>
|
||||
<div className="p-5">{renderActiveTab()}</div>
|
||||
</div>
|
||||
<WebSnippet project={project} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectTabTracking;
|
||||
|
||||
function WebSnippet({ project }: { project: Project }) {
|
||||
const [isNpm, setIsNpm] = React.useState(true);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'Using NPM', value: true },
|
||||
{ label: 'Using Script', value: false }
|
||||
]}
|
||||
value={isNpm}
|
||||
onChange={setIsNpm}
|
||||
block={true}
|
||||
style={{ maxWidth: '200px' }}
|
||||
className="!align-middle"
|
||||
/>
|
||||
|
||||
{isNpm ? (
|
||||
<InstallDocs site={project} />
|
||||
) : (
|
||||
<ProjectCodeSnippet project={project} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSnippet({ project }: { project: Project }) {
|
||||
const [isIos, setIsIos] = React.useState(true);
|
||||
const ingestPoint = `https://${window.location.hostname}/ingest`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Segmented
|
||||
options={[
|
||||
{ label: 'iOS', value: true },
|
||||
{ label: 'Android', value: false }
|
||||
]}
|
||||
value={isIos}
|
||||
onChange={setIsIos}
|
||||
block={true}
|
||||
style={{ maxWidth: '150px' }}
|
||||
className="!align-middle"
|
||||
/>
|
||||
|
||||
|
||||
{isIos ? (
|
||||
<MobileInstallDocs site={project} ingestPoint={ingestPoint} />
|
||||
) : (
|
||||
<AndroidInstallDocs site={project} ingestPoint={ingestPoint} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
|
||||
@import 'zindex.css';
|
||||
|
||||
.modalHeader {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.highLight {
|
||||
background-color: rgba(204, 0, 0, 0.05);
|
||||
color: $red;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.snippetsWrapper {
|
||||
position: relative;
|
||||
& .codeCopy {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: $codeSnippet;
|
||||
padding: 5px 10px;
|
||||
color: $teal;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: all 0.4s;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
& .snippet {
|
||||
overflow: hidden;
|
||||
line-height: 18px;
|
||||
border-radius: 5px;
|
||||
user-select: none;
|
||||
& > div {
|
||||
background-color: $gray-lightest !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.siteInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
& span {
|
||||
color: $teal;
|
||||
}
|
||||
}
|
||||
.instructions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.siteId {
|
||||
font-weight: 500;
|
||||
& span {
|
||||
background: #f6f6f6;
|
||||
border-radius: 3px;
|
||||
padding: 2px 7px;
|
||||
font-weight: normal;
|
||||
margin-left: 4px;
|
||||
border: solid thin #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 5px 10px;
|
||||
background-color: #ffedd1;
|
||||
}
|
||||
|
||||
.number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ export default class ProjectsStore {
|
|||
if (index !== -1) {
|
||||
this.list[index] = this.list[index].edit(project);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getSiteId = () => {
|
||||
return {
|
||||
|
|
@ -105,7 +105,7 @@ export default class ProjectsStore {
|
|||
this.syncProjectInList({
|
||||
id: siteId,
|
||||
gdpr: response.data
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to sync project in list:', error);
|
||||
}
|
||||
|
|
@ -153,18 +153,18 @@ export default class ProjectsStore {
|
|||
this.setLoading(true);
|
||||
try {
|
||||
const response = await projectsService.saveProject(projectData);
|
||||
runInAction(() => {
|
||||
const newSite = new Project(response.data);
|
||||
const index = this.list.findIndex(site => site.id === newSite.id);
|
||||
if (index !== -1) {
|
||||
this.list[index] = newSite;
|
||||
} else {
|
||||
this.list.push(newSite);
|
||||
}
|
||||
this.setSiteId(newSite.id);
|
||||
this.active = newSite;
|
||||
});
|
||||
return response;
|
||||
|
||||
const newSite = new Project(response.data);
|
||||
const index = this.list.findIndex(site => site.id === newSite.id);
|
||||
if (index !== -1) {
|
||||
this.list[index] = newSite;
|
||||
} else {
|
||||
this.list.push(newSite);
|
||||
}
|
||||
this.setSiteId(newSite.id!);
|
||||
this.active = newSite;
|
||||
return newSite;
|
||||
|
||||
} catch (error: any) {
|
||||
throw error || 'An error occurred while saving the project.';
|
||||
} finally {
|
||||
|
|
@ -205,7 +205,10 @@ export default class ProjectsStore {
|
|||
updateProject = async (projectId: string, projectData: Partial<Project>) => {
|
||||
this.setLoading(true);
|
||||
try {
|
||||
const response = await projectsService.updateProject(projectId, projectData);
|
||||
const response = await projectsService.updateProject(projectId, {
|
||||
name: projectData.name,
|
||||
platform: projectData.platform
|
||||
});
|
||||
runInAction(() => {
|
||||
const updatedSite = new Project(response.data);
|
||||
const index = this.list.findIndex(site => site.id === updatedSite.id);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue