Compare commits
14 commits
main
...
event-anal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8481e38aa5 | ||
|
|
fce6a562fd | ||
|
|
3a07a20195 | ||
|
|
a9db2f224d | ||
|
|
b465da5a15 | ||
|
|
bb2f39517b | ||
|
|
73f06bf3eb | ||
|
|
de1e1ca44b | ||
|
|
60fa20d21d | ||
|
|
27b6128c9b | ||
|
|
95f8002eb4 | ||
|
|
dcebeb7b5b | ||
|
|
1851b046af | ||
|
|
6097af4839 |
31 changed files with 2231 additions and 40 deletions
|
|
@ -37,6 +37,11 @@ const components: any = {
|
|||
SpotPure: lazy(() => import('Components/Spots/SpotPlayer')),
|
||||
ScopeSetup: lazy(() => import('Components/ScopeForm')),
|
||||
HighlightsPure: lazy(() => import('Components/Highlights/HighlightsList')),
|
||||
ActivityPure: lazy(() => import('Components/DataManagement/Activity/Page')),
|
||||
UserPage: lazy(() => import('Components/DataManagement/UsersEvents/UserPage')),
|
||||
UsersEventsPage: lazy(() => import('Components/DataManagement/UsersEvents/ListPage')),
|
||||
EventPage: lazy(() => import('Components/DataManagement/UsersEvents/EventPage')),
|
||||
PropertiesList: lazy(() => import('Components/DataManagement/Properties/ListPage')),
|
||||
};
|
||||
|
||||
const enhancedComponents: any = {
|
||||
|
|
@ -60,6 +65,11 @@ const enhancedComponents: any = {
|
|||
Spot: components.SpotPure,
|
||||
ScopeSetup: components.ScopeSetup,
|
||||
Highlights: components.HighlightsPure,
|
||||
Activity: components.ActivityPure,
|
||||
UserPage: components.UserPage,
|
||||
UsersEventsPage: components.UsersEventsPage,
|
||||
EventPage: components.EventPage,
|
||||
PropertiesList: components.PropertiesList,
|
||||
};
|
||||
|
||||
const withSiteId = routes.withSiteId;
|
||||
|
|
@ -290,6 +300,44 @@ function PrivateRoutes() {
|
|||
path={withSiteId(LIVE_SESSION_PATH, siteIdList)}
|
||||
component={enhancedComponents.LiveSession}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(routes.dataManagement.activity(), siteIdList)}
|
||||
component={enhancedComponents.Activity}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(routes.dataManagement.userPage(), siteIdList)}
|
||||
component={enhancedComponents.UserPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(routes.dataManagement.users(), siteIdList)}
|
||||
>
|
||||
<enhancedComponents.UsersEventsPage view={'users'} />
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(routes.dataManagement.events(), siteIdList)}
|
||||
>
|
||||
<enhancedComponents.UsersEventsPage view={'events'} />
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(routes.dataManagement.eventPage(), siteIdList)}
|
||||
component={enhancedComponents.EventPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(routes.dataManagement.properties(), siteIdList)}
|
||||
component={enhancedComponents.PropertiesList}
|
||||
/>
|
||||
{Object.entries(routes.redirects).map(([fr, to]) => (
|
||||
<Redirect key={fr} exact strict from={fr} to={to} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { Input, Checkbox, Button } from 'antd';
|
||||
import cn from 'classnames'
|
||||
|
||||
function ColumnsModal({
|
||||
columns,
|
||||
onSelect,
|
||||
hiddenCols,
|
||||
topOffset = 'top-28'
|
||||
}: {
|
||||
columns: { title: string; key: string }[];
|
||||
onSelect: (col: string[]) => void;
|
||||
hiddenCols: string[];
|
||||
topOffset?: string;
|
||||
}) {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [selected, setSelected] = React.useState(
|
||||
columns.map((col) => col.key).filter((col) => !hiddenCols.includes(col))
|
||||
);
|
||||
|
||||
const onConfirm = () => {
|
||||
onSelect(selected);
|
||||
};
|
||||
const onToggle = (col: string, isSelected: boolean) => {
|
||||
const newList = isSelected
|
||||
? [...selected, col]
|
||||
: selected.filter((c) => c !== col);
|
||||
setSelected(newList);
|
||||
};
|
||||
|
||||
const searchRe = new RegExp(query, 'ig');
|
||||
const filteredList = columns.filter((col) => searchRe.test(col.title));
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
cn('flex flex-col gap-2 shadow border rounded-lg p-4 absolute right-0 z-50 bg-white', topOffset)
|
||||
}
|
||||
>
|
||||
<div className="font-semibold text-lg">Show/Hide Columns</div>
|
||||
<div className="text-sm">
|
||||
Select columns to display. Rearrange them in the table view.
|
||||
</div>
|
||||
<Input.Search
|
||||
placeholder={'Search columns'}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
{filteredList.map((col) => (
|
||||
<Checkbox
|
||||
onChange={(e) => onToggle(col.key, e.target.checked)}
|
||||
checked={selected.includes(col.key)}
|
||||
>
|
||||
{col.title}
|
||||
</Checkbox>
|
||||
))}
|
||||
<Button onClick={onConfirm} type={'primary'}>
|
||||
Show Selected
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColumnsModal;
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import React from 'react'
|
||||
import { EventData } from './data/Event'
|
||||
import { Segmented, Input } from 'antd';
|
||||
import { X, List, Braces, Files } from 'lucide-react';
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
function EventDetailsModal({ ev, onClose }: { ev: EventData, onClose: () => void }) {
|
||||
const tabs = [
|
||||
{
|
||||
label: 'All Properties',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: 'Openreplay Properties',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Custom Properties',
|
||||
value: 'custom',
|
||||
},
|
||||
]
|
||||
|
||||
const views = [
|
||||
{
|
||||
label: <List size={14} />,
|
||||
value: 'pretty',
|
||||
},
|
||||
{
|
||||
label: <Braces size={14} />,
|
||||
value: 'json',
|
||||
}
|
||||
]
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [tab, setTab] = React.useState(tabs[0].value)
|
||||
const [view, setView] = React.useState(views[0].value)
|
||||
const tabProps = {
|
||||
all: { ...ev.$_defaultFields, ...ev.$_customFields },
|
||||
custom: ev.$_customFields,
|
||||
default: ev.$_defaultFields,
|
||||
}
|
||||
const dataFields = tabProps[tab]
|
||||
const fieldArr = Object.entries(dataFields)
|
||||
const filteredArr = view === 'json' ? [] : fieldArr.filter(([key, value]) => {
|
||||
const qReg = new RegExp(query, 'ig')
|
||||
return qReg.test(key) || qReg.test(value)
|
||||
})
|
||||
const strProps = JSON.stringify({
|
||||
event: ev.name,
|
||||
properties: dataFields
|
||||
}, null, 4)
|
||||
const highlightedJson = view === 'pretty' ? '' : query ? strProps.replace(
|
||||
new RegExp(query, 'ig'),
|
||||
(match) => `<mark>${match}</mark>`
|
||||
) : strProps
|
||||
|
||||
const onCopy = () => {
|
||||
copy(strProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'h-screen w-full flex flex-col gap-4 p-4'}>
|
||||
<div className={'flex justify-between items-center'}>
|
||||
<div className={'font-semibold text-xl'}>Event</div>
|
||||
<div className={'p-2 cursor-pointer'} onClick={onClose}>
|
||||
<X size={16} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'p-2 rounded-lg bg-active-blue flex items-center gap-2'}>
|
||||
<div>icn</div>
|
||||
<div className={'font-semibold'}>{ev.name}</div>
|
||||
<div className={'link ml-auto flex gap-1 items-center'}>
|
||||
<span>Play Session</span>
|
||||
<Triangle size={10} color={'blue'} />
|
||||
</div>
|
||||
</div>
|
||||
<Segmented options={tabs} value={tab} onChange={(v) => setTab(v)} />
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Segmented
|
||||
value={view}
|
||||
options={views}
|
||||
size={'small'}
|
||||
onChange={(v) => setView(v)}
|
||||
/>
|
||||
<Input.Search
|
||||
size={'small'}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={'Find Property'}
|
||||
/>
|
||||
</div>
|
||||
{view === 'pretty' ?
|
||||
<div
|
||||
className={'overflow-y-auto flex flex-col gap-2'}
|
||||
style={{ height: 'calc(100% - 200px)' }}
|
||||
>
|
||||
{filteredArr.map(([key, value]) => (
|
||||
<div key={key} className={'flex items-center border-b'}>
|
||||
<div className={'flex-1'}>{key}</div>
|
||||
<div className={'flex-1 text-disabled-text'}>{value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
: (
|
||||
<div className={'relative'}>
|
||||
<div onClick={onCopy} className={'absolute right-0 top-0 cursor-pointer hover:text-blue'}>
|
||||
<Files size={16} />
|
||||
</div>
|
||||
<pre dangerouslySetInnerHTML={{ __html: highlightedJson }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Triangle({ size = 16, color = 'currentColor' }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color}
|
||||
className={'rotate-90'}
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 2L1 21h22L12 2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDetailsModal;
|
||||
350
frontend/app/components/DataManagement/Activity/Page.tsx
Normal file
350
frontend/app/components/DataManagement/Activity/Page.tsx
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
import React from 'react';
|
||||
import { EventsList, FilterList } from 'Shared/Filters/FilterList';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import ColumnsModal from 'Components/DataManagement/Activity/ColumnsModal';
|
||||
import Event from './data/Event';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import EventDetailsModal from './EventDetailsModal';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Select from 'Shared/Select';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { dataManagement, withSiteId } from 'App/routes';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import FullPagination from 'Shared/FullPagination';
|
||||
import AnimatedSVG from 'Shared/AnimatedSVG';
|
||||
import DndTable from 'Shared/DNDTable';
|
||||
import { Code } from 'lucide-react';
|
||||
|
||||
const limit = 100;
|
||||
|
||||
const testEv = new Event({
|
||||
name: 'test ev #',
|
||||
time: Date.now(),
|
||||
defaultFields: {
|
||||
userId: '123',
|
||||
userLocation: 'NY',
|
||||
userEnvironment: 'Mac OS',
|
||||
},
|
||||
customFields: {},
|
||||
isAutoCapture: false,
|
||||
sessionId: '123123',
|
||||
displayName: 'Test Event',
|
||||
description: 'This is A test Event',
|
||||
monthQuery: 100,
|
||||
monthVolume: 1000,
|
||||
});
|
||||
const testAutoEv = new Event({
|
||||
name: 'auto test ev',
|
||||
time: Date.now(),
|
||||
defaultFields: {
|
||||
userId: '123',
|
||||
userLocation: 'NY',
|
||||
userEnvironment: 'Mac OS',
|
||||
},
|
||||
customFields: {},
|
||||
isAutoCapture: true,
|
||||
sessionId: '123123',
|
||||
displayName: 'Test Auto Event',
|
||||
description: 'This is A test Auto Event',
|
||||
monthQuery: 100,
|
||||
monthVolume: 1000,
|
||||
});
|
||||
export const list = [testEv.toData(), testAutoEv.toData()];
|
||||
|
||||
const fetcher = async (
|
||||
page: number
|
||||
): Promise<{ list: any[]; total: number }> => {
|
||||
const total = 3000;
|
||||
return new Promise((resolve) => {
|
||||
resolve({ list, total });
|
||||
});
|
||||
};
|
||||
|
||||
const columnOrderKey = '$__activity_columns_order__$';
|
||||
|
||||
function ActivityPage() {
|
||||
const { projectsStore } = useStore();
|
||||
const siteId = projectsStore.activeSiteId;
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: 'Show/Hide Columns',
|
||||
key: 'edit-columns',
|
||||
onClick: () => setTimeout(() => setEditCols(true), 1),
|
||||
},
|
||||
];
|
||||
const columns = [
|
||||
{
|
||||
title: 'Event Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
render: (text, row) => (
|
||||
<div className={'flex items-center gap-2 code-font'}>
|
||||
<Code size={16} />
|
||||
{row.$_isAutoCapture && <span className={'text-gray-500'}>[a]</span>}
|
||||
<span>{row.name}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Time',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.time - b.time,
|
||||
},
|
||||
{
|
||||
title: 'Distinct ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.userId.localeCompare(b.userId),
|
||||
render: (text) => (
|
||||
<Link
|
||||
to={withSiteId(dataManagement.userPage(text), siteId)}
|
||||
className={'link'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'City',
|
||||
dataIndex: 'userLocation',
|
||||
key: 'userLocation',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.userLocation.localeCompare(b.userLocation),
|
||||
},
|
||||
{
|
||||
title: 'Environment',
|
||||
dataIndex: 'userEnvironment',
|
||||
key: 'userEnvironment',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.userEnvironment.localeCompare(b.userEnvironment),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={'click'}
|
||||
placement={'bottomRight'}
|
||||
>
|
||||
<div className={'cursor-pointer'}>
|
||||
<MoreOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
),
|
||||
dataIndex: '$__opts__$',
|
||||
key: '$__opts__$',
|
||||
width: 50,
|
||||
},
|
||||
];
|
||||
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [cols, setCols] = React.useState(columns);
|
||||
const [hiddenCols, setHiddenCols] = React.useState([]);
|
||||
const { data, isPending } = useQuery({
|
||||
queryKey: ['data', 'events', page],
|
||||
queryFn: () => fetcher(page),
|
||||
initialData: { list: [], total: 0 },
|
||||
});
|
||||
const { list, total } = data;
|
||||
const appliedFilter = { filters: [] };
|
||||
const onAddFilter = () => {};
|
||||
const onUpdateFilter = () => {};
|
||||
const onRemoveFilter = () => {};
|
||||
const onChangeEventsOrder = () => {};
|
||||
const saveRequestPayloads = () => {};
|
||||
const onFilterMove = () => {};
|
||||
const [editCols, setEditCols] = React.useState(false);
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hiddenCols.length) {
|
||||
setCols((cols) =>
|
||||
cols.map((col) => ({
|
||||
...col,
|
||||
hidden: hiddenCols.includes(col.key),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [hiddenCols]);
|
||||
React.useEffect(() => {
|
||||
const savedColumnOrder = localStorage.getItem(columnOrderKey);
|
||||
if (savedColumnOrder) {
|
||||
const keys = savedColumnOrder.split(',');
|
||||
setCols((cols) => {
|
||||
return cols.sort((a, b) => {
|
||||
return keys.indexOf(a.key) - keys.indexOf(b.key);
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onOrderChange = (newCols) => {
|
||||
const order = newCols.map((col) => col.key).join(',');
|
||||
localStorage.setItem(columnOrderKey, order);
|
||||
|
||||
setCols(newCols);
|
||||
};
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
const onItemClick = (ev: Event) => {
|
||||
showModal(<EventDetailsModal ev={ev} onClose={hideModal} />, {
|
||||
width: 420,
|
||||
right: true,
|
||||
});
|
||||
};
|
||||
|
||||
const onUpdateVisibleCols = (cols: string[]) => {
|
||||
setHiddenCols((_) => {
|
||||
return columns
|
||||
.map((col) =>
|
||||
cols.includes(col.key) || col.key === '$__opts__$' ? null : col.key
|
||||
)
|
||||
.filter(Boolean);
|
||||
});
|
||||
setEditCols(false);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={'flex flex-col gap-2'}
|
||||
style={{ maxWidth: '1360px', margin: 'auto' }}
|
||||
>
|
||||
<div className={'shadow rounded-xl'}>
|
||||
<EventsList
|
||||
filter={appliedFilter}
|
||||
onAddFilter={onAddFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
onFilterMove={onFilterMove}
|
||||
mergeDown
|
||||
heading={
|
||||
<div
|
||||
className={
|
||||
'-mx-4 px-4 border-b w-full py-2 font-semibold text-lg'
|
||||
}
|
||||
style={{ width: 'calc(100% + 2rem)' }}
|
||||
>
|
||||
Activity
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<FilterList
|
||||
mergeUp
|
||||
filter={appliedFilter}
|
||||
onAddFilter={onAddFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
saveRequestPayloads={saveRequestPayloads}
|
||||
onFilterMove={onFilterMove}
|
||||
/>
|
||||
</div>
|
||||
<div className={'relative'}>
|
||||
{editCols ? (
|
||||
<OutsideClickDetectingDiv onClickOutside={() => setEditCols(false)}>
|
||||
<ColumnsModal
|
||||
columns={shownCols.filter((col) => col.key !== '$__opts__$')}
|
||||
onSelect={onUpdateVisibleCols}
|
||||
hiddenCols={hiddenCols}
|
||||
/>
|
||||
</OutsideClickDetectingDiv>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={
|
||||
'bg-white rounded-xl shadow border flex flex-col overflow-hidden'
|
||||
}
|
||||
>
|
||||
<div className={'px-4 py-2 flex items-center gap-2'}>
|
||||
<div className={'font-semibold text-lg'}>All users activity</div>
|
||||
<div className={'ml-auto'} />
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Past 24 Hours', value: 'DESC' },
|
||||
{ label: 'Weekly', value: 'ASC' },
|
||||
{ label: 'Other', value: 'Stuff' },
|
||||
]}
|
||||
defaultValue={'DESC'}
|
||||
plain
|
||||
onChange={({ value }) => {
|
||||
console.log(value);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'DESC' },
|
||||
{ label: 'Oldest', value: 'ASC' },
|
||||
]}
|
||||
defaultValue={'DESC'}
|
||||
plain
|
||||
onChange={({ value }) => {
|
||||
console.log(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{total === 0 ? (
|
||||
<div className={'flex items-center justify-center flex-col gap-4'}>
|
||||
<AnimatedSVG name={'no-results'} size={56} />
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className={'text-lg font-semibold'}>
|
||||
No results in the{' '}
|
||||
</div>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Past 24 Hours', value: 'DESC' },
|
||||
{ label: 'Weekly', value: 'ASC' },
|
||||
{ label: 'Other', value: 'Stuff' },
|
||||
]}
|
||||
defaultValue={'DESC'}
|
||||
plain
|
||||
onChange={({ value }) => {
|
||||
console.log(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button type={'text'}>Refresh</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DndTable
|
||||
loading={isPending}
|
||||
onRow={(record) => ({
|
||||
onClick: () => onItemClick(record),
|
||||
})}
|
||||
dataSource={list}
|
||||
pagination={false}
|
||||
columns={cols}
|
||||
onOrderChange={onOrderChange}
|
||||
/>
|
||||
<FullPagination
|
||||
page={page}
|
||||
limit={limit}
|
||||
total={total}
|
||||
listLen={list.length}
|
||||
onPageChange={onPageChange}
|
||||
entity={'events'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ActivityPage);
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
interface DefaultFields {
|
||||
userId: string;
|
||||
userCity: string;
|
||||
userEnvironment: string;
|
||||
}
|
||||
|
||||
export interface EventData {
|
||||
name: string;
|
||||
time: number;
|
||||
$_isAutoCapture: boolean;
|
||||
$_defaultFields: DefaultFields;
|
||||
$_customFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
export default class Event {
|
||||
name: string;
|
||||
eventId: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
monthVolume: number;
|
||||
monthQuery: number;
|
||||
time: number;
|
||||
defaultFields: DefaultFields = {
|
||||
userId: '',
|
||||
userLocation: '',
|
||||
userEnvironment: '',
|
||||
}
|
||||
customFields?: Record<string,any> = undefined;
|
||||
|
||||
readonly $_isAutoCapture: boolean;
|
||||
readonly $_sessionId: string;
|
||||
|
||||
constructor(
|
||||
{
|
||||
name,
|
||||
time,
|
||||
defaultFields,
|
||||
customFields,
|
||||
sessionId,
|
||||
isAutoCapture,
|
||||
displayName,
|
||||
description,
|
||||
monthVolume,
|
||||
monthQuery,
|
||||
}: {
|
||||
name: string;
|
||||
time: number;
|
||||
defaultFields: DefaultFields;
|
||||
customFields?: Record<string, any>;
|
||||
sessionId: string;
|
||||
isAutoCapture: boolean;
|
||||
displayName: string;
|
||||
description: string;
|
||||
monthVolume: number;
|
||||
monthQuery: number;
|
||||
}) {
|
||||
this.name = name;
|
||||
this.eventId = 'asdasd';
|
||||
this.time = time;
|
||||
this.defaultFields = defaultFields;
|
||||
this.customFields = customFields;
|
||||
this.$_isAutoCapture = isAutoCapture;
|
||||
this.$_sessionId = sessionId;
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
this.monthVolume = monthVolume;
|
||||
this.monthQuery = monthQuery;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const obj = this.toData();
|
||||
return JSON.stringify(obj, 4);
|
||||
}
|
||||
|
||||
toData(): EventData {
|
||||
const obj: any = {
|
||||
name: this.name,
|
||||
time: this.time,
|
||||
$_isAutoCapture: this.$_isAutoCapture,
|
||||
$_defaultFields: this.defaultFields,
|
||||
$_customFields: this.customFields,
|
||||
displayName: this.displayName,
|
||||
description: this.description,
|
||||
monthVolume: this.monthVolume,
|
||||
monthQuery: this.monthQuery,
|
||||
}
|
||||
Object.entries(this.defaultFields).forEach(([key, value]) => {
|
||||
obj[key] = value;
|
||||
});
|
||||
if (this.customFields) {
|
||||
Object.entries(this.customFields).forEach(([key, value]) => {
|
||||
obj[key] = value;
|
||||
});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
112
frontend/app/components/DataManagement/DataItemPage.tsx
Normal file
112
frontend/app/components/DataManagement/DataItemPage.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import React from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
import { Triangle } from './Activity/EventDetailsModal';
|
||||
import cn from 'classnames';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
|
||||
function DataItemPage({
|
||||
sessionId,
|
||||
footer,
|
||||
item,
|
||||
backLink,
|
||||
}: {
|
||||
sessionId?: string;
|
||||
footer?: React.ReactNode;
|
||||
item: Record<string, any>;
|
||||
backLink: { name: string; to: string };
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={'flex flex-col gap-2 mx-auto w-full'}
|
||||
style={{ maxWidth: 1360 }}
|
||||
>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: backLink.name, to: backLink.to },
|
||||
{ label: item.name },
|
||||
]}
|
||||
/>
|
||||
<div className={'rounded-lg border bg-white flex flex-col'}>
|
||||
<div
|
||||
className={'p-4 border-b w-full flex items-center justify-between'}
|
||||
>
|
||||
<div
|
||||
className={'bg-gray-lighter rounded-xl px-2 font-semibold text-lg'}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
{sessionId ? (
|
||||
<div className={'link flex gap-1 items-center'}>
|
||||
<span>Play Sessions</span>
|
||||
<Triangle size={10} color={'blue'} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{item.fields.map((field) => (
|
||||
<EditableField
|
||||
onSave={() => null}
|
||||
fieldName={field.name}
|
||||
value={field.value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableField({
|
||||
onSave,
|
||||
fieldName,
|
||||
value,
|
||||
}: {
|
||||
onSave: (value: string) => void;
|
||||
fieldName: string;
|
||||
value: string;
|
||||
}) {
|
||||
const [isEdit, setIsEdit] = React.useState(false);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex border-b last:border-b-0 items-center px-4 py-2 gap-2',
|
||||
isEdit ? 'bg-active-blue' : 'hover:bg-active-blue'
|
||||
)}
|
||||
>
|
||||
<div className={'font-semibold'} style={{ flex: 1 }}>
|
||||
{fieldName}
|
||||
</div>
|
||||
<div style={{ flex: 6 }}>
|
||||
{isEdit ? (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Input size={'small'} defaultValue={value} />
|
||||
<div className={'ml-auto'} />
|
||||
<Button
|
||||
size={'small'}
|
||||
type={'text'}
|
||||
onClick={() => setIsEdit(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size={'small'} type={'primary'}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<span>{value}</span>
|
||||
<div
|
||||
className={'cursor-pointer text-main'}
|
||||
onClick={() => setIsEdit(true)}
|
||||
>
|
||||
<EditOutlined size={16} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataItemPage;
|
||||
232
frontend/app/components/DataManagement/Properties/ListPage.tsx
Normal file
232
frontend/app/components/DataManagement/Properties/ListPage.tsx
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
import React from 'react';
|
||||
import { Input, Table, Button, Dropdown } from 'antd';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { withSiteId, dataManagement } from 'App/routes';
|
||||
import { Filter, Album } from 'lucide-react';
|
||||
import { list } from '../Activity/Page';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import ColumnsModal from 'Components/DataManagement/Activity/ColumnsModal';
|
||||
import FullPagination from 'Shared/FullPagination';
|
||||
import Tabs from 'Shared/Tabs'
|
||||
|
||||
function ListPage() {
|
||||
const [view, setView] = React.useState('users');
|
||||
const views = [
|
||||
{
|
||||
key: 'users',
|
||||
label: <div className={'text-lg font-medium'}>Users</div>,
|
||||
},
|
||||
{
|
||||
key: 'events',
|
||||
label: <div className={'text-lg font-medium'}>Events</div>,
|
||||
},
|
||||
];
|
||||
const { projectsStore } = useStore();
|
||||
const siteId = projectsStore.activeSiteId;
|
||||
const history = useHistory();
|
||||
const toUser = (id: string) =>
|
||||
history.push(withSiteId(dataManagement.userPage(id), siteId));
|
||||
const toEvent = (id: string) =>
|
||||
history.push(withSiteId(dataManagement.eventPage(id), siteId));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-4 rounded-lg border bg-white mx-auto"
|
||||
style={{ maxWidth: 1360 }}
|
||||
>
|
||||
<div className={'flex items-center justify-between border-b px-4 pt-2 '}>
|
||||
<Tabs
|
||||
activeKey={view}
|
||||
onChange={(key) => setView(key)}
|
||||
items={views}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type={'text'} icon={<Album size={14} />}>
|
||||
Docs
|
||||
</Button>
|
||||
<Input.Search size={'small'} placeholder={'Name, email, ID'} />
|
||||
</div>
|
||||
</div>
|
||||
{view === 'users' ? <UserPropsList toUser={toUser} /> : <EventPropsList toEvent={toEvent} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventPropsList({ toEvent }: { toEvent: (id: string) => void }) {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Property',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Display Name',
|
||||
dataIndex: 'displayName',
|
||||
key: 'displayName',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.description.localeCompare(b.description),
|
||||
},
|
||||
{
|
||||
title: '30 Day Volume',
|
||||
dataIndex: 'monthVolume',
|
||||
key: 'monthVolume',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.monthVolume.localeCompare(b.monthVolume),
|
||||
},
|
||||
];
|
||||
const page = 1;
|
||||
const total = 100;
|
||||
const onPageChange = (page: number) => {};
|
||||
const limit = 10;
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
pagination={false}
|
||||
onRow={(record) => ({
|
||||
onClick: () => toEvent(record.eventId),
|
||||
})}
|
||||
/>
|
||||
<FullPagination
|
||||
page={page}
|
||||
limit={limit}
|
||||
total={total}
|
||||
listLen={list.length}
|
||||
onPageChange={onPageChange}
|
||||
entity={'events'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserPropsList({ toUser }: { toUser: (id: string) => void }) {
|
||||
const [editCols, setEditCols] = React.useState(false);
|
||||
const [hiddenCols, setHiddenCols] = React.useState([]);
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: 'Show/Hide Columns',
|
||||
key: 'edit-columns',
|
||||
onClick: () => setTimeout(() => setEditCols(true), 1),
|
||||
},
|
||||
];
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Display Name',
|
||||
dataIndex: 'displayName',
|
||||
key: 'displayName',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.description.localeCompare(b.description),
|
||||
},
|
||||
{
|
||||
title: '# Users',
|
||||
dataIndex: 'users',
|
||||
key: 'users',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.users.localeCompare(b.users),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={'click'}
|
||||
placement={'bottomRight'}
|
||||
>
|
||||
<div className={'cursor-pointer'}>
|
||||
<MoreOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
),
|
||||
dataIndex: '$__opts__$',
|
||||
key: '$__opts__$',
|
||||
width: 50,
|
||||
},
|
||||
];
|
||||
|
||||
const page = 1;
|
||||
const total = 10;
|
||||
const onPageChange = (page: number) => {};
|
||||
const limit = 10;
|
||||
const list = [];
|
||||
|
||||
const onAddFilter = () => console.log('add filter');
|
||||
const excludeFilterKeys = [];
|
||||
const excludeCategory = [];
|
||||
|
||||
const shownCols = columns.map((col) => ({
|
||||
...col,
|
||||
hidden: hiddenCols.includes(col.key),
|
||||
}));
|
||||
const onUpdateVisibleCols = (cols: string[]) => {
|
||||
setHiddenCols((_) => {
|
||||
return columns
|
||||
.map((col) =>
|
||||
cols.includes(col.key) || col.key === '$__opts__$' ? null : col.key
|
||||
)
|
||||
.filter(Boolean);
|
||||
});
|
||||
setEditCols(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className={'relative'}>
|
||||
{editCols ? (
|
||||
<OutsideClickDetectingDiv onClickOutside={() => setEditCols(false)}>
|
||||
<ColumnsModal
|
||||
columns={shownCols.filter((col) => col.key !== '$__opts__$')}
|
||||
onSelect={onUpdateVisibleCols}
|
||||
hiddenCols={hiddenCols}
|
||||
topOffset={'top-24 -mt-4'}
|
||||
/>
|
||||
</OutsideClickDetectingDiv>
|
||||
) : null}
|
||||
<Table
|
||||
onRow={(record) => ({
|
||||
onClick: () => toUser(record.userId),
|
||||
})}
|
||||
pagination={false}
|
||||
rowClassName={'cursor-pointer'}
|
||||
dataSource={[]}
|
||||
columns={shownCols}
|
||||
/>
|
||||
</div>
|
||||
<FullPagination
|
||||
page={page}
|
||||
limit={limit}
|
||||
total={total}
|
||||
listLen={list.length}
|
||||
onPageChange={onPageChange}
|
||||
entity={'users'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ListPage);
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import React from 'react';
|
||||
import FullPagination from 'Shared/FullPagination';
|
||||
import { Input, Table } from 'antd';
|
||||
import Tabs from 'Shared/Tabs';
|
||||
import { list } from "../Activity/Page";
|
||||
|
||||
function PropertiesPage() {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [activeView, setActiveView] = React.useState('properties');
|
||||
const views = [
|
||||
{
|
||||
key: 'user-props',
|
||||
label: <div className={'text-lg font-medium'}>User Properties</div>,
|
||||
},
|
||||
{
|
||||
key: 'events-props',
|
||||
label: <div className={'text-lg font-medium'}>Event Properties</div>,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-4 rounded-lg border bg-white mx-auto"
|
||||
style={{ maxWidth: 1360 }}
|
||||
>
|
||||
<div className={'flex items-center justify-between border-b px-4 pt-2 '}>
|
||||
<Tabs items={views} onChange={setActiveView} activeKey={activeView} />
|
||||
<Input.Search
|
||||
size={'small'}
|
||||
placeholder={'Name, email, ID'}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{activeView === 'user-props' ? <UserPropsList /> : <EventPropsList />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserPropsList() {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Display Name',
|
||||
dataIndex: 'displayName',
|
||||
key: 'displayName',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.description.localeCompare(b.description),
|
||||
},
|
||||
{
|
||||
title: 'Example Value',
|
||||
dataIndex: 'exampleValue',
|
||||
key: 'exampleValue',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.exampleValue.localeCompare(b.exampleValue),
|
||||
},
|
||||
{
|
||||
title: '# of Queries',
|
||||
dataIndex: 'monthQuery',
|
||||
key: 'monthQuery',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.monthQuery.localeCompare(b.monthQuery),
|
||||
},
|
||||
];
|
||||
const page = 1;
|
||||
const total = 100;
|
||||
const onPageChange = (page: number) => {};
|
||||
const limit = 10;
|
||||
return (
|
||||
<div>
|
||||
<Table columns={columns} dataSource={list} pagination={false} />
|
||||
<FullPagination
|
||||
page={page}
|
||||
limit={limit}
|
||||
total={total}
|
||||
listLen={list.length}
|
||||
onPageChange={onPageChange}
|
||||
entity={'events'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventPropsList() {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Display Name',
|
||||
dataIndex: 'displayName',
|
||||
key: 'displayName',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.description.localeCompare(b.description),
|
||||
},
|
||||
{
|
||||
title: 'Example Value',
|
||||
dataIndex: 'exampleValue',
|
||||
key: 'exampleValue',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.exampleValue.localeCompare(b.exampleValue),
|
||||
},
|
||||
{
|
||||
title: '# of Events',
|
||||
dataIndex: 'totalVolume',
|
||||
key: 'totalVolume',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.totalVolume.localeCompare(b.totalVolume),
|
||||
},
|
||||
];
|
||||
const page = 1;
|
||||
const total = 100;
|
||||
const onPageChange = (page: number) => {};
|
||||
const limit = 10;
|
||||
return (
|
||||
<div>
|
||||
<Table columns={columns} dataSource={list} pagination={false} />
|
||||
<FullPagination
|
||||
page={page}
|
||||
limit={limit}
|
||||
total={total}
|
||||
listLen={list.length}
|
||||
onPageChange={onPageChange}
|
||||
entity={'events'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import Event from 'Components/DataManagement/Activity/data/Event'
|
||||
|
||||
function UserProperty() {
|
||||
const testEv = new Event({
|
||||
name: '$broswerthing',
|
||||
displayName: 'Browser Thing',
|
||||
description: 'The browser the user is using',
|
||||
customFields: {
|
||||
exampleValue: 'Chrome',
|
||||
type: 'String',
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className="border rounded-lg flex flex-col gap-4 w-full mx-auto" style={{ maxWidth: 1360 }}>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import { Button, Input, Segmented, Table } from 'antd';
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
import Event from 'Components/DataManagement/Activity/data/Event';
|
||||
import { Triangle } from '../Activity/EventDetailsModal';
|
||||
import cn from 'classnames';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import DataItemPage from '../DataItemPage';
|
||||
|
||||
const testAutoEv = new Event({
|
||||
name: 'auto test ev',
|
||||
time: Date.now(),
|
||||
defaultFields: {
|
||||
userId: '123',
|
||||
userLocation: 'NY',
|
||||
userEnvironment: 'Mac OS',
|
||||
},
|
||||
customFields: {},
|
||||
isAutoCapture: true,
|
||||
sessionId: '123123',
|
||||
displayName: 'Test Auto Event',
|
||||
description: 'This is A test Auto Event',
|
||||
monthQuery: 100,
|
||||
monthVolume: 1000,
|
||||
});
|
||||
|
||||
function EventPage() {
|
||||
const [tab, setTab] = React.useState('all');
|
||||
const tabs = [
|
||||
{
|
||||
label: 'All Properties',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: 'Openreplay Properties',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Custom Properties',
|
||||
value: 'custom',
|
||||
},
|
||||
];
|
||||
|
||||
const evWithFields = {
|
||||
...testAutoEv,
|
||||
fields: [
|
||||
{ name: 'User ID', value: testAutoEv.defaultFields.userId },
|
||||
{ name: 'User Location', value: testAutoEv.defaultFields.userLocation },
|
||||
{
|
||||
name: 'User Environment',
|
||||
value: testAutoEv.defaultFields.userEnvironment,
|
||||
},
|
||||
],
|
||||
};
|
||||
return (
|
||||
<DataItemPage
|
||||
item={evWithFields}
|
||||
backLink={{ name: 'Events', to: '/data/events' }}
|
||||
footer={
|
||||
<div className={'rounded-lg border bg-white'}>
|
||||
<div className={'flex items-center gap-2 p-4'}>
|
||||
<div className={'font-semibold text-lg'}>Event Properties</div>
|
||||
<Segmented options={tabs} value={tab} onChange={(v) => setTab(v)} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventPage;
|
||||
299
frontend/app/components/DataManagement/UsersEvents/ListPage.tsx
Normal file
299
frontend/app/components/DataManagement/UsersEvents/ListPage.tsx
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
import React from 'react';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection/FilterSelection';
|
||||
import User from './data/User';
|
||||
import { Input, Table, Button, Dropdown } from 'antd';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { withSiteId, dataManagement } from 'App/routes';
|
||||
import { Filter, Album } from 'lucide-react';
|
||||
import { list } from '../Activity/Page';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import ColumnsModal from 'Components/DataManagement/Activity/ColumnsModal';
|
||||
import FullPagination from 'Shared/FullPagination';
|
||||
|
||||
function ListPage({ view }: { view: 'users' | 'events' }) {
|
||||
const { projectsStore } = useStore();
|
||||
const siteId = projectsStore.activeSiteId;
|
||||
const history = useHistory();
|
||||
const toUser = (id: string) =>
|
||||
history.push(withSiteId(dataManagement.userPage(id), siteId));
|
||||
const toEvent = (id: string) =>
|
||||
history.push(withSiteId(dataManagement.eventPage(id), siteId));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-4 rounded-lg border bg-white mx-auto"
|
||||
style={{ maxWidth: 1360 }}
|
||||
>
|
||||
<div className={'flex items-center justify-between border-b px-4 pt-2 '}>
|
||||
<div className={'font-semibold text-lg capitalize'}>{view}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type={'text'} icon={<Album size={14} />}>
|
||||
Docs
|
||||
</Button>
|
||||
<Input.Search size={'small'} placeholder={'Name, email, ID'} />
|
||||
</div>
|
||||
</div>
|
||||
{view === 'users' ? <UsersList toUser={toUser} /> : <EventsList toEvent={toEvent} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EventsList({ toEvent }: { toEvent: (id: string) => void }) {
|
||||
const columns = [
|
||||
{
|
||||
title: 'Event Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Display Name',
|
||||
dataIndex: 'displayName',
|
||||
key: 'displayName',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.displayName.localeCompare(b.displayName),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.description.localeCompare(b.description),
|
||||
},
|
||||
{
|
||||
title: '30 Day Volume',
|
||||
dataIndex: 'monthVolume',
|
||||
key: 'monthVolume',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.monthVolume.localeCompare(b.monthVolume),
|
||||
},
|
||||
{
|
||||
title: '30 Day Queries',
|
||||
dataIndex: 'monthQuery',
|
||||
key: 'monthQuery',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.monthQuery.localeCompare(b.monthQuery),
|
||||
},
|
||||
];
|
||||
const page = 1;
|
||||
const total = 100;
|
||||
const onPageChange = (page: number) => {};
|
||||
const limit = 10;
|
||||
return (
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={list}
|
||||
pagination={false}
|
||||
onRow={(record) => ({
|
||||
onClick: () => toEvent(record.eventId),
|
||||
})}
|
||||
/>
|
||||
<FullPagination
|
||||
page={page}
|
||||
limit={limit}
|
||||
total={total}
|
||||
listLen={list.length}
|
||||
onPageChange={onPageChange}
|
||||
entity={'events'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UsersList({ toUser }: { toUser: (id: string) => void }) {
|
||||
const [editCols, setEditCols] = React.useState(false);
|
||||
const [hiddenCols, setHiddenCols] = React.useState([]);
|
||||
const testUsers = [
|
||||
new User({
|
||||
name: 'test123',
|
||||
userId: 'email@id.com',
|
||||
distinctId: ['123123123'],
|
||||
userLocation: 'NY',
|
||||
cohorts: ['test'],
|
||||
properties: {
|
||||
email: 'sad;jsadk',
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
new User({
|
||||
name: 'test123',
|
||||
userId: 'email@id.com',
|
||||
distinctId: ['123123123'],
|
||||
userLocation: 'NY',
|
||||
cohorts: ['test'],
|
||||
properties: {
|
||||
email: 'sad;jsadk',
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
new User({
|
||||
name: 'test123',
|
||||
userId: 'email@id.com',
|
||||
distinctId: ['123123123123'],
|
||||
userLocation: 'NY',
|
||||
cohorts: ['test'],
|
||||
properties: {
|
||||
email: 'sad;jsadk',
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
new User({
|
||||
name: 'test123',
|
||||
userId: 'email@id.com',
|
||||
distinctId: ['1231214143123'],
|
||||
userLocation: 'NY',
|
||||
cohorts: ['test'],
|
||||
properties: {
|
||||
email: 'sad;jsadk',
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
];
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: 'Show/Hide Columns',
|
||||
key: 'edit-columns',
|
||||
onClick: () => setTimeout(() => setEditCols(true), 1),
|
||||
},
|
||||
];
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.name.localeCompare(b.name),
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.userId.localeCompare(b.userId),
|
||||
},
|
||||
{
|
||||
title: 'Distinct ID',
|
||||
dataIndex: 'distinctId',
|
||||
key: 'distinctId',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.distinctId[0].localeCompare(b.distinctId[0]),
|
||||
},
|
||||
{
|
||||
title: 'Updated',
|
||||
dataIndex: 'updatedAt',
|
||||
key: 'updatedAt',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.updatedAt.localeCompare(b.updatedAt),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={'click'}
|
||||
placement={'bottomRight'}
|
||||
>
|
||||
<div className={'cursor-pointer'}>
|
||||
<MoreOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
),
|
||||
dataIndex: '$__opts__$',
|
||||
key: '$__opts__$',
|
||||
width: 50,
|
||||
},
|
||||
];
|
||||
|
||||
const page = 1;
|
||||
const total = 10;
|
||||
const onPageChange = (page: number) => {};
|
||||
const limit = 10;
|
||||
const list = [];
|
||||
|
||||
const onAddFilter = () => console.log('add filter');
|
||||
const excludeFilterKeys = [];
|
||||
const excludeCategory = [];
|
||||
|
||||
const shownCols = columns.map((col) => ({
|
||||
...col,
|
||||
hidden: hiddenCols.includes(col.key),
|
||||
}));
|
||||
const onUpdateVisibleCols = (cols: string[]) => {
|
||||
setHiddenCols((_) => {
|
||||
return columns
|
||||
.map((col) =>
|
||||
cols.includes(col.key) || col.key === '$__opts__$' ? null : col.key
|
||||
)
|
||||
.filter(Boolean);
|
||||
});
|
||||
setEditCols(false);
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2 px-4 pb-2">
|
||||
{/* 1.23 -- <span>Show by</span>*/}
|
||||
{/*<Segmented*/}
|
||||
{/* size={'small'}*/}
|
||||
{/* options={[*/}
|
||||
{/* { label: 'Profiles', value: 'profiles' },*/}
|
||||
{/* { label: 'Company', value: 'company' },*/}
|
||||
{/* ]}*/}
|
||||
{/*/>*/}
|
||||
<FilterSelection
|
||||
mode={'filters'}
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
disabled={false}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
excludeCategory={excludeCategory}
|
||||
isLive={false}
|
||||
>
|
||||
<Button
|
||||
icon={<Filter size={16} strokeWidth={1} />}
|
||||
type="default"
|
||||
size={'small'}
|
||||
className="btn-add-filter"
|
||||
>
|
||||
Filters
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
</div>
|
||||
<div className={'relative'}>
|
||||
{editCols ? (
|
||||
<OutsideClickDetectingDiv onClickOutside={() => setEditCols(false)}>
|
||||
<ColumnsModal
|
||||
columns={shownCols.filter((col) => col.key !== '$__opts__$')}
|
||||
onSelect={onUpdateVisibleCols}
|
||||
hiddenCols={hiddenCols}
|
||||
topOffset={'top-24 -mt-4'}
|
||||
/>
|
||||
</OutsideClickDetectingDiv>
|
||||
) : null}
|
||||
<Table
|
||||
onRow={(record) => ({
|
||||
onClick: () => toUser(record.userId),
|
||||
})}
|
||||
pagination={false}
|
||||
rowClassName={'cursor-pointer'}
|
||||
dataSource={testUsers}
|
||||
columns={shownCols}
|
||||
/>
|
||||
</div>
|
||||
<FullPagination
|
||||
page={page}
|
||||
limit={limit}
|
||||
total={total}
|
||||
listLen={list.length}
|
||||
onPageChange={onPageChange}
|
||||
entity={'users'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ListPage);
|
||||
210
frontend/app/components/DataManagement/UsersEvents/UserPage.tsx
Normal file
210
frontend/app/components/DataManagement/UsersEvents/UserPage.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import React from 'react';
|
||||
import EventDetailsModal, { Triangle } from '../Activity/EventDetailsModal';
|
||||
import User from './data/User';
|
||||
import { Dropdown, Popover } from 'antd';
|
||||
import { MoreOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import Event from 'Components/DataManagement/Activity/data/Event';
|
||||
import { Files, Users, Eye, EyeOff } from 'lucide-react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { list } from '../Activity/Page';
|
||||
import Select from 'Shared/Select';
|
||||
import { tsToCheckRecent } from 'App/date';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import UserPropertiesModal from './components/UserPropertiesModal';
|
||||
import Tag from './components/Tag';
|
||||
import EventsByDay from './components/EventsByDay';
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
import { dataManagement } from 'App/routes'
|
||||
|
||||
const card = 'rounded-lg border bg-white';
|
||||
|
||||
function UserPage() {
|
||||
return (
|
||||
<div className={'flex flex-col gap-2 mx-auto'} style={{ maxWidth: 1360 }}>
|
||||
<UserInfo />
|
||||
<Activity />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Activity() {
|
||||
const testEvs = [...list, ...list, ...list];
|
||||
const [show, setShow] = React.useState(true);
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
const onItemClick = (ev: Event) => {
|
||||
showModal(<EventDetailsModal ev={ev} onClose={hideModal} />, {
|
||||
width: 420,
|
||||
right: true,
|
||||
});
|
||||
};
|
||||
const byDays: Record<string, Event[]> = testEvs.reduce((acc, ev) => {
|
||||
const date = tsToCheckRecent(ev.time, 'LLL dd, yyyy');
|
||||
if (!acc[date]) {
|
||||
acc[date] = [];
|
||||
}
|
||||
acc[date].push(ev);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const toggleEvents = () => {
|
||||
setShow((prev) => !prev);
|
||||
};
|
||||
return (
|
||||
<div className={card}>
|
||||
<div className={'px-4 py-2 flex items-center gap-2'}>
|
||||
<div className={'text-lg font-semibold'}>Activity</div>
|
||||
<div className={'link flex gap-1 items-center'}>
|
||||
<span>Play Sessions</span>
|
||||
<Triangle size={10} color={'blue'} />
|
||||
</div>
|
||||
<div className={'ml-auto'} />
|
||||
<div
|
||||
className={'flex items-center gap-2 cursor-pointer'}
|
||||
onClick={toggleEvents}
|
||||
>
|
||||
{!show ? <Eye size={16} /> : <EyeOff size={16} />}
|
||||
<span className={'font-medium'}>{show ? 'Hide' : 'Show'} Events</span>
|
||||
</div>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'DESC' },
|
||||
{ label: 'Oldest', value: 'ASC' },
|
||||
]}
|
||||
defaultValue={'DESC'}
|
||||
plain
|
||||
onChange={({ value }) => {
|
||||
console.log(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={show ? 'block' : 'hidden'}>
|
||||
<EventsByDay byDays={byDays} onItemClick={onItemClick} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserInfo() {
|
||||
const { showModal, hideModal } = useModal();
|
||||
const testUser = new User({
|
||||
name: 'test user',
|
||||
userId: 'test@email.com',
|
||||
distinctId: ['123123123123', '123123123123', '123123123123'],
|
||||
userLocation: 'NY',
|
||||
cohorts: ['test'],
|
||||
properties: {
|
||||
email: 'test@test.com',
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: 'Delete User',
|
||||
key: 'delete-user',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: () => console.log('confirm'),
|
||||
},
|
||||
];
|
||||
|
||||
const showAll = () => {
|
||||
showModal(<UserPropertiesModal properties={testUser.properties} />, {
|
||||
width: 420,
|
||||
right: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={[
|
||||
{ label: 'Users', to: dataManagement.users(), withSiteId: true },
|
||||
{ label: testUser.name },
|
||||
]} />
|
||||
|
||||
<div className={card}>
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={
|
||||
'bg-gray-lighter h-11 w-12 rounded-full flex items-center justify-center text-gray-medium border border-gray-medium'
|
||||
}
|
||||
>
|
||||
{testUser.name.slice(0, 2)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-xl font-semibold">{testUser.name}</div>
|
||||
<div>{testUser.userId}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className={'font-semibold'}>Distinct ID</div>
|
||||
<div>
|
||||
{testUser.distinctId[0]}
|
||||
{testUser.distinctId.length > 1 && (
|
||||
<Popover
|
||||
title={
|
||||
<div className={'text-disabled-text'}>
|
||||
Tracking IDs linked to this user
|
||||
</div>
|
||||
}
|
||||
trigger={'click'}
|
||||
placement={'bottom'}
|
||||
arrow={false}
|
||||
content={
|
||||
<div className={'flex flex-col gap-2'}>
|
||||
{testUser.distinctId.map((id) => (
|
||||
<div className={'w-full group flex justify-between'}>
|
||||
<span>{id}</span>
|
||||
<div
|
||||
className={
|
||||
'hidden group-hover:block cursor-pointer active:text-blue'
|
||||
}
|
||||
onClick={() => copy(id)}
|
||||
>
|
||||
<Files size={14} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={'w-fit cursor-pointer inline-block ml-2'}>
|
||||
<Tag>+{testUser.distinctId.length - 1}</Tag>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className={'font-semibold'}>Location</div>
|
||||
<div>{testUser.userLocation}</div>
|
||||
</div>
|
||||
<div className={'flex items-center gap-4'}>
|
||||
<div onClick={showAll} className={'link font-semibold'}>
|
||||
+{Object.keys(testUser.properties).length} properties
|
||||
</div>
|
||||
<Dropdown
|
||||
menu={{ items: dropdownItems }}
|
||||
trigger={['click']}
|
||||
placement={'bottomRight'}
|
||||
>
|
||||
<div className={'cursor-pointer'}>
|
||||
<MoreOutlined />
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center p-4">
|
||||
<Users size={14} />
|
||||
<div className={'mr-4 ml-2'}>Cohorts</div>
|
||||
{testUser.cohorts.map((cohort) => (
|
||||
<Tag>{cohort}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserPage;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { formatTs } from 'App/date';
|
||||
import { CalendarFold, ChevronRight } from 'lucide-react';
|
||||
|
||||
function EventsByDay({
|
||||
byDays,
|
||||
onItemClick,
|
||||
}: {
|
||||
byDays: Record<string, any[]>;
|
||||
onItemClick: (ev: any) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(byDays).map((date) => (
|
||||
<div className={'flex flex-col'}>
|
||||
<div
|
||||
className={
|
||||
'bg-gray-lightest px-4 py-2 border font-semibold flex items-center gap-2'
|
||||
}
|
||||
>
|
||||
<CalendarFold size={16} />
|
||||
<span>{date}</span>
|
||||
</div>
|
||||
{byDays[date].map((ev) => (
|
||||
<div
|
||||
onClick={() => onItemClick(ev)}
|
||||
className={
|
||||
'hover:bg-gray-lightest border-b cursor-pointer px-4 py-2 flex items-center group'
|
||||
}
|
||||
>
|
||||
<div className={'w-56'}>{formatTs(ev.time, 'HH:mm:ss a')}</div>
|
||||
<div>{ev.name}</div>
|
||||
<div className={'hidden group-hover:block ml-auto'}>
|
||||
<ChevronRight size={16} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventsByDay;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
function Tag({ children }: { children: React.ReactNode }) {
|
||||
return <div className="px-2 bg-gray-lighter rounded-xl">{children}</div>;
|
||||
}
|
||||
|
||||
export default Tag;
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { Input, Button } from 'antd'
|
||||
import { Pencil } from 'lucide-react'
|
||||
|
||||
function UserPropertiesModal({
|
||||
properties,
|
||||
}: {
|
||||
properties: Record<string, string>
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4 flex flex-col gap-4 h-screen w-full">
|
||||
<div className="font-semibold text-xl">All User Properties</div>
|
||||
<Input.Search size={'small'} />
|
||||
{Object.entries(properties).map(([key, value]) => (
|
||||
<Property pkey={key} value={value} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Property({ pkey, value, onSave }: {
|
||||
pkey: string,
|
||||
value: string,
|
||||
onSave?: (key: string, value: string) => void
|
||||
}) {
|
||||
const [isEdit, setIsEdit] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className="p-4 flex items-start border-b group w-full hover:bg-gray-lightest">
|
||||
<div className={'flex-1'}>{pkey}</div>
|
||||
{isEdit ? (
|
||||
<div className={'flex-1 flex flex-col gap-2'}>
|
||||
<Input size={'small'} defaultValue={value} />
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<Button type={'text'} onClick={() => setIsEdit(false)}>Cancel</Button>
|
||||
<Button type={'primary'}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={'flex-1 text-disabled-text flex justify-between items-start'}>
|
||||
<span>{value}</span>
|
||||
<div className={'hidden group-hover:block cursor-pointer active:text-blue ml-auto'} onClick={() => setIsEdit(true)}>
|
||||
<Pencil size={16} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserPropertiesModal;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
export default class User {
|
||||
name: string;
|
||||
userId: string;
|
||||
distinctId: string[];
|
||||
userLocation: string;
|
||||
cohorts: string[];
|
||||
properties: Record<string, any>;
|
||||
updatedAt: number;
|
||||
|
||||
constructor({
|
||||
name,
|
||||
userId,
|
||||
distinctId,
|
||||
userLocation,
|
||||
cohorts,
|
||||
properties,
|
||||
updatedAt
|
||||
}: {
|
||||
name: string;
|
||||
userId: string;
|
||||
distinctId: string[];
|
||||
userLocation: string;
|
||||
cohorts: string[];
|
||||
properties: Record<string, any>;
|
||||
updatedAt: number;
|
||||
}) {
|
||||
this.name = name;
|
||||
this.userId = userId;
|
||||
this.distinctId = distinctId;
|
||||
this.userLocation = userLocation;
|
||||
this.cohorts = cohorts;
|
||||
this.properties = properties;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ function FunnelTable(props: Props) {
|
|||
if (props.compData) {
|
||||
tableData.push({
|
||||
conversion: props.compData.funnel.totalConversionsPercentage,
|
||||
})
|
||||
});
|
||||
const compFunnel = props.compData.funnel;
|
||||
compFunnel.stages.forEach((st, ind) => {
|
||||
tableData[1]['st_' + ind] = st.count;
|
||||
|
|
@ -71,9 +71,7 @@ function FunnelTable(props: Props) {
|
|||
pagination={false}
|
||||
size={'middle'}
|
||||
scroll={{ x: 'max-content' }}
|
||||
rowClassName={(_, index) => (
|
||||
index > 0 ? 'opacity-70' : ''
|
||||
)}
|
||||
rowClassName={(_, index) => (index > 0 ? 'opacity-70' : '')}
|
||||
/>
|
||||
<TableExporter
|
||||
tableColumns={tableProps}
|
||||
|
|
@ -101,7 +99,6 @@ export function TableExporter({
|
|||
}) {
|
||||
const onClick = () => exportAntCsv(tableColumns, tableData, filename);
|
||||
return (
|
||||
<Tooltip title='Export Data to CSV'>
|
||||
<div
|
||||
className={`absolute ${top ? top : 'top-0'} ${
|
||||
right ? right : '-right-1'
|
||||
|
|
@ -111,17 +108,16 @@ export function TableExporter({
|
|||
items={[{ icon: 'download', text: 'Export to CSV', onClick }]}
|
||||
bold
|
||||
customTrigger={
|
||||
<div
|
||||
className={
|
||||
'flex items-center justify-center bg-gradient-to-r from-[#fafafa] to-neutral-200 cursor-pointer rounded-lg h-[38px] w-[38px] btn-export-table-data'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex items-center justify-center bg-gradient-to-r from-[#fafafa] to-neutral-200 cursor-pointer rounded-lg h-[38px] w-[38px] btn-export-table-data'
|
||||
}
|
||||
>
|
||||
<EllipsisVertical size={16} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { withSiteId } from "App/routes";
|
||||
|
||||
interface Props {
|
||||
items: any;
|
||||
|
|
@ -8,6 +11,9 @@ interface Props {
|
|||
|
||||
function Breadcrumb(props: Props) {
|
||||
const { items } = props;
|
||||
const { projectsStore } = useStore();
|
||||
const siteId = projectsStore.activeSiteId;
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex items-center text-lg">
|
||||
{items.map((item: any, index: any) => {
|
||||
|
|
@ -28,7 +34,7 @@ function Breadcrumb(props: Props) {
|
|||
}
|
||||
return (
|
||||
<div key={index} className="color-gray-darkest hover:text-teal group flex items-center">
|
||||
<Link to={item.to} className="flex items-center default-hover">
|
||||
<Link to={item.withSiteId ? withSiteId(item.to, siteId) : item.to} className="flex items-center default-hover">
|
||||
{index === 0 && (
|
||||
<Icon name="chevron-left" size={16} className="mr-1 group-hover:fill-teal" />
|
||||
)}
|
||||
|
|
@ -42,4 +48,4 @@ function Breadcrumb(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default Breadcrumb;
|
||||
export default observer(Breadcrumb);
|
||||
|
|
|
|||
100
frontend/app/components/shared/DNDTable.tsx
Normal file
100
frontend/app/components/shared/DNDTable.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import React, { useRef, useState, useCallback } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import { useDrag, useDrop, DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
|
||||
const type = 'COLUMN';
|
||||
|
||||
function DraggableHeaderCell({ index, moveColumn, children, ...rest }) {
|
||||
const ref = useRef<HTMLTableHeaderCellElement>(null);
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type,
|
||||
item: { index },
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
const [, drop] = useDrop({
|
||||
accept: type,
|
||||
hover: (item, monitor) => {
|
||||
if (!ref.current) return;
|
||||
const dragIndex = item.index;
|
||||
if (dragIndex === index) return;
|
||||
const hoverBoundingRect = ref.current.getBoundingClientRect();
|
||||
const hoverMiddleX =
|
||||
(hoverBoundingRect.right - hoverBoundingRect.left) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientX = clientOffset.x - hoverBoundingRect.left;
|
||||
if (dragIndex < index && hoverClientX < hoverMiddleX) return;
|
||||
if (dragIndex > index && hoverClientX > hoverMiddleX) return;
|
||||
moveColumn(dragIndex, index);
|
||||
item.index = index;
|
||||
},
|
||||
});
|
||||
drag(drop(ref));
|
||||
return (
|
||||
<th
|
||||
ref={ref}
|
||||
{...rest}
|
||||
style={{
|
||||
...rest.style,
|
||||
cursor: 'default',
|
||||
opacity: isDragging ? 0.3 : 1,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className={'cursor-grab'}>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
const DNDTable = ({ columns: initCols, onOrderChange, ...tableProps }) => {
|
||||
const [cols, setCols] = useState(initCols);
|
||||
|
||||
const moveColumn = useCallback(
|
||||
(dragIndex, hoverIndex) => {
|
||||
const updated = [...cols];
|
||||
const [removed] = updated.splice(dragIndex, 1);
|
||||
updated.splice(hoverIndex, 0, removed);
|
||||
setCols(updated);
|
||||
onOrderChange?.(updated);
|
||||
},
|
||||
[cols, onOrderChange]
|
||||
);
|
||||
|
||||
const components = {
|
||||
header: {
|
||||
cell: (cellProps) => {
|
||||
const i = cols.findIndex((c) => c.key === cellProps['data-col-key']);
|
||||
const isOptionsCell = cellProps['data-col-key'] === '$__opts__$';
|
||||
return !isOptionsCell && i > -1 ? (
|
||||
<DraggableHeaderCell
|
||||
{...cellProps}
|
||||
index={i}
|
||||
moveColumn={moveColumn}
|
||||
/>
|
||||
) : (
|
||||
<th {...cellProps} />
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mergedCols = cols.map((col) => ({
|
||||
...col,
|
||||
onHeaderCell: () => ({ 'data-col-key': col.key }),
|
||||
}));
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Table {...tableProps} columns={mergedCols} components={components} />
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DNDTable;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { GripVertical, Plus, Filter } from 'lucide-react';
|
||||
import { GripVertical, Plus, Filter, SquareDashedMousePointer } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button } from 'antd';
|
||||
|
|
@ -27,6 +27,8 @@ interface Props {
|
|||
mergeUp?: boolean;
|
||||
borderless?: boolean;
|
||||
cannotAdd?: boolean;
|
||||
heading?: React.ReactNode;
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
export const FilterList = observer((props: Props) => {
|
||||
|
|
@ -39,7 +41,8 @@ export const FilterList = observer((props: Props) => {
|
|||
onAddFilter,
|
||||
readonly,
|
||||
borderless,
|
||||
excludeCategory
|
||||
excludeCategory,
|
||||
isLive
|
||||
} = props;
|
||||
|
||||
const filters = filter.filters;
|
||||
|
|
@ -70,6 +73,7 @@ export const FilterList = observer((props: Props) => {
|
|||
disabled={readonly}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
excludeCategory={excludeCategory}
|
||||
isLive={isLive}
|
||||
>
|
||||
<Button
|
||||
icon={<Filter size={16} strokeWidth={1} />}
|
||||
|
|
@ -97,6 +101,7 @@ export const FilterList = observer((props: Props) => {
|
|||
isFilter={true}
|
||||
filterIndex={filterIndex}
|
||||
filter={filter}
|
||||
isLive={isLive}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
|
|
@ -213,9 +218,10 @@ export const EventsList = observer((props: Props) => {
|
|||
marginBottom: props.mergeDown ? '-1px' : undefined
|
||||
}}
|
||||
>
|
||||
{props.heading ? props.heading : null}
|
||||
<div className="flex items-center mb-2 gap-2">
|
||||
<div className="font-medium">Events</div>
|
||||
{cannotAdd ? null : (
|
||||
{filters.length === 0 || cannotAdd ? null : (
|
||||
<FilterSelection
|
||||
mode={'events'}
|
||||
filter={undefined}
|
||||
|
|
@ -224,12 +230,12 @@ export const EventsList = observer((props: Props) => {
|
|||
excludeCategory={excludeCategory}
|
||||
>
|
||||
<Button
|
||||
icon={<Plus size={16} strokeWidth={1} />}
|
||||
icon={<SquareDashedMousePointer size={16} strokeWidth={1} />}
|
||||
type="default"
|
||||
size={'small'}
|
||||
className="btn-add-event"
|
||||
>
|
||||
Add
|
||||
Select Event
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
)}
|
||||
|
|
@ -243,6 +249,28 @@ export const EventsList = observer((props: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
<div className={'flex flex-col '}>
|
||||
{filters.length === 0 ? (
|
||||
<div className={'flex items-center gap-2 mb-2'}>
|
||||
<div className={'bg-gray-lighter rounded-full leading-none flex items-center justify-center w-5 h-5'}>
|
||||
<div>1</div>
|
||||
</div>
|
||||
<FilterSelection
|
||||
mode={'events'}
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
excludeCategory={excludeCategory}
|
||||
>
|
||||
<Button
|
||||
icon={<SquareDashedMousePointer size={16} strokeWidth={1} />}
|
||||
type="default"
|
||||
size={'small'}
|
||||
className='btn-add-event'
|
||||
>
|
||||
Select Event
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
</div>
|
||||
) : null}
|
||||
{filters.map((filter: any, filterIndex: number) =>
|
||||
filter.isEvent ? (
|
||||
<div
|
||||
|
|
|
|||
42
frontend/app/components/shared/FullPagination.tsx
Normal file
42
frontend/app/components/shared/FullPagination.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import { Pagination } from 'UI';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
function FullPagination({
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
listLen,
|
||||
onPageChange,
|
||||
entity,
|
||||
}: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
listLen: number;
|
||||
onPageChange: (page: number) => void;
|
||||
entity?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-3 shadow-sm w-full bg-white rounded-lg mt-2">
|
||||
<div>
|
||||
{'Showing '}
|
||||
<span className="font-medium">{(page - 1) * limit + 1}</span>
|
||||
{' to '}
|
||||
<span className="font-medium">{(page - 1) * limit + listLen}</span>
|
||||
{' of '}
|
||||
<span className="font-medium">{numberWithCommas(total)}</span>
|
||||
{entity ? ` ${entity}.` : '.'}
|
||||
</div>
|
||||
<Pagination
|
||||
page={page}
|
||||
total={total}
|
||||
onPageChange={onPageChange}
|
||||
limit={limit}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FullPagination;
|
||||
35
frontend/app/components/shared/Tabs.tsx
Normal file
35
frontend/app/components/shared/Tabs.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import { Tabs, TabsProps } from 'antd';
|
||||
|
||||
const customTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => (
|
||||
<DefaultTabBar {...props} className="!mb-0" />
|
||||
);
|
||||
|
||||
function CustomizedTabs({
|
||||
items,
|
||||
onChange,
|
||||
activeKey,
|
||||
}: {
|
||||
items: { key: string; label: React.ReactNode }[];
|
||||
onChange: (key: string) => void;
|
||||
activeKey: string;
|
||||
}) {
|
||||
const customItems = items.map((i) => ({
|
||||
...i,
|
||||
content: <div>placeholder</div>,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
type={'line'}
|
||||
defaultActiveKey={items[0].key}
|
||||
activeKey={activeKey}
|
||||
style={{ borderBottom: 'none' }}
|
||||
onChange={onChange}
|
||||
items={customItems}
|
||||
renderTabBar={customTabBar}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomizedTabs;
|
||||
|
|
@ -12,7 +12,7 @@ interface Props {
|
|||
function Square_mouse_pointer(props: Props) {
|
||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"/></svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6" fill="none"/></svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ export function getDateFromString(date: string, format = 'yyyy-MM-dd HH:mm:ss:SS
|
|||
return DateTime.fromISO(date).toFormat(format);
|
||||
}
|
||||
|
||||
export const formatTs = (ts: number, format: string): string => {
|
||||
return DateTime.fromMillis(ts).toFormat(format);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats a given duration.
|
||||
*
|
||||
|
|
@ -164,6 +169,9 @@ export const checkForRecent = (date: DateTime, format: string): string => {
|
|||
// Formatted
|
||||
return date.toFormat(format);
|
||||
};
|
||||
export const tsToCheckRecent = (ts: number, format: string): string => {
|
||||
return checkForRecent(DateTime.fromMillis(ts), format);
|
||||
}
|
||||
export const resentOrDate = (ts, short?: boolean) => {
|
||||
const date = DateTime.fromMillis(ts);
|
||||
const d = new Date();
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ function SideMenu(props: Props) {
|
|||
}
|
||||
if (item.hidden) return item;
|
||||
|
||||
const dataAnalytics = [MENU.ACTIVITY, MENU.USERS, MENU.EVENTS]
|
||||
const isHidden = [
|
||||
item.key === MENU.RECOMMENDATIONS &&
|
||||
modules.includes(MODULES.RECOMMENDATIONS),
|
||||
|
|
@ -108,7 +109,8 @@ function SideMenu(props: Props) {
|
|||
item.key === MENU.ALERTS && modules.includes(MODULES.ALERTS),
|
||||
item.key === MENU.USABILITY_TESTS && modules.includes(MODULES.USABILITY_TESTS),
|
||||
item.isAdmin && !isAdmin,
|
||||
item.isEnterprise && !isEnterprise
|
||||
item.isEnterprise && !isEnterprise,
|
||||
dataAnalytics.includes(item.key) && isMobile
|
||||
].some((cond) => cond);
|
||||
|
||||
return { ...item, hidden: isHidden };
|
||||
|
|
@ -152,6 +154,10 @@ function SideMenu(props: Props) {
|
|||
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
|
||||
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
|
||||
[MENU.HIGHLIGHTS]: () => withSiteId(routes.highlights(''), siteId),
|
||||
[MENU.ACTIVITY]: () => withSiteId(routes.dataManagement.activity(), siteId),
|
||||
[MENU.USERS]: () => withSiteId(routes.dataManagement.users(), siteId),
|
||||
[MENU.EVENTS]: () => withSiteId(routes.dataManagement.events(), siteId),
|
||||
[MENU.PROPS]: () => withSiteId(routes.dataManagement.properties(), siteId),
|
||||
};
|
||||
|
||||
const handleClick = (item: any) => {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ export const enum MENU {
|
|||
SUPPORT = 'support',
|
||||
EXIT = 'exit',
|
||||
SPOTS = 'spots',
|
||||
ACTIVITY = 'activity',
|
||||
USER = 'user-page',
|
||||
USERS = 'data-users',
|
||||
EVENTS = 'data-events',
|
||||
PROPS = 'data-properties',
|
||||
DATA_MANAGEMENT = 'data-management',
|
||||
}
|
||||
|
||||
export const categories: Category[] = [
|
||||
|
|
@ -61,26 +67,27 @@ export const categories: Category[] = [
|
|||
key: 'replays',
|
||||
items: [
|
||||
{ label: 'Sessions', key: MENU.SESSIONS, icon: 'collection-play' },
|
||||
{ label: 'Recommendations', key: MENU.RECOMMENDATIONS, icon: 'magic', hidden: true },
|
||||
{
|
||||
label: 'Recommendations',
|
||||
key: MENU.RECOMMENDATIONS,
|
||||
icon: 'magic',
|
||||
hidden: true,
|
||||
},
|
||||
{ label: 'Vault', key: MENU.VAULT, icon: 'safe', hidden: true },
|
||||
{ label: 'Bookmarks', key: MENU.BOOKMARKS, icon: 'bookmark' },
|
||||
//{ label: 'Notes', key: MENU.NOTES, icon: 'stickies' },
|
||||
{ label: 'Highlights', key: MENU.HIGHLIGHTS, icon: 'chat-square-quote' }
|
||||
]
|
||||
{ label: 'Highlights', key: MENU.HIGHLIGHTS, icon: 'chat-square-quote' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'spot',
|
||||
items: [
|
||||
{ label: 'Spots', key: MENU.SPOTS, icon: 'orspotOutline' },
|
||||
]
|
||||
items: [{ label: 'Spots', key: MENU.SPOTS, icon: 'orspotOutline' }],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'assist',
|
||||
items: [
|
||||
{ label: 'Co-Browse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
|
||||
]
|
||||
items: [{ label: 'Co-Browse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' }],
|
||||
},
|
||||
{
|
||||
title: 'Analytics',
|
||||
|
|
@ -96,25 +103,46 @@ export const categories: Category[] = [
|
|||
// { label: 'Resource Monitoring', key: MENU.RESOURCE_MONITORING }
|
||||
// ]
|
||||
// },
|
||||
{ label: 'Alerts', key: MENU.ALERTS, icon: 'bell' }
|
||||
]
|
||||
{ label: 'Alerts', key: MENU.ALERTS, icon: 'bell' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Data Management',
|
||||
key: 'data-management',
|
||||
items: [
|
||||
{ label: 'Activity', key: MENU.ACTIVITY, icon: 'square-mouse-pointer' },
|
||||
{ label: 'Data Management', key: MENU.DATA_MANAGEMENT, icon: 'memory', children: [
|
||||
{ label: 'Users', key: MENU.USERS, icon: 'square-mouse-pointer' },
|
||||
{ label: 'Events', key: MENU.EVENTS, icon: 'square-mouse-pointer' },
|
||||
{ label: 'Properties', key: MENU.PROPS, icon: 'square-mouse-pointer' },
|
||||
]}
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Product Optimization',
|
||||
key: 'product-optimization',
|
||||
items: [
|
||||
{ label: 'Feature Flags', key: MENU.FEATURE_FLAGS, icon: 'toggles' },
|
||||
{ label: 'Usability Tests', key: MENU.USABILITY_TESTS, icon: 'clipboard-check' },
|
||||
]
|
||||
{
|
||||
label: 'Usability Tests',
|
||||
key: MENU.USABILITY_TESTS,
|
||||
icon: 'clipboard-check',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'other',
|
||||
items: [
|
||||
{ label: 'Preferences', key: MENU.PREFERENCES, icon: 'sliders', leading: 'chevron-right' },
|
||||
{ label: 'Support', key: MENU.SUPPORT, icon: 'question-circle' }
|
||||
]
|
||||
}
|
||||
{
|
||||
label: 'Preferences',
|
||||
key: MENU.PREFERENCES,
|
||||
icon: 'sliders',
|
||||
leading: 'chevron-right',
|
||||
},
|
||||
{ label: 'Support', key: MENU.SUPPORT, icon: 'question-circle' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const preferences: Category[] = [
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ export const setQueryParams = (location: Location, params: Record<string, any>):
|
|||
};
|
||||
|
||||
export const login = (): string => '/login';
|
||||
export const spotLogin = (): string => '/spot-login';
|
||||
export const signup = (): string => '/signup';
|
||||
|
||||
export const forgotPassword = (): string => '/reset-password';
|
||||
|
|
@ -148,6 +147,15 @@ export const scopeSetup = (): string => '/scope-setup';
|
|||
|
||||
export const highlights = (): string => '/highlights';
|
||||
|
||||
export const dataManagement = {
|
||||
activity: () => '/data-management/activity',
|
||||
userPage: (id = ':userId', hash?: string | number) => hashed(`/data-management/user/${id}`, hash),
|
||||
users: () => '/data-management/users',
|
||||
events: () => '/data-management/events',
|
||||
eventPage: (id = ':eventId', hash?: string | number) => hashed(`/data-management/event/${id}`, hash),
|
||||
properties: () => '/data-management/properties',
|
||||
}
|
||||
|
||||
const REQUIRED_SITE_ID_ROUTES = [
|
||||
liveSession(''),
|
||||
session(''),
|
||||
|
|
@ -192,6 +200,12 @@ const REQUIRED_SITE_ID_ROUTES = [
|
|||
usabilityTestingView(''),
|
||||
|
||||
highlights(),
|
||||
|
||||
dataManagement.activity(),
|
||||
dataManagement.userPage(''),
|
||||
dataManagement.users(),
|
||||
dataManagement.events(),
|
||||
dataManagement.eventPage(''),
|
||||
];
|
||||
const routeNeedsSiteId = (path: string): boolean => REQUIRED_SITE_ID_ROUTES.some(r => path.startsWith(r));
|
||||
const siteIdToUrl = (siteId = ':siteId'): string => {
|
||||
|
|
|
|||
|
|
@ -403,4 +403,8 @@ svg {
|
|||
background-color: #c6c6c6;
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.ant-table-column-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
@ -1 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-mouse-pointer"><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-mouse-pointer">
|
||||
<path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/>
|
||||
<path d="M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6" stroke="no-fill"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 421 B After Width: | Height: | Size: 450 B |
|
|
@ -116,6 +116,7 @@ function ${titleCase(fileName)}(props: Props) {
|
|||
/clipRule="evenoddCustomFill"/g,
|
||||
'clipRule="evenodd" fillRule="evenodd"'
|
||||
)
|
||||
.replaceAll(`stroke="no-fill"`, 'fill="none"')
|
||||
.replaceAll(/fill-rule/g, 'fillRule')
|
||||
.replaceAll(/fill-opacity/g, 'fillOpacity')
|
||||
.replaceAll(/stop-color/g, 'stopColor')
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitAny": false,
|
||||
"alwaysStrict": true,
|
||||
"strictNullChecks": true,
|
||||
"skipLibCheck": true,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue