Compare commits

...
Sign in to create a new pull request.

14 commits

Author SHA1 Message Date
nick-delirium
8481e38aa5
ui: add grouping, icon 2025-02-18 17:52:30 +01:00
nick-delirium
fce6a562fd
ui: reorganising side menu and prop/ev lists 2025-02-18 09:13:15 +01:00
nick-delirium
3a07a20195
ui: drag and drop table comp, separate item page 2025-02-17 14:43:07 +01:00
nick-delirium
a9db2f224d
ui: event page 2025-02-14 14:46:14 +01:00
nick-delirium
b465da5a15
ui: event and user props lists 2025-02-14 12:32:45 +01:00
nick-delirium
bb2f39517b
ui: fixing the layout 2025-02-14 12:32:45 +01:00
nick-delirium
73f06bf3eb
ui: more props for events, event types table 2025-02-14 12:32:45 +01:00
nick-delirium
de1e1ca44b
ui: tracked user profile and list 2025-02-14 12:32:45 +01:00
nick-delirium
60fa20d21d
ui: typo 2025-02-14 12:32:45 +01:00
nick-delirium
27b6128c9b
ui: gap for header 2025-02-14 12:32:45 +01:00
nick-delirium
95f8002eb4
ui: cols modal, more fixes 2025-02-14 12:32:45 +01:00
nick-delirium
dcebeb7b5b
ui: fetcher, better type mapping 2025-02-14 12:32:44 +01:00
nick-delirium
1851b046af
ui: even details modal 2025-02-14 12:32:44 +01:00
nick-delirium
6097af4839
ui: start event analytics 2025-02-14 12:32:43 +01:00
31 changed files with 2231 additions and 40 deletions

View file

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

View file

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

View file

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

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

View file

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

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

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

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

View file

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

View file

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

View file

@ -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) => {

View file

@ -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[] = [

View file

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

View file

@ -403,4 +403,8 @@ svg {
background-color: #c6c6c6; background-color: #c6c6c6;
border-radius: 4px; border-radius: 4px;
cursor: grab; cursor: grab;
}
.ant-table-column-title {
font-weight: 500;
} }

View file

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

View file

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

View file

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