Compare commits
9 commits
main
...
ui-project
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b0ca4af5 | ||
|
|
1c7c3a776d | ||
|
|
40ff41f97d | ||
|
|
1b1287515b | ||
|
|
fb7c7b38a9 | ||
|
|
1e7948bcf7 | ||
|
|
9374c98669 | ||
|
|
c329d47c7f | ||
|
|
869b5c00c8 |
23 changed files with 1005 additions and 331 deletions
|
|
@ -8,6 +8,7 @@ import Integrations from './Integrations';
|
||||||
import UserView from './Users/UsersView';
|
import UserView from './Users/UsersView';
|
||||||
import AuditView from './Audit/AuditView';
|
import AuditView from './Audit/AuditView';
|
||||||
import Sites from './Sites';
|
import Sites from './Sites';
|
||||||
|
import Projects from './Projects';
|
||||||
import CustomFields from './CustomFields';
|
import CustomFields from './CustomFields';
|
||||||
import Webhooks from './Webhooks';
|
import Webhooks from './Webhooks';
|
||||||
import Notifications from './Notifications';
|
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.SESSIONS_LISTING)} component={SessionsListingSettings} />
|
||||||
<Route exact strict path={clientRoute(CLIENT_TABS.INTEGRATIONS)} component={Integrations} />
|
<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.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.CUSTOM_FIELDS)} component={CustomFields} />
|
||||||
<Route exact strict path={clientRoute(CLIENT_TABS.WEBHOOKS)} component={Webhooks} />
|
<Route exact strict path={clientRoute(CLIENT_TABS.WEBHOOKS)} component={Webhooks} />
|
||||||
<Route exact strict path={clientRoute(CLIENT_TABS.NOTIFICATIONS)} component={Notifications} />
|
<Route exact strict path={clientRoute(CLIENT_TABS.NOTIFICATIONS)} component={Notifications} />
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Form, Input, confirm } from 'UI';
|
import { Form, Input } from 'UI';
|
||||||
import styles from './customFieldForm.module.css';
|
import styles from './customFieldForm.module.css';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { useModal } from 'Components/Modal';
|
import { useModal } from 'Components/Modal';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { Button } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
import { Trash } from 'UI/Icons';
|
import { Trash } from 'UI/Icons';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
|
@ -23,16 +23,14 @@ const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
|
||||||
const exists = field?.exists();
|
const exists = field?.exists();
|
||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
if (
|
Modal.confirm({
|
||||||
await confirm({
|
title: 'Metadata',
|
||||||
header: 'Metadata',
|
content: `Are you sure you want to remove?`,
|
||||||
confirmation: `Are you sure you want to remove?`
|
onOk: async () => {
|
||||||
})
|
await store.remove(siteId, field?.index!);
|
||||||
) {
|
|
||||||
store.remove(siteId, field?.index!).then(() => {
|
|
||||||
hideModal();
|
hideModal();
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSave = (field: any) => {
|
const onSave = (field: any) => {
|
||||||
|
|
@ -48,7 +46,7 @@ const CustomFieldForm: React.FC<CustomFieldFormProps> = ({ siteId }) => {
|
||||||
toast.error('An error occurred while saving metadata.');
|
toast.error('An error occurred while saving metadata.');
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
|
|
@ -1,107 +1,86 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
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 CustomFieldForm from './CustomFieldForm';
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
import ListItem from './ListItem';
|
|
||||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { observer } from 'mobx-react-lite';
|
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';
|
||||||
|
import { Empty } from '.store/antd-virtual-7db13b4af6/package';
|
||||||
|
|
||||||
const CustomFields = () => {
|
const CustomFields = () => {
|
||||||
|
usePageTitle('Metadata - OpenReplay Preferences');
|
||||||
const { customFieldStore: store, projectsStore } = useStore();
|
const { customFieldStore: store, projectsStore } = useStore();
|
||||||
const sites = projectsStore.list;
|
const currentSite = projectsStore.config.project;
|
||||||
const [currentSite, setCurrentSite] = useState(sites[0]);
|
|
||||||
const [deletingItem, setDeletingItem] = useState<number | null>(null);
|
|
||||||
const { showModal, hideModal } = useModal();
|
const { showModal, hideModal } = useModal();
|
||||||
const fields = store.list;
|
const fields = store.list;
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeSite = sites[0];
|
|
||||||
if (!activeSite) return;
|
|
||||||
|
|
||||||
setCurrentSite(activeSite);
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
store.fetchList(activeSite.id).finally(() => {
|
store.fetchList(currentSite?.id).finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
}, [sites]);
|
}, [currentSite]);
|
||||||
|
|
||||||
const handleInit = (field?: any) => {
|
const handleInit = (field?: any) => {
|
||||||
store.init(field);
|
store.init(field);
|
||||||
showModal(<CustomFieldForm siteId={currentSite.id} />, {
|
showModal(<CustomFieldForm siteId={currentSite?.projectId + ''} />, {
|
||||||
title: field ? 'Edit Metadata' : 'Add Metadata', right: true
|
title: field ? 'Edit Metadata' : 'Add Metadata', right: true
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeSelect = ({ value }: { value: { value: number } }) => {
|
const remaining = 10 - fields.length;
|
||||||
const site = sites.find((s: any) => s.id === value.value);
|
|
||||||
setCurrentSite(site);
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
store.fetchList(site.id).finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm border p-5">
|
<div className="flex flex-col gap-6">
|
||||||
<div className={cn(styles.tabHeader)}>
|
<Typography.Text>
|
||||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
|
Attach key-value pairs to session replays for enhanced filtering, searching, and identifying relevant user
|
||||||
<div style={{ marginRight: '15px' }}>
|
sessions. Learn More.
|
||||||
<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.
|
|
||||||
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">
|
<a href="https://docs.openreplay.com/installation/metadata" className="link ml-1" target="_blank">
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</Typography.Text>
|
||||||
|
|
||||||
<Loader loading={loading}>
|
<Space>
|
||||||
<NoContent
|
<Tooltip
|
||||||
title={
|
title={remaining > 0 ? '' : 'You\'ve reached the limit of 10 metadata.'}
|
||||||
<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}
|
|
||||||
>
|
>
|
||||||
<div className={styles.list}>
|
<Button icon={<PlusIcon size={18} />} type="primary"
|
||||||
{fields
|
disabled={remaining === 0}
|
||||||
.filter((i: any) => i.index)
|
onClick={() => handleInit()}>
|
||||||
.map((field: any) => (
|
Add Metadata
|
||||||
<>
|
</Button>
|
||||||
<ListItem
|
</Tooltip>
|
||||||
disabled={deletingItem !== null && deletingItem === field.index}
|
{/*{remaining === 0 && <Icon name="info-circle" size={16} color="black" />}*/}
|
||||||
key={field._key}
|
<Typography.Text type="secondary">
|
||||||
field={field}
|
{remaining === 0 ? 'You have reached the limit of 10 metadata.' : `${remaining}/10 Remaining for this project`}
|
||||||
onEdit={handleInit}
|
</Typography.Text>
|
||||||
/>
|
</Space>
|
||||||
<Divider className="m-0" />
|
|
||||||
</>
|
<List
|
||||||
))}
|
locale={{
|
||||||
</div>
|
emptyText: <Empty description="None added yet" image={<AnimatedSVG name={ICONS.NO_METADATA} size={60} />} />
|
||||||
</NoContent>
|
}}
|
||||||
</Loader>
|
loading={loading}
|
||||||
|
dataSource={fields}
|
||||||
|
renderItem={(field: any) => (
|
||||||
|
<List.Item
|
||||||
|
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>
|
||||||
|
)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withPageTitle('Metadata - OpenReplay Preferences')(observer(CustomFields));
|
export default observer(CustomFields);
|
||||||
|
|
|
||||||
143
frontend/app/components/Client/Projects/ProjectCaptureRate.tsx
Normal file
143
frontend/app/components/Client/Projects/ProjectCaptureRate.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Button, Space, Switch, Tooltip, Input, Typography } from 'antd';
|
||||||
|
import { Icon, Loader } from 'UI';
|
||||||
|
import cn from 'classnames';
|
||||||
|
import ConditionalRecordingSettings from 'Shared/SessionSettings/components/ConditionalRecordingSettings';
|
||||||
|
import { Conditions } from '@/mstore/types/FeatureFlag';
|
||||||
|
import { useStore } from '@/mstore';
|
||||||
|
import Project from '@/mstore/types/project';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project: Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectCaptureRate(props: Props) {
|
||||||
|
const [conditions, setConditions] = React.useState<Conditions[]>([]);
|
||||||
|
const { projectId, platform } = props.project;
|
||||||
|
const isMobile = platform !== 'web';
|
||||||
|
const { settingsStore, userStore } = useStore();
|
||||||
|
const isAdmin = userStore.account.admin || userStore.account.superAdmin;
|
||||||
|
const isEnterprise = userStore.isEnterprise;
|
||||||
|
const [changed, setChanged] = useState(false);
|
||||||
|
const {
|
||||||
|
sessionSettings: {
|
||||||
|
captureRate,
|
||||||
|
changeCaptureRate,
|
||||||
|
conditionalCapture,
|
||||||
|
changeConditionalCapture,
|
||||||
|
captureConditions
|
||||||
|
},
|
||||||
|
loadingCaptureRate,
|
||||||
|
updateCaptureConditions,
|
||||||
|
fetchCaptureConditions
|
||||||
|
} = settingsStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectId) {
|
||||||
|
setChanged(false);
|
||||||
|
void fetchCaptureConditions(projectId);
|
||||||
|
}
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setConditions(captureConditions.map((condition: any) => new Conditions(condition, true, isMobile)));
|
||||||
|
}, [captureConditions]);
|
||||||
|
|
||||||
|
const onCaptureRateChange = (input: string) => {
|
||||||
|
setChanged(true);
|
||||||
|
changeCaptureRate(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleRate = () => {
|
||||||
|
setChanged(true);
|
||||||
|
const newValue = !conditionalCapture;
|
||||||
|
changeConditionalCapture(newValue);
|
||||||
|
if (newValue) {
|
||||||
|
changeCaptureRate('100');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdate = () => {
|
||||||
|
updateCaptureConditions(projectId!, {
|
||||||
|
rate: parseInt(captureRate, 10),
|
||||||
|
conditionalCapture: conditionalCapture,
|
||||||
|
conditions: isEnterprise ? conditions.map((c) => c.toCaptureCondition()) : []
|
||||||
|
});
|
||||||
|
setChanged(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDisabled = !changed || !isAdmin || (isEnterprise && (conditionalCapture && conditions.length === 0));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Loader loading={loadingCaptureRate || !projectId}>
|
||||||
|
<Tooltip title={isAdmin ? '' : 'You don\'t have permission to change.'}>
|
||||||
|
<div className="flex flex-col gap-4 border-b pb-4">
|
||||||
|
<Space>
|
||||||
|
<Typography.Text>Define percentage of sessions you want to capture</Typography.Text>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
'Define the percentage of user sessions to be recorded for detailed replay and analysis.' +
|
||||||
|
'\nSessions exceeding this specified limit will not be captured or stored.'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon size={16} color={'black'} name={'info-circle'} />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Space className="flex items-center gap-6 h-6">
|
||||||
|
<Switch
|
||||||
|
checked={conditionalCapture}
|
||||||
|
onChange={toggleRate}
|
||||||
|
checkedChildren={!isEnterprise ? '100%' : 'Conditional'}
|
||||||
|
disabled={!isAdmin}
|
||||||
|
unCheckedChildren={!isEnterprise ? 'Custom' : 'Capture Rate'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!conditionalCapture ? (
|
||||||
|
<div className={cn('relative', { disabled: !isAdmin })}>
|
||||||
|
<Input
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (/^\d+$/.test(e.target.value) || e.target.value === '') {
|
||||||
|
onCaptureRateChange(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={captureRate.toString()}
|
||||||
|
style={{ height: '26px', width: '70px' }}
|
||||||
|
disabled={conditionalCapture}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
className="absolute right-0 mr-2 top-0 bottom-0 m-auto"
|
||||||
|
name="percent"
|
||||||
|
color="gray-medium"
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={onUpdate}
|
||||||
|
disabled={updateDisabled}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
{conditionalCapture && isEnterprise ? (
|
||||||
|
<ConditionalRecordingSettings
|
||||||
|
setChanged={setChanged}
|
||||||
|
conditions={conditions}
|
||||||
|
setConditions={setConditions}
|
||||||
|
isMobile={isMobile}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Tooltip>
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(ProjectCaptureRate);
|
||||||
153
frontend/app/components/Client/Projects/ProjectForm.tsx
Normal file
153
frontend/app/components/Client/Projects/ProjectForm.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
import React, { ChangeEvent, FormEvent, useEffect } from 'react';
|
||||||
|
import { Icon } from 'UI';
|
||||||
|
import Project from '@/mstore/types/project';
|
||||||
|
import { projectStore, useStore } from '@/mstore';
|
||||||
|
import { Modal, Segmented, Form, Input, Button } from 'antd';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project?: Project;
|
||||||
|
onClose?: (arg: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectForm(props: Props) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { onClose } = props;
|
||||||
|
const { projectsStore } = useStore();
|
||||||
|
const project = projectsStore.instance as Project;
|
||||||
|
const loading = projectsStore.loading;
|
||||||
|
const canDelete = projectsStore.list.length > 1;
|
||||||
|
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 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (e: FormEvent) => {
|
||||||
|
if (!projectsStore.instance) return;
|
||||||
|
if (projectsStore.instance.id && projectsStore.instance.exists()) {
|
||||||
|
projectsStore
|
||||||
|
.updateProject(projectsStore.instance.id, project)
|
||||||
|
.then((response: any) => {
|
||||||
|
if (!response || !response.errors || response.errors.size === 0) {
|
||||||
|
if (onClose) {
|
||||||
|
onClose(null);
|
||||||
|
}
|
||||||
|
if (!pathname.includes('onboarding')) {
|
||||||
|
void projectsStore.fetchList();
|
||||||
|
}
|
||||||
|
toast.success('Project updated successfully');
|
||||||
|
} else {
|
||||||
|
toast.error(response.errors[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
projectsStore
|
||||||
|
.save(projectsStore.instance!)
|
||||||
|
.then(() => {
|
||||||
|
toast.success('Project created successfully');
|
||||||
|
onClose?.(null);
|
||||||
|
|
||||||
|
mstore.searchStore.clearSearch();
|
||||||
|
mstore.searchStoreLive.clearSearch();
|
||||||
|
mstore.initClient();
|
||||||
|
})
|
||||||
|
.catch((error: string) => {
|
||||||
|
toast.error(error || 'An error occurred while creating the project');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'Project Deletion Alert',
|
||||||
|
content: 'Are you sure you want to delete this project? Deleting it will permanently remove the project, along with all associated sessions and data.',
|
||||||
|
onOk: () => {
|
||||||
|
projectsStore.removeProject(project.id!).then(() => {
|
||||||
|
if (onClose) {
|
||||||
|
onClose(null);
|
||||||
|
}
|
||||||
|
if (project.id === projectsStore.active?.id) {
|
||||||
|
projectsStore.setSiteId(projectStore.list[0].id!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
requiredMark={false}
|
||||||
|
onFinish={onSubmit}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: 'Please enter a name' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Ex. OpenReplay"
|
||||||
|
name="name"
|
||||||
|
maxLength={40}
|
||||||
|
value={project.name}
|
||||||
|
onChange={handleEdit}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Project Type">
|
||||||
|
<div>
|
||||||
|
<Segmented
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: 'web',
|
||||||
|
label: 'Web'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ios',
|
||||||
|
label: 'Mobile'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
value={project.platform}
|
||||||
|
onChange={(value) => {
|
||||||
|
projectsStore.editInstance({ platform: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
<div className="mt-6 flex justify-between">
|
||||||
|
<Button
|
||||||
|
htmlType="submit"
|
||||||
|
type="primary"
|
||||||
|
className="float-left mr-2"
|
||||||
|
loading={loading}
|
||||||
|
// disabled={!project.validate}
|
||||||
|
>
|
||||||
|
{project.exists() ? 'Update' : 'Add'}
|
||||||
|
</Button>
|
||||||
|
{project.exists() && (
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
onClick={handleRemove}
|
||||||
|
disabled={!canDelete}
|
||||||
|
>
|
||||||
|
<Icon name="trash" size="16" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(ProjectForm);
|
||||||
92
frontend/app/components/Client/Projects/ProjectList.tsx
Normal file
92
frontend/app/components/Client/Projects/ProjectList.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Avatar, Input, Menu, MenuProps, Progress } from 'antd';
|
||||||
|
import { useStore } from '@/mstore';
|
||||||
|
import Project from '@/mstore/types/project';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { AppWindowMac, Smartphone } from 'lucide-react';
|
||||||
|
|
||||||
|
type MenuItem = Required<MenuProps>['items'][number];
|
||||||
|
|
||||||
|
const ProjectList: React.FC = () => {
|
||||||
|
const { projectsStore } = useStore();
|
||||||
|
const [search, setSearch] = React.useState('');
|
||||||
|
|
||||||
|
const filteredProjects = projectsStore.list.filter((project: Project) =>
|
||||||
|
project.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => setSearch(value);
|
||||||
|
|
||||||
|
const onClick: MenuProps['onClick'] = (e) => {
|
||||||
|
const pid = parseInt(e.key as string);
|
||||||
|
projectsStore.setConfigProject(pid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuItems: MenuItem[] = filteredProjects.map((project) => ({
|
||||||
|
key: project.id + '',
|
||||||
|
label: project.name,
|
||||||
|
icon: (
|
||||||
|
<ProjectIconWithProgress
|
||||||
|
platform={project.platform}
|
||||||
|
progress={project.sampleRate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col gap-4">
|
||||||
|
<div className="px-4 mt-4">
|
||||||
|
<Input.Search
|
||||||
|
placeholder="Search projects"
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="overflow-y-auto"
|
||||||
|
style={{ height: 'calc(100vh - 250px)' }}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
onClick={onClick}
|
||||||
|
selectedKeys={[String(projectsStore.config.pid)]}
|
||||||
|
className="w-full !bg-white !border-0"
|
||||||
|
inlineIndent={11}
|
||||||
|
items={menuItems}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(ProjectList);
|
||||||
|
|
||||||
|
const ProjectIconWithProgress: React.FC<{
|
||||||
|
platform: string;
|
||||||
|
progress: number;
|
||||||
|
}> = ({ platform, progress }) => (
|
||||||
|
<div className="relative flex items-center justify-center mr-2 leading-none">
|
||||||
|
<Progress
|
||||||
|
type="circle"
|
||||||
|
percent={progress}
|
||||||
|
size={28}
|
||||||
|
format={() => ''}
|
||||||
|
strokeWidth={4}
|
||||||
|
strokeColor="#23959a"
|
||||||
|
/>
|
||||||
|
<div className="absolute">
|
||||||
|
<Avatar
|
||||||
|
className="bg-tealx-light"
|
||||||
|
size={26}
|
||||||
|
icon={
|
||||||
|
platform === 'web' ? (
|
||||||
|
<AppWindowMac size={16} color="teal" />
|
||||||
|
) : (
|
||||||
|
<Smartphone size={16} color="teal" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
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';
|
||||||
|
import ProjectCaptureRate from 'Components/Client/Projects/ProjectCaptureRate';
|
||||||
|
import { Empty } from 'antd';
|
||||||
|
|
||||||
|
const ProjectTabContent: React.FC = () => {
|
||||||
|
const { projectsStore } = useStore();
|
||||||
|
const { pid, tab } = projectsStore.config;
|
||||||
|
|
||||||
|
const project = React.useMemo(
|
||||||
|
() => projectsStore.list.find((p) => p.projectId === pid),
|
||||||
|
[pid, projectsStore.list]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return <Empty description="Project not found" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabContent: Record<string, React.ReactNode> = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
installation: <ProjectTabTracking project={project} />,
|
||||||
|
captureRate: <ProjectCaptureRate project={project} />,
|
||||||
|
metadata: <CustomFields />,
|
||||||
|
tags: <ProjectTags />
|
||||||
|
}),
|
||||||
|
[project]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{tabContent[tab] || <Empty description="Tab not found" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(ProjectTabContent);
|
||||||
|
|
@ -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;
|
||||||
42
frontend/app/components/Client/Projects/ProjectTabs.tsx
Normal file
42
frontend/app/components/Client/Projects/ProjectTabs.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Tabs, TabsProps } from 'antd';
|
||||||
|
import { useStore } from '@/mstore';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
const customTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => (
|
||||||
|
<DefaultTabBar {...props} className="!mb-0" />
|
||||||
|
);
|
||||||
|
|
||||||
|
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={tabItems[0].key}
|
||||||
|
activeKey={activeTab}
|
||||||
|
style={{ borderBottom: 'none' }}
|
||||||
|
onChange={onTabChange}
|
||||||
|
renderTabBar={customTabBar}
|
||||||
|
items={tabItems.map((tab) => ({
|
||||||
|
key: tab.key,
|
||||||
|
label: tab.label
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(ProjectTabs);
|
||||||
69
frontend/app/components/Client/Projects/ProjectTags.tsx
Normal file
69
frontend/app/components/Client/Projects/ProjectTags.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useStore } from '@/mstore';
|
||||||
|
import { List, Button, Typography, Space, Empty } 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';
|
||||||
|
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||||
|
|
||||||
|
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
|
||||||
|
locale={{
|
||||||
|
emptyText: <Empty description="No tags found" image={<AnimatedSVG name={ICONS.NO_METADATA} size={60} />} />
|
||||||
|
}}
|
||||||
|
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);
|
||||||
112
frontend/app/components/Client/Projects/Projects.tsx
Normal file
112
frontend/app/components/Client/Projects/Projects.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { App, Button, Card, Layout, Space, Tooltip, 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 { KeyIcon, PlusIcon } from 'lucide-react';
|
||||||
|
import ProjectTabContent from 'Components/Client/Projects/ProjectTabContent';
|
||||||
|
import { useModal } from 'Components/ModalContext';
|
||||||
|
import ProjectForm from 'Components/Client/Projects/ProjectForm';
|
||||||
|
import Project from '@/mstore/types/project';
|
||||||
|
|
||||||
|
function Projects() {
|
||||||
|
const { projectsStore } = useStore();
|
||||||
|
const history = useHistory();
|
||||||
|
const { project, pid, tab } = projectsStore.config;
|
||||||
|
const { openModal, closeModal } = useModal();
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const createProject = () => {
|
||||||
|
openModal(<ProjectForm onClose={closeModal} project={new Project()} />, {
|
||||||
|
title: 'New Project'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
style={{ height: 'calc(100vh - 130px)' }}
|
||||||
|
classNames={{
|
||||||
|
header: '!border-b !px-4',
|
||||||
|
body: '!p-0 !border-t'
|
||||||
|
}}
|
||||||
|
title={<Typography.Title level={4} className="!m-0">Projects</Typography.Title>}
|
||||||
|
extra={[
|
||||||
|
<Button onClick={createProject} icon={<PlusIcon size={18} />}>
|
||||||
|
Create Project
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Sider width={260} trigger={null}
|
||||||
|
className="!bg-white border-r">
|
||||||
|
<ProjectList />
|
||||||
|
</Layout.Sider>
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<Layout.Header className="flex justify-between items-center p-4 !bg-white border-b"
|
||||||
|
style={{ height: 46 }}>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Typography.Title level={5}
|
||||||
|
className="capitalize !m-0 whitespace-nowrap truncate">
|
||||||
|
{project?.name}
|
||||||
|
</Typography.Title>
|
||||||
|
<ProjectKeyButton project={project} />
|
||||||
|
</div>
|
||||||
|
<ProjectTabs />
|
||||||
|
</Layout.Header>
|
||||||
|
<Layout.Content
|
||||||
|
style={{
|
||||||
|
padding: 24,
|
||||||
|
height: 'calc(100vh - 260px)'
|
||||||
|
}}
|
||||||
|
className="bg-white overflow-y-auto"
|
||||||
|
>
|
||||||
|
{project && <ProjectTabContent />}
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(Projects);
|
||||||
|
|
||||||
|
function ProjectKeyButton({ project }: { project: Project | null }) {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
|
const copyKey = () => {
|
||||||
|
if (!project || !project.projectKey) {
|
||||||
|
void message.error('Project key not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void navigator.clipboard.writeText(project?.projectKey || '');
|
||||||
|
void message.success('Project key copied to clipboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title="Copy Project Key">
|
||||||
|
<Button onClick={copyKey} icon={<KeyIcon size={14} />} size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
frontend/app/components/Client/Projects/TagForm.tsx
Normal file
80
frontend/app/components/Client/Projects/TagForm.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
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 = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
tagWatchStore.updateTagName(tag.tagId, name, projectId)
|
||||||
|
.then(() => {
|
||||||
|
closeModal();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 || name === tag.name || loading}
|
||||||
|
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;
|
||||||
1
frontend/app/components/Client/Projects/index.ts
Normal file
1
frontend/app/components/Client/Projects/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './Projects';
|
||||||
|
|
@ -20,14 +20,14 @@ inputModeOptions.forEach((o, i) => inputModeOptionsMap[o.value] = i)
|
||||||
const ProjectCodeSnippet = props => {
|
const ProjectCodeSnippet = props => {
|
||||||
const { projectsStore } = useStore();
|
const { projectsStore } = useStore();
|
||||||
const site = props.site;
|
const site = props.site;
|
||||||
const gdpr = projectsStore.instance.gdpr;
|
const gdpr = site.gdpr;
|
||||||
const saveGdpr = projectsStore.saveGDPR;
|
const saveGdpr = projectsStore.saveGDPR;
|
||||||
const editGdpr = projectsStore.editGDPR;
|
const editGdpr = projectsStore.editGDPR;
|
||||||
const [changed, setChanged] = useState(false)
|
const [changed, setChanged] = useState(false)
|
||||||
|
|
||||||
const saveGDPR = () => {
|
const saveGDPR = () => {
|
||||||
setChanged(true)
|
setChanged(true)
|
||||||
saveGdpr(site.id);
|
void saveGdpr(site.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeSelect = ({ name, value }) => {
|
const onChangeSelect = ({ name, value }) => {
|
||||||
|
|
@ -39,7 +39,7 @@ const ProjectCodeSnippet = props => {
|
||||||
editGdpr({ [ name ]: checked });
|
editGdpr({ [ name ]: checked });
|
||||||
saveGDPR()
|
saveGDPR()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import './styles/index.css';
|
import './styles/index.css';
|
||||||
import './styles/global.css'
|
import './styles/global.css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import './init';
|
import './init';
|
||||||
|
|
@ -7,19 +7,19 @@ import Router from './Router';
|
||||||
import { StoreProvider, RootStore } from './mstore';
|
import { StoreProvider, RootStore } from './mstore';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { ConfigProvider, theme, ThemeConfig } from 'antd';
|
import { ConfigProvider, App, theme, ThemeConfig } from 'antd';
|
||||||
import colors from 'App/theme/colors';
|
import colors from 'App/theme/colors';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { Notification, MountPoint } from 'UI';
|
import { Notification, MountPoint } from 'UI';
|
||||||
import {
|
import {
|
||||||
QueryClient,
|
QueryClient,
|
||||||
QueryClientProvider,
|
QueryClientProvider
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.getCommitHash = () => console.log(window.env.COMMIT_HASH);
|
window.getCommitHash = () => console.log(window.env.COMMIT_HASH);
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient();
|
||||||
const customTheme: ThemeConfig = {
|
const customTheme: ThemeConfig = {
|
||||||
// algorithm: theme.compactAlgorithm,
|
// algorithm: theme.compactAlgorithm,
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -29,7 +29,7 @@ const customTheme: ThemeConfig = {
|
||||||
},
|
},
|
||||||
Segmented: {
|
Segmented: {
|
||||||
itemSelectedBg: '#FFFFFF',
|
itemSelectedBg: '#FFFFFF',
|
||||||
itemSelectedColor: colors['main'],
|
itemSelectedColor: colors['main']
|
||||||
},
|
},
|
||||||
Menu: {
|
Menu: {
|
||||||
colorPrimary: colors.teal,
|
colorPrimary: colors.teal,
|
||||||
|
|
@ -46,13 +46,13 @@ const customTheme: ThemeConfig = {
|
||||||
itemSelectedColor: colors['teal'],
|
itemSelectedColor: colors['teal'],
|
||||||
|
|
||||||
itemMarginBlock: 0,
|
itemMarginBlock: 0,
|
||||||
itemPaddingInline: 50,
|
// itemPaddingInline: 50,
|
||||||
iconMarginInlineEnd: 14,
|
// iconMarginInlineEnd: 14,
|
||||||
collapsedWidth: 180,
|
collapsedWidth: 180
|
||||||
},
|
},
|
||||||
Button: {
|
Button: {
|
||||||
colorPrimary: colors.teal
|
colorPrimary: colors.teal
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: colors.teal,
|
colorPrimary: colors.teal,
|
||||||
|
|
@ -64,7 +64,8 @@ const customTheme: ThemeConfig = {
|
||||||
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: '\'Roboto\', \'ArialMT\', \'Arial\''
|
fontFamily: '\'Roboto\', \'ArialMT\', \'Arial\'',
|
||||||
|
fontWeightStrong: 400,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -73,21 +74,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
|
||||||
// const theme = window.localStorage.getItem('theme');
|
// const theme = window.localStorage.getItem('theme');
|
||||||
root.render(
|
root.render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ConfigProvider theme={customTheme}>
|
<ConfigProvider theme={customTheme}>
|
||||||
<StoreProvider store={new RootStore()}>
|
<App>
|
||||||
<DndProvider backend={HTML5Backend}>
|
<StoreProvider store={new RootStore()}>
|
||||||
<BrowserRouter>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Notification />
|
<BrowserRouter>
|
||||||
<Router />
|
<Notification />
|
||||||
</BrowserRouter>
|
<Router />
|
||||||
</DndProvider>
|
</BrowserRouter>
|
||||||
<MountPoint />
|
</DndProvider>
|
||||||
</StoreProvider>
|
<MountPoint />
|
||||||
</ConfigProvider>
|
</StoreProvider>
|
||||||
|
</App>
|
||||||
|
</ConfigProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,7 @@ import * as routes from 'App/routes';
|
||||||
import {
|
import {
|
||||||
CLIENT_DEFAULT_TAB,
|
CLIENT_DEFAULT_TAB,
|
||||||
CLIENT_TABS,
|
CLIENT_TABS,
|
||||||
bookmarks,
|
|
||||||
client,
|
client,
|
||||||
fflags,
|
|
||||||
notes,
|
|
||||||
sessions,
|
|
||||||
withSiteId
|
withSiteId
|
||||||
} from 'App/routes';
|
} from 'App/routes';
|
||||||
import { MODULES } from 'Components/Client/Modules';
|
import { MODULES } from 'Components/Client/Modules';
|
||||||
|
|
@ -33,13 +29,6 @@ import { useStore } from 'App/mstore';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const TabToUrlMap = {
|
|
||||||
all: sessions() as '/sessions',
|
|
||||||
bookmark: bookmarks() as '/bookmarks',
|
|
||||||
notes: notes() as '/notes',
|
|
||||||
flags: fflags() as '/feature-flags'
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props extends RouteComponentProps {
|
interface Props extends RouteComponentProps {
|
||||||
siteId?: string;
|
siteId?: string;
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
|
|
@ -135,16 +124,6 @@ function SideMenu(props: Props) {
|
||||||
});
|
});
|
||||||
}, [isAdmin, isEnterprise, isPreferencesActive, modules, spotOnly, siteId]);
|
}, [isAdmin, isEnterprise, isPreferencesActive, modules, spotOnly, siteId]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const currentLocation = location.pathname;
|
|
||||||
// const tab = Object.keys(TabToUrlMap).find((tab: keyof typeof TabToUrlMap) =>
|
|
||||||
// currentLocation.includes(TabToUrlMap[tab])
|
|
||||||
// );
|
|
||||||
// if (tab && tab !== searchStore.activeTab && siteId) {
|
|
||||||
// searchStore.setActiveTab({ type: tab });
|
|
||||||
// }
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
const menuRoutes: any = {
|
const menuRoutes: any = {
|
||||||
[MENU.EXIT]: () =>
|
[MENU.EXIT]: () =>
|
||||||
props.history.push(withSiteId(routes.sessions(), siteId)),
|
props.history.push(withSiteId(routes.sessions(), siteId)),
|
||||||
|
|
@ -164,7 +143,6 @@ function SideMenu(props: Props) {
|
||||||
[PREFERENCES_MENU.SESSION_LISTING]: () =>
|
[PREFERENCES_MENU.SESSION_LISTING]: () =>
|
||||||
client(CLIENT_TABS.SESSIONS_LISTING),
|
client(CLIENT_TABS.SESSIONS_LISTING),
|
||||||
[PREFERENCES_MENU.INTEGRATIONS]: () => client(CLIENT_TABS.INTEGRATIONS),
|
[PREFERENCES_MENU.INTEGRATIONS]: () => client(CLIENT_TABS.INTEGRATIONS),
|
||||||
[PREFERENCES_MENU.METADATA]: () => client(CLIENT_TABS.CUSTOM_FIELDS),
|
|
||||||
[PREFERENCES_MENU.WEBHOOKS]: () => client(CLIENT_TABS.WEBHOOKS),
|
[PREFERENCES_MENU.WEBHOOKS]: () => client(CLIENT_TABS.WEBHOOKS),
|
||||||
[PREFERENCES_MENU.PROJECTS]: () => client(CLIENT_TABS.SITES),
|
[PREFERENCES_MENU.PROJECTS]: () => client(CLIENT_TABS.SITES),
|
||||||
[PREFERENCES_MENU.ROLES_ACCESS]: () => client(CLIENT_TABS.MANAGE_ROLES),
|
[PREFERENCES_MENU.ROLES_ACCESS]: () => client(CLIENT_TABS.MANAGE_ROLES),
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ export const enum PREFERENCES_MENU {
|
||||||
ACCOUNT = 'account',
|
ACCOUNT = 'account',
|
||||||
SESSION_LISTING = 'session-listing',
|
SESSION_LISTING = 'session-listing',
|
||||||
INTEGRATIONS = 'integrations',
|
INTEGRATIONS = 'integrations',
|
||||||
METADATA = 'metadata',
|
|
||||||
WEBHOOKS = 'webhooks',
|
WEBHOOKS = 'webhooks',
|
||||||
MODULES = 'modules',
|
MODULES = 'modules',
|
||||||
PROJECTS = 'projects',
|
PROJECTS = 'projects',
|
||||||
|
|
@ -131,7 +130,6 @@ export const preferences: Category[] = [
|
||||||
{ label: 'Account', key: PREFERENCES_MENU.ACCOUNT, icon: 'person' },
|
{ label: 'Account', key: PREFERENCES_MENU.ACCOUNT, icon: 'person' },
|
||||||
{ label: 'Sessions Listing', key: PREFERENCES_MENU.SESSION_LISTING, icon: 'card-list' },
|
{ label: 'Sessions Listing', key: PREFERENCES_MENU.SESSION_LISTING, icon: 'card-list' },
|
||||||
{ label: 'Integrations', key: PREFERENCES_MENU.INTEGRATIONS, icon: 'plug' },
|
{ label: 'Integrations', key: PREFERENCES_MENU.INTEGRATIONS, icon: 'plug' },
|
||||||
{ label: 'Metadata', key: PREFERENCES_MENU.METADATA, icon: 'tags' },
|
|
||||||
{ label: 'Webhooks', key: PREFERENCES_MENU.WEBHOOKS, icon: 'link-45deg' },
|
{ label: 'Webhooks', key: PREFERENCES_MENU.WEBHOOKS, icon: 'link-45deg' },
|
||||||
{ label: 'Modules', key: PREFERENCES_MENU.MODULES, icon: 'puzzle' },
|
{ label: 'Modules', key: PREFERENCES_MENU.MODULES, icon: 'puzzle' },
|
||||||
{ label: 'Projects', key: PREFERENCES_MENU.PROJECTS, icon: 'folder2' },
|
{ label: 'Projects', key: PREFERENCES_MENU.PROJECTS, icon: 'folder2' },
|
||||||
|
|
@ -159,4 +157,4 @@ export const spotOnlyCats = [
|
||||||
MENU.PREFERENCES,
|
MENU.PREFERENCES,
|
||||||
MENU.SUPPORT,
|
MENU.SUPPORT,
|
||||||
MENU.SPOTS,
|
MENU.SPOTS,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,14 @@ import { makeAutoObservable, runInAction } from 'mobx';
|
||||||
import Project from './types/project';
|
import Project from './types/project';
|
||||||
import GDPR from './types/gdpr';
|
import GDPR from './types/gdpr';
|
||||||
import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
|
import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from 'App/constants/storageKeys';
|
||||||
import { projectsService } from "App/services";
|
import { projectsService } from 'App/services';
|
||||||
|
import { toast } from '.store/react-toastify-virtual-9dd0f3eae1/package';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
project: Project | null;
|
||||||
|
pid: number | undefined;
|
||||||
|
tab: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class ProjectsStore {
|
export default class ProjectsStore {
|
||||||
list: Project[] = [];
|
list: Project[] = [];
|
||||||
|
|
@ -11,6 +18,11 @@ export default class ProjectsStore {
|
||||||
active: Project | null = null;
|
active: Project | null = null;
|
||||||
sitesLoading = true;
|
sitesLoading = true;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
config: Config = {
|
||||||
|
project: null,
|
||||||
|
pid: undefined,
|
||||||
|
tab: 'installation'
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY);
|
const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY);
|
||||||
|
|
@ -22,45 +34,52 @@ export default class ProjectsStore {
|
||||||
return this.active ? ['ios', 'android'].includes(this.active.platform) : false;
|
return this.active ? ['ios', 'android'].includes(this.active.platform) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncProjectInList = (project: Partial<Project>) => {
|
||||||
|
const index = this.list.findIndex(site => site.id === project.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.list[index] = this.list[index].edit(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getSiteId = () => {
|
getSiteId = () => {
|
||||||
return {
|
return {
|
||||||
siteId: this.siteId,
|
siteId: this.siteId,
|
||||||
active: this.active,
|
active: this.active
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
initProject = (project: Partial<Project>) => {
|
initProject = (project: Partial<Project>) => {
|
||||||
this.instance = new Project(project);
|
this.instance = new Project(project);
|
||||||
}
|
};
|
||||||
|
|
||||||
setSitesLoading = (loading: boolean) => {
|
setSitesLoading = (loading: boolean) => {
|
||||||
this.sitesLoading = loading;
|
this.sitesLoading = loading;
|
||||||
}
|
};
|
||||||
|
|
||||||
setLoading = (loading: boolean) => {
|
setLoading = (loading: boolean) => {
|
||||||
this.loading = loading;
|
this.loading = loading;
|
||||||
}
|
};
|
||||||
|
|
||||||
setSiteId = (siteId: string) => {
|
setSiteId = (siteId: string) => {
|
||||||
localStorage.setItem(SITE_ID_STORAGE_KEY, siteId.toString());
|
localStorage.setItem(SITE_ID_STORAGE_KEY, siteId.toString());
|
||||||
this.siteId = siteId;
|
this.siteId = siteId;
|
||||||
this.active = this.list.find((site) => site.id! === siteId) ?? null;
|
this.active = this.list.find((site) => site.id! === siteId) ?? null;
|
||||||
}
|
};
|
||||||
|
|
||||||
editGDPR = (gdprData: Partial<GDPR>) => {
|
editGDPR = (gdprData: Partial<GDPR>) => {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
this.instance.gdpr.edit(gdprData);
|
this.instance.gdpr.edit(gdprData);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
editInstance = (instance: Partial<Project>) => {
|
editInstance = (instance: Partial<Project>) => {
|
||||||
if (!this.instance) return;
|
if (!this.instance) return;
|
||||||
this.instance = this.instance.edit(instance);
|
this.instance = this.instance.edit(instance);
|
||||||
}
|
};
|
||||||
|
|
||||||
fetchGDPR = async (siteId: string) => {
|
fetchGDPR = async (siteId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await projectsService.fetchGDPR(siteId)
|
const response = await projectsService.fetchGDPR(siteId);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
Object.assign(this.instance.gdpr, response.data);
|
Object.assign(this.instance.gdpr, response.data);
|
||||||
|
|
@ -69,7 +88,7 @@ export default class ProjectsStore {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch GDPR:', error);
|
console.error('Failed to fetch GDPR:', error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
saveGDPR = async (siteId: string) => {
|
saveGDPR = async (siteId: string) => {
|
||||||
if (!this.instance) return;
|
if (!this.instance) return;
|
||||||
|
|
@ -77,10 +96,19 @@ export default class ProjectsStore {
|
||||||
const gdprData = this.instance.gdpr.toData();
|
const gdprData = this.instance.gdpr.toData();
|
||||||
const response = await projectsService.saveGDPR(siteId, gdprData);
|
const response = await projectsService.saveGDPR(siteId, gdprData);
|
||||||
this.editGDPR(response.data);
|
this.editGDPR(response.data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.syncProjectInList({
|
||||||
|
id: siteId,
|
||||||
|
gdpr: response.data
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to sync project in list:', error);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save GDPR:', error);
|
console.error('Failed to save GDPR:', error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
fetchList = async (siteIdFromPath?: string) => {
|
fetchList = async (siteIdFromPath?: string) => {
|
||||||
this.setSitesLoading(true);
|
this.setSitesLoading(true);
|
||||||
|
|
@ -115,7 +143,7 @@ export default class ProjectsStore {
|
||||||
} finally {
|
} finally {
|
||||||
this.setSitesLoading(false);
|
this.setSitesLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
save = async (projectData: Partial<Project>) => {
|
save = async (projectData: Partial<Project>) => {
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
|
|
@ -132,12 +160,13 @@ export default class ProjectsStore {
|
||||||
this.setSiteId(newSite.id);
|
this.setSiteId(newSite.id);
|
||||||
this.active = newSite;
|
this.active = newSite;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
return response;
|
||||||
console.error('Failed to save site:', error);
|
} catch (error: any) {
|
||||||
|
throw error || 'An error occurred while saving the project.';
|
||||||
} finally {
|
} finally {
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
updateProjectRecordingStatus = (siteId: string, status: boolean) => {
|
updateProjectRecordingStatus = (siteId: string, status: boolean) => {
|
||||||
const site = this.list.find(site => site.id === siteId);
|
const site = this.list.find(site => site.id === siteId);
|
||||||
|
|
@ -150,7 +179,7 @@ export default class ProjectsStore {
|
||||||
localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS);
|
localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
removeProject = async (projectId: string) => {
|
removeProject = async (projectId: string) => {
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
|
|
@ -161,13 +190,13 @@ export default class ProjectsStore {
|
||||||
if (this.siteId === projectId) {
|
if (this.siteId === projectId) {
|
||||||
this.setSiteId(this.list[0].id!);
|
this.setSiteId(this.list[0].id!);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to remove project:', e);
|
console.error('Failed to remove project:', e);
|
||||||
} finally {
|
} finally {
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
updateProject = async (projectId: string, projectData: Partial<Project>) => {
|
updateProject = async (projectId: string, projectData: Partial<Project>) => {
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
|
|
@ -183,7 +212,24 @@ export default class ProjectsStore {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update site:', error);
|
console.error('Failed to update site:', error);
|
||||||
} finally {
|
} 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';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import Webhook, { IWebhook } from 'Types/webhook';
|
||||||
import { webhookService } from 'App/services';
|
import { webhookService } from 'App/services';
|
||||||
import { GettingStarted } from './types/gettingStarted';
|
import { GettingStarted } from './types/gettingStarted';
|
||||||
import { MENU_COLLAPSED } from 'App/constants/storageKeys';
|
import { MENU_COLLAPSED } from 'App/constants/storageKeys';
|
||||||
|
import { projectStore } from '@/mstore/index';
|
||||||
|
|
||||||
interface CaptureConditions {
|
interface CaptureConditions {
|
||||||
rate: number;
|
rate: number;
|
||||||
|
|
@ -102,6 +103,15 @@ export default class SettingsStore {
|
||||||
conditionalCapture: data.conditionalCapture,
|
conditionalCapture: data.conditionalCapture,
|
||||||
captureConditions: data.conditions,
|
captureConditions: data.conditions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
projectStore.syncProjectInList({
|
||||||
|
id: projectId + '',
|
||||||
|
sampleRate: data.rate,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to update project in list:', e);
|
||||||
|
}
|
||||||
toast.success('Settings updated successfully');
|
toast.success('Settings updated successfully');
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { makeAutoObservable } from 'mobx';
|
import { makeAutoObservable } from 'mobx';
|
||||||
import { tagWatchService } from 'App/services';
|
import { tagWatchService } from 'App/services';
|
||||||
import { CreateTag, Tag } from 'App/services/TagWatchService';
|
import { CreateTag, Tag } from 'App/services/TagWatchService';
|
||||||
|
import { projectStore } from '@/mstore';
|
||||||
|
|
||||||
export default class TagWatchStore {
|
export default class TagWatchStore {
|
||||||
tags: Tag[] = [];
|
tags: Tag[] = [];
|
||||||
|
|
@ -18,13 +19,14 @@ export default class TagWatchStore {
|
||||||
this.isLoading = loading;
|
this.isLoading = loading;
|
||||||
};
|
};
|
||||||
|
|
||||||
getTags = async () => {
|
getTags = async (projectId?: number) => {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
try {
|
try {
|
||||||
const tags: Tag[] = await tagWatchService.getTags();
|
const pid = projectId || projectStore.active?.projectId;
|
||||||
|
const tags: Tag[] = await tagWatchService.getTags(pid!);
|
||||||
this.setTags(tags);
|
this.setTags(tags);
|
||||||
return tags;
|
return tags;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -34,28 +36,31 @@ export default class TagWatchStore {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createTag = async (data: CreateTag) => {
|
createTag = async (data: CreateTag, projectId?: number) => {
|
||||||
try {
|
try {
|
||||||
const tagId: number = await tagWatchService.createTag(data);
|
const pid = projectId || projectStore.active?.projectId;
|
||||||
|
const tagId: number = await tagWatchService.createTag(pid!, data);
|
||||||
return tagId;
|
return tagId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteTag = async (id: number) => {
|
deleteTag = async (id: number, projectId?: number) => {
|
||||||
try {
|
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));
|
this.setTags(this.tags.filter((t) => t.tagId !== id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateTagName = async (id: number, name: string) => {
|
updateTagName = async (id: number, name: string, projectId?: number) => {
|
||||||
try {
|
try {
|
||||||
await tagWatchService.updateTagName(id, name);
|
const pid = projectId || projectStore.active?.projectId;
|
||||||
const updatedTag = this.tags.find((t) => t.tagId === id)
|
await tagWatchService.updateTagName(pid!, id, name);
|
||||||
|
const updatedTag = this.tags.find((t) => t.tagId === id);
|
||||||
if (updatedTag) {
|
if (updatedTag) {
|
||||||
this.setTags(this.tags.map((t) => t.tagId === id ? { ...updatedTag, name } : t));
|
this.setTags(this.tags.map((t) => t.tagId === id ? { ...updatedTag, name } : t));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,47 @@
|
||||||
import BaseService from "./BaseService";
|
import BaseService from './BaseService';
|
||||||
|
|
||||||
export default class ProjectsService extends BaseService {
|
export default class ProjectsService extends BaseService {
|
||||||
fetchGDPR = async (siteId: string) => {
|
fetchGDPR = async (siteId: string) => {
|
||||||
const r = await this.client.get(`/${siteId}/gdpr`);
|
const r = await this.client.get(`/${siteId}/gdpr`);
|
||||||
|
|
||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
saveGDPR = async (siteId: string, gdprData: any) => {
|
saveGDPR = async (siteId: string, gdprData: any) => {
|
||||||
const r = await this.client.post(`/${siteId}/gdpr`, gdprData);
|
const r = await this.client.post(`/${siteId}/gdpr`, gdprData);
|
||||||
|
|
||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
fetchList = async () => {
|
fetchList = async () => {
|
||||||
const r = await this.client.get('/projects');
|
const r = await this.client.get('/projects');
|
||||||
|
|
||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
saveProject = async (projectData: any) => {
|
saveProject = async (projectData: any): Promise<any> => {
|
||||||
const r = await this.client.post('/projects', projectData);
|
try {
|
||||||
|
const response = await this.client.post('/projects', projectData);
|
||||||
|
return response.json();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
const errorData = await error.response.json();
|
||||||
|
throw errorData.errors?.[0] || 'An error occurred while saving the project.';
|
||||||
|
}
|
||||||
|
|
||||||
return await r.json();
|
throw 'An unexpected error occurred.';
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
removeProject = async (projectId: string) => {
|
removeProject = async (projectId: string) => {
|
||||||
const r = await this.client.delete(`/projects/${projectId}`)
|
const r = await this.client.delete(`/projects/${projectId}`);
|
||||||
|
|
||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
};
|
||||||
|
|
||||||
updateProject = async (projectId: string, projectData: any) => {
|
updateProject = async (projectId: string, projectData: any) => {
|
||||||
const r = await this.client.put(`/projects/${projectId}`, projectData);
|
const r = await this.client.put(`/projects/${projectId}`, projectData);
|
||||||
|
|
||||||
return await r.json();
|
return await r.json();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,27 +12,27 @@ export interface Tag extends CreateTag {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TagWatchService extends BaseService {
|
export default class TagWatchService extends BaseService {
|
||||||
createTag(data: CreateTag) {
|
async createTag(projectId: number, data: CreateTag) {
|
||||||
return this.client.post('/tags', data)
|
const r = await this.client.post(`/${projectId}/tags`, data);
|
||||||
.then(r => r.json())
|
const response = await r.json();
|
||||||
.then((response: { data: any; }) => response.data || {})
|
return response.data || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
getTags() {
|
async getTags(projectId: number) {
|
||||||
return this.client.get('/tags')
|
const r = await this.client.get(`/${projectId}/tags`);
|
||||||
.then(r => r.json())
|
const response = await r.json();
|
||||||
.then((response: { data: any; }) => response.data || {})
|
return response.data || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTag(id: number) {
|
async deleteTag(projectId: number, id: number) {
|
||||||
return this.client.delete(`/tags/${id}`)
|
const r = await this.client.delete(`/${projectId}/tags/${id}`);
|
||||||
.then(r => r.json())
|
const response = await r.json();
|
||||||
.then((response: { data: any; }) => response.data || {})
|
return response.data || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTagName(id: number, name: string) {
|
async updateTagName(projectId: number, id: number, name: string) {
|
||||||
return this.client.put(`/tags/${id}`, { name })
|
const r = await this.client.put(`/${projectId}/tags/${id}`, { name });
|
||||||
.then(r => r.json())
|
const response = await r.json();
|
||||||
.then((response: { data: any; }) => response.data || {})
|
return response.data || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue