change(ui): projects revamtp (wip)

This commit is contained in:
Shekar Siri 2025-01-06 17:23:28 +01:00
parent 22d04436c0
commit 869b5c00c8
16 changed files with 543 additions and 266 deletions

View file

@ -8,6 +8,7 @@ import Integrations from './Integrations';
import UserView from './Users/UsersView';
import AuditView from './Audit/AuditView';
import Sites from './Sites';
import Projects from './Projects';
import CustomFields from './CustomFields';
import Webhooks from './Webhooks';
import Notifications from './Notifications';
@ -31,7 +32,7 @@ export default class Client extends React.PureComponent {
<Route exact strict path={clientRoute(CLIENT_TABS.SESSIONS_LISTING)} component={SessionsListingSettings} />
<Route exact strict path={clientRoute(CLIENT_TABS.INTEGRATIONS)} component={Integrations} />
<Route exact strict path={clientRoute(CLIENT_TABS.MANAGE_USERS)} component={UserView} />
<Route exact strict path={clientRoute(CLIENT_TABS.SITES)} component={Sites} />
<Route exact strict path={clientRoute(CLIENT_TABS.SITES)} component={Projects} />
<Route exact strict path={clientRoute(CLIENT_TABS.CUSTOM_FIELDS)} component={CustomFields} />
<Route exact strict path={clientRoute(CLIENT_TABS.WEBHOOKS)} component={Webhooks} />
<Route exact strict path={clientRoute(CLIENT_TABS.NOTIFICATIONS)} component={Notifications} />

View file

@ -1,10 +1,10 @@
import React, { useRef, useState } from 'react';
import { Form, Input, confirm } from 'UI';
import { Form, Input } from 'UI';
import styles from './customFieldForm.module.css';
import { useStore } from 'App/mstore';
import { useModal } from 'Components/Modal';
import { toast } from 'react-toastify';
import { Button } from 'antd';
import { Button, Modal } from 'antd';
import { Trash } from 'UI/Icons';
import { observer } from 'mobx-react-lite';
@ -23,16 +23,14 @@ const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
const exists = field?.exists();
const onDelete = async () => {
if (
await confirm({
header: 'Metadata',
confirmation: `Are you sure you want to remove?`
})
) {
store.remove(siteId, field?.index!).then(() => {
Modal.confirm({
title: 'Metadata',
content: `Are you sure you want to remove?`,
onOk: async () => {
await store.remove(siteId, field?.index!);
hideModal();
});
}
}
});
};
const onSave = (field: any) => {
@ -48,7 +46,7 @@ const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
toast.error('An error occurred while saving metadata.');
}).finally(() => {
setLoading(false);
})
});
};
return (

View file

@ -1,137 +0,0 @@
import React, { useEffect } from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import withPageTitle from 'HOCs/withPageTitle';
import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI';
import { init, fetchList, save, remove } from 'Duck/customField';
import SiteDropdown from 'Shared/SiteDropdown';
import styles from './customFields.module.css';
import CustomFieldForm from './CustomFieldForm';
import ListItem from './ListItem';
import { confirm } from 'UI';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { useModal } from 'App/components/Modal';
import { toast } from 'react-toastify';
function CustomFields(props) {
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
const [deletingItem, setDeletingItem] = React.useState(null);
const { showModal, hideModal } = useModal();
useEffect(() => {
const activeSite = props.sites.get(0);
if (!activeSite) return;
props.fetchList(activeSite.id);
}, []);
const save = (field) => {
props.save(currentSite.id, field).then((response) => {
if (!response || !response.errors || response.errors.size === 0) {
hideModal();
toast.success('Metadata added successfully!');
} else {
toast.error(response.errors[0]);
}
});
};
const init = (field) => {
props.init(field);
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => removeMetadata(field)} />);
};
const onChangeSelect = ({ value }) => {
const site = props.sites.find((s) => s.id === value.value);
setCurrentSite(site);
props.fetchList(site.id);
};
const removeMetadata = async (field) => {
if (
await confirm({
header: 'Metadata',
confirmation: `Are you sure you want to remove?`
})
) {
setDeletingItem(field.index);
props
.remove(currentSite.id, field.index)
.then(() => {
hideModal();
})
.finally(() => {
setDeletingItem(null);
});
}
};
const { fields, loading } = props;
return (
<div className="bg-white rounded-lg shadow-sm border p-5 ">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.size < 10}>
<Button disabled={fields.size >= 10} variant="primary" onClick={() => init()}>Add Metadata</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additonal user information in sessions.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">Learn more</a>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
{/* <div className="mt-4" /> */}
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.size === 0}
>
<div className={styles.list}>
{fields
.filter((i) => i.index)
.map((field) => (
<>
<ListItem
disabled={deletingItem && deletingItem === field.index}
key={field._key}
field={field}
onEdit={init}
// onDelete={ () => removeMetadata(field) }
/>
<Divider className="m-0" />
</>
))}
</div>
</NoContent>
</Loader>
</div>
);
}
export default connect(
(state) => ({
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
field: state.getIn(['customFields', 'instance']),
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
sites: state.getIn(['site', 'list']),
errors: state.getIn(['customFields', 'saveRequest', 'errors'])
}),
{
init,
fetchList,
save,
remove
}
)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields));

View file

@ -1,107 +1,96 @@
import React, { useEffect, useState } from 'react';
import cn from 'classnames';
import withPageTitle from 'HOCs/withPageTitle';
import { Button, Loader, NoContent, Icon, Tooltip, Divider } from 'UI';
import SiteDropdown from 'Shared/SiteDropdown';
import styles from './customFields.module.css';
import { NoContent } from 'UI';
import CustomFieldForm from './CustomFieldForm';
import ListItem from './ListItem';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { List, Space, Typography, Button, Tooltip } from 'antd';
import { PencilIcon, PlusIcon, Tags } from 'lucide-react';
import usePageTitle from '@/hooks/usePageTitle';
const CustomFields = () => {
usePageTitle('Metadata - OpenReplay Preferences');
const { customFieldStore: store, projectsStore } = useStore();
const sites = projectsStore.list;
const [currentSite, setCurrentSite] = useState(sites[0]);
const [deletingItem, setDeletingItem] = useState<number | null>(null);
const currentSite = projectsStore.config.project;
const { showModal, hideModal } = useModal();
const fields = store.list;
const [loading, setLoading] = useState(false);
useEffect(() => {
const activeSite = sites[0];
if (!activeSite) return;
setCurrentSite(activeSite);
setLoading(true);
store.fetchList(activeSite.id).finally(() => {
store.fetchList(currentSite?.id).finally(() => {
setLoading(false);
});
}, [sites]);
}, [currentSite]);
const handleInit = (field?: any) => {
store.init(field);
showModal(<CustomFieldForm siteId={currentSite.id} />, {
showModal(<CustomFieldForm siteId={currentSite?.projectId + ''} />, {
title: field ? 'Edit Metadata' : 'Add Metadata', right: true
});
};
const onChangeSelect = ({ value }: { value: { value: number } }) => {
const site = sites.find((s: any) => s.id === value.value);
setCurrentSite(site);
setLoading(true);
store.fetchList(site.id).finally(() => {
setLoading(false);
});
};
const remaining = 10 - fields.length;
return (
<div className="bg-white rounded-lg shadow-sm border p-5">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
<div className="ml-auto">
<Tooltip title="You've reached the limit of 10 metadata." disabled={fields.length < 10}>
<Button disabled={fields.length >= 10} variant="primary" onClick={() => handleInit()}>
Add Metadata
</Button>
</Tooltip>
</div>
</div>
<div className="text-base text-disabled-text flex px-5 items-center my-3">
<Icon name="info-circle-fill" className="mr-2" size={16} />
See additional user information in sessions.
<div className="flex flex-col gap-6">
<Typography.Text>
Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user
sessions. Learn More.
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">
Learn more
</a>
</div>
</Typography.Text>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
<div className="text-center my-4">None added yet</div>
</div>
}
size="small"
show={fields.length === 0}
<Space>
<Tooltip
title={remaining > 0 ? '' : 'You\'ve reached the limit of 10 metadata.'}
>
<div className={styles.list}>
{fields
.filter((i: any) => i.index)
.map((field: any) => (
<>
<ListItem
disabled={deletingItem !== null && deletingItem === field.index}
key={field._key}
field={field}
onEdit={handleInit}
/>
<Divider className="m-0" />
</>
))}
<Button icon={<PlusIcon size={18} />} type="primary"
disabled={remaining === 0}
onClick={() => handleInit()}>
Add Metadata
</Button>
</Tooltip>
{/*{remaining === 0 && <Icon name="info-circle" size={16} color="black" />}*/}
<Typography.Text type="secondary">
{remaining === 0 ? 'You have reached the limit of 10 metadata.' : `${remaining}/10 Remaining for this project`}
</Typography.Text>
</Space>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_METADATA} size={60} />
<div className="text-center my-4">None added yet</div>
</div>
</NoContent>
</Loader>
}
size="small"
show={fields.length === 0}
>
<List
loading={loading}
dataSource={fields}
renderItem={(field: any) => (
<List.Item
// disabled={deletingItem !== null && deletingItem === field.index}
onClick={() => handleInit(field)}
className="cursor-pointer group hover:bg-active-blue !px-4"
actions={[
<Button className="opacity-0 group-hover:!opacity-100" icon={<PencilIcon size={14} />} />
]}
>
<List.Item.Meta
title={field.key}
avatar={<Tags size={20} />}
/>
</List.Item>
)} />
</NoContent>
</div>
);
};
export default withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields));
export default observer(CustomFields);

View file

@ -0,0 +1,53 @@
import React from 'react';
import { Avatar, Input, List, 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';
function ProjectList() {
const { projectsStore } = useStore();
const list = projectsStore.list;
const [search, setSearch] = React.useState('');
const config = projectsStore.config;
console.log('config', config.pid);
const onSearch = (value: string) => {
setSearch(value);
};
const onProjectClick = (project: Project) => {
projectsStore.setConfigProject(project.projectId);
};
return (
<div>
<Input.Search placeholder="Search" onSearch={onSearch} />
<div className="my-3" />
<List
dataSource={list.filter((item) => item.name.toLowerCase().includes(search.toLowerCase()))}
renderItem={(item: Project) => (
<List.Item
key={item.id}
onClick={() => onProjectClick(item)}
className={`!py-2 mb-2 rounded-lg cursor-pointer !border-b-0 ${config.pid == item.projectId ? 'bg-teal-light' : 'bg-white'}`}
>
<List.Item.Meta
className="flex !items-center px-2 overflow-hidden"
avatar={
<Avatar
className="bg-tealx-light"
icon={item.platform === 'web' ? <AppWindowMac size={18} color="teal" /> :
<Smartphone size={18} color="teal" />}
/>
}
title={<Typography.Text className="capitalize truncate text-ellipsis">{item.name}</Typography.Text>}
/>
</List.Item>
)}
/>
</div>
);
}
export default observer(ProjectList);

View file

@ -0,0 +1,30 @@
import React from 'react';
import { useStore } from '@/mstore';
import { observer } from 'mobx-react-lite';
import ProjectTabTracking from 'Components/Client/Projects/ProjectTabTracking';
import CustomFields from 'Components/Client/CustomFields';
import ProjectTags from 'Components/Client/Projects/ProjectTags';
function ProjectTabContent() {
const { projectsStore } = useStore();
const { pid, tab } = projectsStore.config;
const tabContent: Record<string, React.ReactNode> = React.useMemo(() => {
const project = projectsStore.list.find((p) => p.projectId == pid);
return {
installation: <ProjectTabTracking project={project!} />,
captureRate: <div>Capture Rate Content</div>,
metadata: <CustomFields />,
tags: <ProjectTags />,
groupKeys: <div>Group Keys Content</div>
};
}, [pid, projectsStore.list]);
return (
<div>
{tabContent[tab]}
</div>
);
}
export default observer(ProjectTabContent);

View file

@ -0,0 +1,54 @@
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';
const TABS = [
{ key: DOCUMENTATION, text: DOCUMENTATION },
{ key: PROJECT, text: PROJECT }
];
interface Props {
project: Project;
}
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} />
) : (
<div>
<Tabs
tabs={TABS}
active={activeTab}
onClick={(tab: string) => setActiveTab(tab)}
/>
<div className="p-5">{renderActiveTab()}</div>
</div>
)}
</div>
);
}
export default ProjectTabTracking;

View file

@ -0,0 +1,38 @@
import React from 'react';
import { Tabs } from 'antd';
import { useStore } from '@/mstore';
import { observer } from 'mobx-react-lite';
function ProjectTabs() {
const { projectsStore } = useStore();
const activeTab = projectsStore.config.tab;
const tabItems = [
{ key: 'installation', label: 'Installation', content: <div>Installation Content</div> },
{ key: 'captureRate', label: 'Capture Rate', content: <div>Capture Rate Content</div> },
{ key: 'metadata', label: 'Metadata', content: <div>Metadata Content</div> },
{ key: 'tags', label: 'Tags', content: <div>Tags Content</div> },
{ key: 'groupKeys', label: 'Group Keys', content: <div>Group Keys Content</div> }
];
const onTabChange = (key: string) => {
projectsStore.setConfigTab(key);
};
return (
<Tabs
type="line"
defaultActiveKey="installation"
activeKey={activeTab}
style={{ borderBottom: 'none' }}
onChange={onTabChange}
items={tabItems.map((tab) => ({
key: tab.key,
label: tab.label
// children: tab.content,
}))}
/>
);
}
export default observer(ProjectTabs);

View file

@ -0,0 +1,65 @@
import React, { useEffect } from 'react';
import { useStore } from '@/mstore';
import { List, Button, Typography, Space } from 'antd';
import { observer } from 'mobx-react-lite';
import { PencilIcon, ScanSearch } from 'lucide-react';
import { useModal } from 'Components/ModalContext';
import TagForm from 'Components/Client/Projects/TagForm';
function ProjectTags() {
const { tagWatchStore, projectsStore } = useStore();
const list = tagWatchStore.tags;
const { openModal } = useModal();
const { pid } = projectsStore.config;
useEffect(() => {
void tagWatchStore.getTags(pid);
}, [pid]);
const handleInit = (tag?: any) => {
openModal(<TagForm tag={tag} projectId={pid!} />, {
title: tag ? 'Edit Tag' : 'Add Tag'
});
};
return (
<div className="flex flex-col gap-6">
<Space direction="vertical">
<Typography.Text>
Manage Tag Elements here. Rename tags for easy identification or delete those you no longer need.
</Typography.Text>
<ul className="!list-disc list-inside">
<li><Typography.Text>To create new tags, navigate to the Tags tab while playing a session.</Typography.Text>
</li>
<li><Typography.Text>Use tags in OmniSearch to quickly find relevant sessions.</Typography.Text>
</li>
</ul>
</Space>
<List
loading={tagWatchStore.isLoading}
dataSource={list}
renderItem={(item) => (
<List.Item
className="cursor-pointer group hover:bg-active-blue !px-4"
actions={[
<Button className="opacity-0 group-hover:!opacity-100" icon={<PencilIcon size={14} />} />
]}
onClick={() => handleInit(item)}
>
<List.Item.Meta
title={item.name}
avatar={<ScanSearch size={20} />}
/>
</List.Item>
)}
pagination={{
pageSize: 5,
showSizeChanger: false,
size: 'small'
}}
/>
</div>
);
}
export default observer(ProjectTags);

View file

@ -0,0 +1,70 @@
import React from 'react';
import { Button, Card, Col, Divider, Row, Space, Typography } from 'antd';
import ProjectList from 'Components/Client/Projects/ProjectList';
import ProjectTabs from 'Components/Client/Projects/ProjectTabs';
import { useHistory } from 'react-router-dom';
import { useStore } from '@/mstore';
import { observer } from 'mobx-react-lite';
import { PlusIcon } from 'lucide-react';
import ProjectTabContent from 'Components/Client/Projects/ProjectTabContent';
function Projects() {
const { projectsStore } = useStore();
const history = useHistory();
const { project, pid, tab } = projectsStore.config;
React.useEffect(() => {
const params = new URLSearchParams(history.location.search);
const pid = params.get('pid');
const tab = params.get('tab');
projectsStore.setConfigProject(pid ? parseInt(pid) : undefined);
projectsStore.setConfigTab(tab);
}, []);
React.useEffect(() => {
const params = new URLSearchParams(history.location.search);
if (projectsStore.config.pid) {
params.set('pid', projectsStore.config.pid + '');
}
if (projectsStore.config.tab) {
params.set('tab', projectsStore.config.tab);
}
history.push({ search: params.toString() });
}, [pid, tab]);
return (
<Card
title="Projects"
classNames={{
cover: '!rounded-lg border shadow-sm',
body: '!p-0'
}}
style={{ height: 'calc(100vh - 140px)' }}
extra={
<Space>
<Button type="primary" onClick={() => projectsStore.setConfigProject(undefined)} icon={<PlusIcon />}>Create
Project</Button>
</Space>
}
>
<Row className="items-stretch">
<Col span={6} className="border-r !p-4">
<ProjectList />
</Col>
<Col span={18} className="!p-4 !overflow-hidden">
<Space className="flex justify-between">
<Typography.Title level={5} className="capitalize !m-0">{project?.name}</Typography.Title>
<ProjectTabs />
</Space>
<Divider />
<div className="!overflow-y-auto">
{project && <ProjectTabContent />}
</div>
</Col>
</Row>
</Card>
);
}
export default observer(Projects);

View file

@ -0,0 +1,73 @@
import React from 'react';
import { Button, Form, Input, Space, Modal } from 'antd';
import { Trash } from 'UI/Icons';
import { useStore } from '@/mstore';
import { useModal } from 'Components/ModalContext';
interface Props {
tag: any;
projectId: number;
}
function TagForm(props: Props) {
const { tag, projectId } = props;
const { tagWatchStore } = useStore();
const [name, setName] = React.useState(tag.name);
const [loading, setLoading] = React.useState(false);
const { closeModal } = useModal();
const write = ({ target: { value, name } }: any) => {
setName(value);
};
const onDelete = async () => {
Modal.confirm({
title: 'Tag',
content: `Are you sure you want to remove?`,
onOk: async () => {
await tagWatchStore.deleteTag(tag.tagId, projectId);
closeModal();
}
});
};
const onSave = () => {
void tagWatchStore.updateTagName(tag.tagId, name, projectId);
};
return (
<Form layout="vertical">
<Form.Item label="Name:">
<Input
autoFocus
name="name"
value={name}
onChange={write}
placeholder="Name"
maxLength={50}
/>
</Form.Item>
<div className="flex justify-between">
<Space>
<Button
onClick={onSave}
disabled={name.length === 0}
loading={loading}
type="primary"
className="float-left mr-2"
>
Update
</Button>
<Button onClick={closeModal}>
Cancel
</Button>
</Space>
<Button type="text" icon={<Trash />} onClick={onDelete}></Button>
</div>
</Form>
);
}
export default TagForm;

View file

@ -0,0 +1 @@
export { default } from './Projects';

View file

@ -0,0 +1,9 @@
import React from 'react';
function CaptureRateSettings() {
return (
<div></div>
);
}
export default CaptureRateSettings;

View file

@ -2,7 +2,13 @@ import { makeAutoObservable, runInAction } from 'mobx';
import Project from './types/project';
import GDPR from './types/gdpr';
import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
import { projectsService } from "App/services";
import { projectsService } from 'App/services';
interface Config {
project: Project | null;
pid: number | undefined;
tab: string;
}
export default class ProjectsStore {
list: Project[] = [];
@ -11,6 +17,11 @@ export default class ProjectsStore {
active: Project | null = null;
sitesLoading = true;
loading = false;
config: Config = {
project: null,
pid: undefined,
tab: 'installation'
};
constructor() {
const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY);
@ -25,42 +36,42 @@ export default class ProjectsStore {
getSiteId = () => {
return {
siteId: this.siteId,
active: this.active,
active: this.active
};
}
};
initProject = (project: Partial<Project>) => {
this.instance = new Project(project);
}
};
setSitesLoading = (loading: boolean) => {
this.sitesLoading = loading;
}
};
setLoading = (loading: boolean) => {
this.loading = loading;
}
};
setSiteId = (siteId: string) => {
localStorage.setItem(SITE_ID_STORAGE_KEY, siteId.toString());
this.siteId = siteId;
this.active = this.list.find((site) => site.id! === siteId) ?? null;
}
};
editGDPR = (gdprData: Partial<GDPR>) => {
if (this.instance) {
this.instance.gdpr.edit(gdprData);
}
}
};
editInstance = (instance: Partial<Project>) => {
if (!this.instance) return;
this.instance = this.instance.edit(instance);
}
};
fetchGDPR = async (siteId: string) => {
try {
const response = await projectsService.fetchGDPR(siteId)
const response = await projectsService.fetchGDPR(siteId);
runInAction(() => {
if (this.instance) {
Object.assign(this.instance.gdpr, response.data);
@ -69,7 +80,7 @@ export default class ProjectsStore {
} catch (error) {
console.error('Failed to fetch GDPR:', error);
}
}
};
saveGDPR = async (siteId: string) => {
if (!this.instance) return;
@ -80,7 +91,7 @@ export default class ProjectsStore {
} catch (error) {
console.error('Failed to save GDPR:', error);
}
}
};
fetchList = async (siteIdFromPath?: string) => {
this.setSitesLoading(true);
@ -115,7 +126,7 @@ export default class ProjectsStore {
} finally {
this.setSitesLoading(false);
}
}
};
save = async (projectData: Partial<Project>) => {
this.setLoading(true);
@ -137,7 +148,7 @@ export default class ProjectsStore {
} finally {
this.setLoading(false);
}
}
};
updateProjectRecordingStatus = (siteId: string, status: boolean) => {
const site = this.list.find(site => site.id === siteId);
@ -150,7 +161,7 @@ export default class ProjectsStore {
localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS);
}
}
}
};
removeProject = async (projectId: string) => {
this.setLoading(true);
@ -161,13 +172,13 @@ export default class ProjectsStore {
if (this.siteId === projectId) {
this.setSiteId(this.list[0].id!);
}
})
});
} catch (e) {
console.error('Failed to remove project:', e);
} finally {
this.setLoading(false);
}
}
};
updateProject = async (projectId: string, projectData: Partial<Project>) => {
this.setLoading(true);
@ -183,7 +194,24 @@ export default class ProjectsStore {
} catch (error) {
console.error('Failed to update site:', error);
} finally {
this.setLoading(false)
this.setLoading(false);
}
}
};
setConfigProject = (pid?: number) => {
if (!pid) {
const firstProject = this.list[0];
this.config.pid = firstProject?.projectId ?? undefined;
this.config.project = firstProject ?? null;
return;
}
const project = this.list.find(site => site.projectId === pid);
this.config.pid = project?.projectId ?? undefined;
this.config.project = project ?? null;
};
setConfigTab = (tab: string | null) => {
this.config.tab = tab ?? 'installation';
};
}

