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')),
|
SpotPure: lazy(() => import('Components/Spots/SpotPlayer')),
|
||||||
ScopeSetup: lazy(() => import('Components/ScopeForm')),
|
ScopeSetup: lazy(() => import('Components/ScopeForm')),
|
||||||
HighlightsPure: lazy(() => import('Components/Highlights/HighlightsList')),
|
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 = {
|
const enhancedComponents: any = {
|
||||||
|
|
@ -60,6 +65,11 @@ const enhancedComponents: any = {
|
||||||
Spot: components.SpotPure,
|
Spot: components.SpotPure,
|
||||||
ScopeSetup: components.ScopeSetup,
|
ScopeSetup: components.ScopeSetup,
|
||||||
Highlights: components.HighlightsPure,
|
Highlights: components.HighlightsPure,
|
||||||
|
Activity: components.ActivityPure,
|
||||||
|
UserPage: components.UserPage,
|
||||||
|
UsersEventsPage: components.UsersEventsPage,
|
||||||
|
EventPage: components.EventPage,
|
||||||
|
PropertiesList: components.PropertiesList,
|
||||||
};
|
};
|
||||||
|
|
||||||
const withSiteId = routes.withSiteId;
|
const withSiteId = routes.withSiteId;
|
||||||
|
|
@ -290,6 +300,44 @@ function PrivateRoutes() {
|
||||||
path={withSiteId(LIVE_SESSION_PATH, siteIdList)}
|
path={withSiteId(LIVE_SESSION_PATH, siteIdList)}
|
||||||
component={enhancedComponents.LiveSession}
|
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]) => (
|
{Object.entries(routes.redirects).map(([fr, to]) => (
|
||||||
<Redirect key={fr} exact strict from={fr} to={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) {
|
if (props.compData) {
|
||||||
tableData.push({
|
tableData.push({
|
||||||
conversion: props.compData.funnel.totalConversionsPercentage,
|
conversion: props.compData.funnel.totalConversionsPercentage,
|
||||||
})
|
});
|
||||||
const compFunnel = props.compData.funnel;
|
const compFunnel = props.compData.funnel;
|
||||||
compFunnel.stages.forEach((st, ind) => {
|
compFunnel.stages.forEach((st, ind) => {
|
||||||
tableData[1]['st_' + ind] = st.count;
|
tableData[1]['st_' + ind] = st.count;
|
||||||
|
|
@ -71,9 +71,7 @@ function FunnelTable(props: Props) {
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size={'middle'}
|
size={'middle'}
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
rowClassName={(_, index) => (
|
rowClassName={(_, index) => (index > 0 ? 'opacity-70' : '')}
|
||||||
index > 0 ? 'opacity-70' : ''
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<TableExporter
|
<TableExporter
|
||||||
tableColumns={tableProps}
|
tableColumns={tableProps}
|
||||||
|
|
@ -101,7 +99,6 @@ export function TableExporter({
|
||||||
}) {
|
}) {
|
||||||
const onClick = () => exportAntCsv(tableColumns, tableData, filename);
|
const onClick = () => exportAntCsv(tableColumns, tableData, filename);
|
||||||
return (
|
return (
|
||||||
<Tooltip title='Export Data to CSV'>
|
|
||||||
<div
|
<div
|
||||||
className={`absolute ${top ? top : 'top-0'} ${
|
className={`absolute ${top ? top : 'top-0'} ${
|
||||||
right ? right : '-right-1'
|
right ? right : '-right-1'
|
||||||
|
|
@ -111,17 +108,16 @@ export function TableExporter({
|
||||||
items={[{ icon: 'download', text: 'Export to CSV', onClick }]}
|
items={[{ icon: 'download', text: 'Export to CSV', onClick }]}
|
||||||
bold
|
bold
|
||||||
customTrigger={
|
customTrigger={
|
||||||
<div
|
<div
|
||||||
className={
|
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'
|
'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} />
|
<EllipsisVertical size={16} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from 'UI';
|
import { Icon } from 'UI';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useStore } from 'App/mstore'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { withSiteId } from "App/routes";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: any;
|
items: any;
|
||||||
|
|
@ -8,6 +11,9 @@ interface Props {
|
||||||
|
|
||||||
function Breadcrumb(props: Props) {
|
function Breadcrumb(props: Props) {
|
||||||
const { items } = props;
|
const { items } = props;
|
||||||
|
const { projectsStore } = useStore();
|
||||||
|
const siteId = projectsStore.activeSiteId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 flex items-center text-lg">
|
<div className="mb-3 flex items-center text-lg">
|
||||||
{items.map((item: any, index: any) => {
|
{items.map((item: any, index: any) => {
|
||||||
|
|
@ -28,7 +34,7 @@ function Breadcrumb(props: Props) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={index} className="color-gray-darkest hover:text-teal group flex items-center">
|
<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 && (
|
{index === 0 && (
|
||||||
<Icon name="chevron-left" size={16} className="mr-1 group-hover:fill-teal" />
|
<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 { observer } from 'mobx-react-lite';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
|
|
@ -27,6 +27,8 @@ interface Props {
|
||||||
mergeUp?: boolean;
|
mergeUp?: boolean;
|
||||||
borderless?: boolean;
|
borderless?: boolean;
|
||||||
cannotAdd?: boolean;
|
cannotAdd?: boolean;
|
||||||
|
heading?: React.ReactNode;
|
||||||
|
isLive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterList = observer((props: Props) => {
|
export const FilterList = observer((props: Props) => {
|
||||||
|
|
@ -39,7 +41,8 @@ export const FilterList = observer((props: Props) => {
|
||||||
onAddFilter,
|
onAddFilter,
|
||||||
readonly,
|
readonly,
|
||||||
borderless,
|
borderless,
|
||||||
excludeCategory
|
excludeCategory,
|
||||||
|
isLive
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const filters = filter.filters;
|
const filters = filter.filters;
|
||||||
|
|
@ -70,6 +73,7 @@ export const FilterList = observer((props: Props) => {
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
excludeFilterKeys={excludeFilterKeys}
|
excludeFilterKeys={excludeFilterKeys}
|
||||||
excludeCategory={excludeCategory}
|
excludeCategory={excludeCategory}
|
||||||
|
isLive={isLive}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<Filter size={16} strokeWidth={1} />}
|
icon={<Filter size={16} strokeWidth={1} />}
|
||||||
|
|
@ -97,6 +101,7 @@ export const FilterList = observer((props: Props) => {
|
||||||
isFilter={true}
|
isFilter={true}
|
||||||
filterIndex={filterIndex}
|
filterIndex={filterIndex}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
isLive={isLive}
|
||||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||||
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
onRemoveFilter={() => onRemoveFilter(filterIndex)}
|
||||||
excludeFilterKeys={excludeFilterKeys}
|
excludeFilterKeys={excludeFilterKeys}
|
||||||
|
|
@ -213,9 +218,10 @@ export const EventsList = observer((props: Props) => {
|
||||||
marginBottom: props.mergeDown ? '-1px' : undefined
|
marginBottom: props.mergeDown ? '-1px' : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{props.heading ? props.heading : null}
|
||||||
<div className="flex items-center mb-2 gap-2">
|
<div className="flex items-center mb-2 gap-2">
|
||||||
<div className="font-medium">Events</div>
|
<div className="font-medium">Events</div>
|
||||||
{cannotAdd ? null : (
|
{filters.length === 0 || cannotAdd ? null : (
|
||||||
<FilterSelection
|
<FilterSelection
|
||||||
mode={'events'}
|
mode={'events'}
|
||||||
filter={undefined}
|
filter={undefined}
|
||||||
|
|
@ -224,12 +230,12 @@ export const EventsList = observer((props: Props) => {
|
||||||
excludeCategory={excludeCategory}
|
excludeCategory={excludeCategory}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<Plus size={16} strokeWidth={1} />}
|
icon={<SquareDashedMousePointer size={16} strokeWidth={1} />}
|
||||||
type="default"
|
type="default"
|
||||||
size={'small'}
|
size={'small'}
|
||||||
className="btn-add-event"
|
className="btn-add-event"
|
||||||
>
|
>
|
||||||
Add
|
Select Event
|
||||||
</Button>
|
</Button>
|
||||||
</FilterSelection>
|
</FilterSelection>
|
||||||
)}
|
)}
|
||||||
|
|
@ -243,6 +249,28 @@ export const EventsList = observer((props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex flex-col '}>
|
<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) =>
|
{filters.map((filter: any, filterIndex: number) =>
|
||||||
filter.isEvent ? (
|
filter.isEvent ? (
|
||||||
<div
|
<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) {
|
function Square_mouse_pointer(props: Props) {
|
||||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||||
return (
|
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);
|
return DateTime.fromISO(date).toFormat(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatTs = (ts: number, format: string): string => {
|
||||||
|
return DateTime.fromMillis(ts).toFormat(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a given duration.
|
* Formats a given duration.
|
||||||
*
|
*
|
||||||
|
|
@ -164,6 +169,9 @@ export const checkForRecent = (date: DateTime, format: string): string => {
|
||||||
// Formatted
|
// Formatted
|
||||||
return date.toFormat(format);
|
return date.toFormat(format);
|
||||||
};
|
};
|
||||||
|
export const tsToCheckRecent = (ts: number, format: string): string => {
|
||||||
|
return checkForRecent(DateTime.fromMillis(ts), format);
|
||||||
|
}
|
||||||
export const resentOrDate = (ts, short?: boolean) => {
|
export const resentOrDate = (ts, short?: boolean) => {
|
||||||
const date = DateTime.fromMillis(ts);
|
const date = DateTime.fromMillis(ts);
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ function SideMenu(props: Props) {
|
||||||
}
|
}
|
||||||
if (item.hidden) return item;
|
if (item.hidden) return item;
|
||||||
|
|
||||||
|
const dataAnalytics = [MENU.ACTIVITY, MENU.USERS, MENU.EVENTS]
|
||||||
const isHidden = [
|
const isHidden = [
|
||||||
item.key === MENU.RECOMMENDATIONS &&
|
item.key === MENU.RECOMMENDATIONS &&
|
||||||
modules.includes(MODULES.RECOMMENDATIONS),
|
modules.includes(MODULES.RECOMMENDATIONS),
|
||||||
|
|
@ -108,7 +109,8 @@ function SideMenu(props: Props) {
|
||||||
item.key === MENU.ALERTS && modules.includes(MODULES.ALERTS),
|
item.key === MENU.ALERTS && modules.includes(MODULES.ALERTS),
|
||||||
item.key === MENU.USABILITY_TESTS && modules.includes(MODULES.USABILITY_TESTS),
|
item.key === MENU.USABILITY_TESTS && modules.includes(MODULES.USABILITY_TESTS),
|
||||||
item.isAdmin && !isAdmin,
|
item.isAdmin && !isAdmin,
|
||||||
item.isEnterprise && !isEnterprise
|
item.isEnterprise && !isEnterprise,
|
||||||
|
dataAnalytics.includes(item.key) && isMobile
|
||||||
].some((cond) => cond);
|
].some((cond) => cond);
|
||||||
|
|
||||||
return { ...item, hidden: isHidden };
|
return { ...item, hidden: isHidden };
|
||||||
|
|
@ -152,6 +154,10 @@ function SideMenu(props: Props) {
|
||||||
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
|
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
|
||||||
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
|
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
|
||||||
[MENU.HIGHLIGHTS]: () => withSiteId(routes.highlights(''), siteId),
|
[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) => {
|
const handleClick = (item: any) => {
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ export const enum MENU {
|
||||||
SUPPORT = 'support',
|
SUPPORT = 'support',
|
||||||
EXIT = 'exit',
|
EXIT = 'exit',
|
||||||
SPOTS = 'spots',
|
SPOTS = 'spots',
|
||||||
|
ACTIVITY = 'activity',
|
||||||
|
USER = 'user-page',
|
||||||
|
USERS = 'data-users',
|
||||||
|
EVENTS = 'data-events',
|
||||||
|
PROPS = 'data-properties',
|
||||||
|
DATA_MANAGEMENT = 'data-management',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const categories: Category[] = [
|
export const categories: Category[] = [
|
||||||
|
|
@ -61,26 +67,27 @@ export const categories: Category[] = [
|
||||||
key: 'replays',
|
key: 'replays',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Sessions', key: MENU.SESSIONS, icon: 'collection-play' },
|
{ 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: 'Vault', key: MENU.VAULT, icon: 'safe', hidden: true },
|
||||||
{ label: 'Bookmarks', key: MENU.BOOKMARKS, icon: 'bookmark' },
|
{ label: 'Bookmarks', key: MENU.BOOKMARKS, icon: 'bookmark' },
|
||||||
//{ label: 'Notes', key: MENU.NOTES, icon: 'stickies' },
|
//{ 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: '',
|
title: '',
|
||||||
key: 'spot',
|
key: 'spot',
|
||||||
items: [
|
items: [{ label: 'Spots', key: MENU.SPOTS, icon: 'orspotOutline' }],
|
||||||
{ label: 'Spots', key: MENU.SPOTS, icon: 'orspotOutline' },
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
key: 'assist',
|
key: 'assist',
|
||||||
items: [
|
items: [{ label: 'Co-Browse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' }],
|
||||||
{ label: 'Co-Browse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Analytics',
|
title: 'Analytics',
|
||||||
|
|
@ -96,25 +103,46 @@ export const categories: Category[] = [
|
||||||
// { label: 'Resource Monitoring', key: MENU.RESOURCE_MONITORING }
|
// { 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',
|
title: 'Product Optimization',
|
||||||
key: 'product-optimization',
|
key: 'product-optimization',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Feature Flags', key: MENU.FEATURE_FLAGS, icon: 'toggles' },
|
{ 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: '',
|
title: '',
|
||||||
key: 'other',
|
key: 'other',
|
||||||
items: [
|
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[] = [
|
export const preferences: Category[] = [
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ export const setQueryParams = (location: Location, params: Record<string, any>):
|
||||||
};
|
};
|
||||||
|
|
||||||
export const login = (): string => '/login';
|
export const login = (): string => '/login';
|
||||||
export const spotLogin = (): string => '/spot-login';
|
|
||||||
export const signup = (): string => '/signup';
|
export const signup = (): string => '/signup';
|
||||||
|
|
||||||
export const forgotPassword = (): string => '/reset-password';
|
export const forgotPassword = (): string => '/reset-password';
|
||||||
|
|
@ -148,6 +147,15 @@ export const scopeSetup = (): string => '/scope-setup';
|
||||||
|
|
||||||
export const highlights = (): string => '/highlights';
|
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 = [
|
const REQUIRED_SITE_ID_ROUTES = [
|
||||||
liveSession(''),
|
liveSession(''),
|
||||||
session(''),
|
session(''),
|
||||||
|
|
@ -192,6 +200,12 @@ const REQUIRED_SITE_ID_ROUTES = [
|
||||||
usabilityTestingView(''),
|
usabilityTestingView(''),
|
||||||
|
|
||||||
highlights(),
|
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 routeNeedsSiteId = (path: string): boolean => REQUIRED_SITE_ID_ROUTES.some(r => path.startsWith(r));
|
||||||
const siteIdToUrl = (siteId = ':siteId'): string => {
|
const siteIdToUrl = (siteId = ':siteId'): string => {
|
||||||
|
|
|
||||||
|
|
@ -404,3 +404,7 @@ svg {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: grab;
|
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="evenoddCustomFill"/g,
|
||||||
'clipRule="evenodd" fillRule="evenodd"'
|
'clipRule="evenodd" fillRule="evenodd"'
|
||||||
)
|
)
|
||||||
|
.replaceAll(`stroke="no-fill"`, 'fill="none"')
|
||||||
.replaceAll(/fill-rule/g, 'fillRule')
|
.replaceAll(/fill-rule/g, 'fillRule')
|
||||||
.replaceAll(/fill-opacity/g, 'fillOpacity')
|
.replaceAll(/fill-opacity/g, 'fillOpacity')
|
||||||
.replaceAll(/stop-color/g, 'stopColor')
|
.replaceAll(/stop-color/g, 'stopColor')
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": false,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue