Merge branch 'dev' into live-se-red

This commit is contained in:
nick-delirium 2025-01-10 17:02:06 +01:00
commit 6c42bb0952
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
16 changed files with 440 additions and 116 deletions

View file

@ -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

View file

@ -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

View file

@ -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)}

View file

@ -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;"""

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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);

View file

@ -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>

View file

@ -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}

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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);