View file

@ -1,6 +1,7 @@
import { makeAutoObservable } from 'mobx';
import { tagWatchService } from 'App/services';
import { CreateTag, Tag } from 'App/services/TagWatchService';
import { projectStore } from '@/mstore';
export default class TagWatchStore {
tags: Tag[] = [];
@ -18,13 +19,14 @@ export default class TagWatchStore {
this.isLoading = loading;
};
getTags = async () => {
getTags = async (projectId?: number) => {
if (this.isLoading) {
return;
}
this.setLoading(true);
try {
const tags: Tag[] = await tagWatchService.getTags();
const pid = projectId || projectStore.active?.projectId;
const tags: Tag[] = await tagWatchService.getTags(pid!);
this.setTags(tags);
return tags;
} catch (e) {
@ -34,28 +36,31 @@ export default class TagWatchStore {
}
};
createTag = async (data: CreateTag) => {
createTag = async (data: CreateTag, projectId?: number) => {
try {
const tagId: number = await tagWatchService.createTag(data);
const pid = projectId || projectStore.active?.projectId;
const tagId: number = await tagWatchService.createTag(pid!, data);
return tagId;
} catch (e) {
console.error(e);
}
};
deleteTag = async (id: number) => {
deleteTag = async (id: number, projectId?: number) => {
try {
await tagWatchService.deleteTag(id);
const pid = projectId || projectStore.active?.projectId;
await tagWatchService.deleteTag(pid!, id);
this.setTags(this.tags.filter((t) => t.tagId !== id));
} catch (e) {
console.error(e);
}
};
updateTagName = async (id: number, name: string) => {
updateTagName = async (id: number, name: string, projectId?: number) => {
try {
await tagWatchService.updateTagName(id, name);
const updatedTag = this.tags.find((t) => t.tagId === id)
const pid = projectId || projectStore.active?.projectId;
await tagWatchService.updateTagName(pid!, id, name);
const updatedTag = this.tags.find((t) => t.tagId === id);
if (updatedTag) {
this.setTags(this.tags.map((t) => t.tagId === id ? { ...updatedTag, name } : t));
}

View file

@ -12,27 +12,27 @@ export interface Tag extends CreateTag {
}
export default class TagWatchService extends BaseService {
createTag(data: CreateTag) {
return this.client.post('/tags', data)
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
async createTag(projectId: number, data: CreateTag) {
const r = await this.client.post(`/${projectId}/tags`, data);
const response = await r.json();
return response.data || {};
}
getTags() {
return this.client.get('/tags')
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
async getTags(projectId: number) {
const r = await this.client.get(`/${projectId}/tags`);
const response = await r.json();
return response.data || {};
}
deleteTag(id: number) {
return this.client.delete(`/tags/${id}`)
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
async deleteTag(projectId: number, id: number) {
const r = await this.client.delete(`/${projectId}/tags/${id}`);
const response = await r.json();
return response.data || {};
}
updateTagName(id: number, name: string) {
return this.client.put(`/tags/${id}`, { name })
.then(r => r.json())
.then((response: { data: any; }) => response.data || {})
async updateTagName(projectId: number, id: number, name: string) {
const r = await this.client.put(`/${projectId}/tags/${id}`, { name });
const response = await r.json();
return response.data || {};
}
}
}