ui: tracked user profile and list
This commit is contained in:
parent
60fa20d21d
commit
de1e1ca44b
15 changed files with 676 additions and 46 deletions
|
|
@ -38,6 +38,8 @@ const components: any = {
|
||||||
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')),
|
ActivityPure: lazy(() => import('Components/DataManagement/Activity/Page')),
|
||||||
|
UserPage: lazy(() => import('Components/DataManagement/UsersEvents/UserPage')),
|
||||||
|
UsersEventsPage: lazy(() => import('Components/DataManagement/UsersEvents/ListPage')),
|
||||||
};
|
};
|
||||||
|
|
||||||
const enhancedComponents: any = {
|
const enhancedComponents: any = {
|
||||||
|
|
@ -62,6 +64,8 @@ const enhancedComponents: any = {
|
||||||
ScopeSetup: components.ScopeSetup,
|
ScopeSetup: components.ScopeSetup,
|
||||||
Highlights: components.HighlightsPure,
|
Highlights: components.HighlightsPure,
|
||||||
Activity: components.ActivityPure,
|
Activity: components.ActivityPure,
|
||||||
|
UserPage: components.UserPage,
|
||||||
|
UsersEventsPage: components.UsersEventsPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const withSiteId = routes.withSiteId;
|
const withSiteId = routes.withSiteId;
|
||||||
|
|
@ -112,7 +116,9 @@ const SCOPE_SETUP = routes.scopeSetup();
|
||||||
const HIGHLIGHTS_PATH = routes.highlights();
|
const HIGHLIGHTS_PATH = routes.highlights();
|
||||||
|
|
||||||
const DATA_MANAGEMENT = {
|
const DATA_MANAGEMENT = {
|
||||||
ACTIVITY: routes.dataManagement.activity()
|
ACTIVITY: routes.dataManagement.activity(),
|
||||||
|
USER_PAGE: routes.dataManagement.userPage(),
|
||||||
|
USERS_EVENTS: routes.dataManagement.usersEvents(),
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrivateRoutes() {
|
function PrivateRoutes() {
|
||||||
|
|
@ -302,6 +308,18 @@ function PrivateRoutes() {
|
||||||
path={withSiteId(DATA_MANAGEMENT.ACTIVITY, siteIdList)}
|
path={withSiteId(DATA_MANAGEMENT.ACTIVITY, siteIdList)}
|
||||||
component={enhancedComponents.Activity}
|
component={enhancedComponents.Activity}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
strict
|
||||||
|
path={withSiteId(DATA_MANAGEMENT.USER_PAGE, siteIdList)}
|
||||||
|
component={enhancedComponents.UserPage}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
strict
|
||||||
|
path={withSiteId(DATA_MANAGEMENT.USERS_EVENTS, siteIdList)}
|
||||||
|
component={enhancedComponents.UsersEventsPage}
|
||||||
|
/>
|
||||||
{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} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function EventDetailsModal({ ev, onClose }: { ev: EventData, onClose: () => void
|
||||||
return (
|
return (
|
||||||
<div className={'h-screen w-full flex flex-col gap-4 p-4'}>
|
<div className={'h-screen w-full flex flex-col gap-4 p-4'}>
|
||||||
<div className={'flex justify-between items-center'}>
|
<div className={'flex justify-between items-center'}>
|
||||||
<div className={'font-semibold text-lg'}>Event</div>
|
<div className={'font-semibold text-xl'}>Event</div>
|
||||||
<div className={'p-2 cursor-pointer'} onClick={onClose}>
|
<div className={'p-2 cursor-pointer'} onClick={onClose}>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -112,7 +112,7 @@ function EventDetailsModal({ ev, onClose }: { ev: EventData, onClose: () => void
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Triangle({ size = 16, color = 'currentColor' }) {
|
export function Triangle({ size = 16, color = 'currentColor' }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
||||||
|
|
@ -11,45 +11,52 @@ import { useModal } from 'App/components/Modal';
|
||||||
import EventDetailsModal from './EventDetailsModal';
|
import EventDetailsModal from './EventDetailsModal';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import Select from 'Shared/Select';
|
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';
|
||||||
|
|
||||||
const limit = 100;
|
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',
|
||||||
|
});
|
||||||
|
const testAutoEv = new Event({
|
||||||
|
name: 'auto test ev',
|
||||||
|
time: Date.now(),
|
||||||
|
defaultFields: {
|
||||||
|
userId: '123',
|
||||||
|
userLocation: 'NY',
|
||||||
|
userEnvironment: 'Mac OS',
|
||||||
|
},
|
||||||
|
customFields: {},
|
||||||
|
isAutoCapture: true,
|
||||||
|
sessionId: '123123',
|
||||||
|
});
|
||||||
|
export const list = [testEv.toData(), testAutoEv.toData()];
|
||||||
|
|
||||||
const fetcher = async (
|
const fetcher = async (
|
||||||
page: number
|
page: number
|
||||||
): Promise<{ list: any[]; total: number }> => {
|
): Promise<{ list: any[]; total: number }> => {
|
||||||
const total = 3000;
|
const total = 3000;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const testEv = new Event({
|
|
||||||
name: 'test ev #' + page,
|
|
||||||
time: Date.now(),
|
|
||||||
defaultFields: {
|
|
||||||
userId: '123',
|
|
||||||
userCity: 'NY',
|
|
||||||
userEnvironment: 'Mac OS',
|
|
||||||
},
|
|
||||||
customFields: {},
|
|
||||||
isAutoCapture: false,
|
|
||||||
sessionId: '123123',
|
|
||||||
});
|
|
||||||
const testAutoEv = new Event({
|
|
||||||
name: 'auto test ev',
|
|
||||||
time: Date.now(),
|
|
||||||
defaultFields: {
|
|
||||||
userId: '123',
|
|
||||||
userCity: 'NY',
|
|
||||||
userEnvironment: 'Mac OS',
|
|
||||||
},
|
|
||||||
customFields: {},
|
|
||||||
isAutoCapture: true,
|
|
||||||
sessionId: '123123',
|
|
||||||
});
|
|
||||||
const list = [testEv.toData(), testAutoEv.toData()];
|
|
||||||
|
|
||||||
resolve({ list, total });
|
resolve({ list, total });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function ActivityPage() {
|
function ActivityPage() {
|
||||||
|
const { projectsStore } = useStore()
|
||||||
|
const siteId = projectsStore.activeSiteId;
|
||||||
|
|
||||||
const [page, setPage] = React.useState(1);
|
const [page, setPage] = React.useState(1);
|
||||||
const [hiddenCols, setHiddenCols] = React.useState([]);
|
const [hiddenCols, setHiddenCols] = React.useState([]);
|
||||||
const { data, isPending } = useQuery({
|
const { data, isPending } = useQuery({
|
||||||
|
|
@ -106,22 +113,23 @@ function ActivityPage() {
|
||||||
showSorterTooltip: { target: 'full-header' },
|
showSorterTooltip: { target: 'full-header' },
|
||||||
sorter: (a, b) => a.userId.localeCompare(b.userId),
|
sorter: (a, b) => a.userId.localeCompare(b.userId),
|
||||||
render: (text) => (
|
render: (text) => (
|
||||||
<div
|
<Link
|
||||||
|
to={withSiteId(dataManagement.userPage(text), siteId)}
|
||||||
className={'link'}
|
className={'link'}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'City',
|
title: 'City',
|
||||||
dataIndex: 'userCity',
|
dataIndex: 'userLocation',
|
||||||
key: 'userCity',
|
key: 'userLocation',
|
||||||
showSorterTooltip: { target: 'full-header' },
|
showSorterTooltip: { target: 'full-header' },
|
||||||
sorter: (a, b) => a.userCity.localeCompare(b.userCity),
|
sorter: (a, b) => a.userLocation.localeCompare(b.userLocation),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Environment',
|
title: 'Environment',
|
||||||
|
|
@ -234,23 +242,23 @@ function ActivityPage() {
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Past 24 Hours', value: 'DESC' },
|
{ label: 'Past 24 Hours', value: 'DESC' },
|
||||||
{ label: 'Weekly', value: 'ASC' },
|
{ label: 'Weekly', value: 'ASC' },
|
||||||
{ label: 'Other', value: 'Stuff' }
|
{ label: 'Other', value: 'Stuff' },
|
||||||
]}
|
]}
|
||||||
defaultValue={'DESC'}
|
defaultValue={'DESC'}
|
||||||
plain
|
plain
|
||||||
onChange={({ value }) => {
|
onChange={({ value }) => {
|
||||||
console.log(value)
|
console.log(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
{ label: 'Newest', value: 'DESC' },
|
{ label: 'Newest', value: 'DESC' },
|
||||||
{ label: 'Oldest', value: 'ASC' }
|
{ label: 'Oldest', value: 'ASC' },
|
||||||
]}
|
]}
|
||||||
defaultValue={'DESC'}
|
defaultValue={'DESC'}
|
||||||
plain
|
plain
|
||||||
onChange={({ value }) => {
|
onChange={({ value }) => {
|
||||||
console.log(value)
|
console.log(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -289,4 +297,4 @@ function ActivityPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ActivityPage;
|
export default observer(ActivityPage);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ interface DefaultFields {
|
||||||
|
|
||||||
export interface EventData {
|
export interface EventData {
|
||||||
name: string;
|
name: string;
|
||||||
time: string;
|
time: number;
|
||||||
$_isAutoCapture: boolean;
|
$_isAutoCapture: boolean;
|
||||||
$_defaultFields: DefaultFields;
|
$_defaultFields: DefaultFields;
|
||||||
$_customFields?: Record<string, any>;
|
$_customFields?: Record<string, any>;
|
||||||
|
|
@ -14,10 +14,10 @@ export interface EventData {
|
||||||
|
|
||||||
export default class Event {
|
export default class Event {
|
||||||
name: string;
|
name: string;
|
||||||
time: string;
|
time: number;
|
||||||
defaultFields: DefaultFields = {
|
defaultFields: DefaultFields = {
|
||||||
userId: '',
|
userId: '',
|
||||||
userCity: '',
|
userLocation: '',
|
||||||
userEnvironment: '',
|
userEnvironment: '',
|
||||||
}
|
}
|
||||||
customFields?: Record<string,any> = undefined;
|
customFields?: Record<string,any> = undefined;
|
||||||
|
|
@ -35,7 +35,7 @@ export default class Event {
|
||||||
isAutoCapture,
|
isAutoCapture,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
time: string;
|
time: number;
|
||||||
defaultFields: DefaultFields;
|
defaultFields: DefaultFields;
|
||||||
customFields?: Record<string, any>;
|
customFields?: Record<string, any>;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
|
||||||
236
frontend/app/components/DataManagement/UsersEvents/ListPage.tsx
Normal file
236
frontend/app/components/DataManagement/UsersEvents/ListPage.tsx
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { numberWithCommas } from 'App/utils';
|
||||||
|
import FilterSelection from "Shared/Filters/FilterSelection/FilterSelection";
|
||||||
|
import User from './data/User';
|
||||||
|
import { Pagination } from 'UI';
|
||||||
|
import { Segmented, Input, Table, Button, Dropdown, Tabs } from 'antd';
|
||||||
|
import { MoreOutlined } from '@ant-design/icons';
|
||||||
|
import { TabsProps } from ".store/antd-virtual-7db13b4af6/package";
|
||||||
|
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 } from "lucide-react";
|
||||||
|
|
||||||
|
const customTabBar: TabsProps['renderTabBar'] = (props, DefaultTabBar) => (
|
||||||
|
<DefaultTabBar {...props} className="!mb-0" />
|
||||||
|
);
|
||||||
|
|
||||||
|
function ListPage() {
|
||||||
|
const { projectsStore } = useStore();
|
||||||
|
const siteId = projectsStore.activeSiteId;
|
||||||
|
const history = useHistory();
|
||||||
|
const toUser = (id: string) => history.push(withSiteId(dataManagement.userPage(id), siteId));
|
||||||
|
const [view, setView] = React.useState('users');
|
||||||
|
|
||||||
|
const views = [
|
||||||
|
{
|
||||||
|
key: 'users',
|
||||||
|
label: <div className={'text-lg font-medium'}>Users</div>,
|
||||||
|
content: <div>placeholder</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'events',
|
||||||
|
label: <div className={'text-lg font-medium'}>Events</div>,
|
||||||
|
content: <div>placeholder</div>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 p-4 pt-2 rounded-lg border bg-white">
|
||||||
|
<div className={'flex items-center justify-between border-b'}>
|
||||||
|
<Tabs
|
||||||
|
type={'line'}
|
||||||
|
defaultActiveKey={'users'}
|
||||||
|
activeKey={view}
|
||||||
|
style={{ borderBottom: 'none' }}
|
||||||
|
onChange={(key) => setView(key)}
|
||||||
|
items={views}
|
||||||
|
renderTabBar={customTabBar}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button type={'text'}>Docs</Button>
|
||||||
|
<Input.Search placeholder={'Name, email, ID'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{view === 'users' ? <UsersList toUser={toUser} /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UsersList({ toUser }: { toUser: (id: string) => void }) {
|
||||||
|
const [editCols, setEditCols] = React.useState(false);
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-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>
|
||||||
|
<Table
|
||||||
|
onRow={(record) => ({
|
||||||
|
onClick: () => toUser(record.userId),
|
||||||
|
})}
|
||||||
|
pagination={false}
|
||||||
|
rowClassName={'cursor-pointer'}
|
||||||
|
dataSource={testUsers}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
<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 + list.length}
|
||||||
|
</span>
|
||||||
|
{' of '}
|
||||||
|
<span className="font-medium">{numberWithCommas(total)}</span>
|
||||||
|
{' users.'}
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
total={total}
|
||||||
|
onPageChange={onPageChange}
|
||||||
|
limit={limit}
|
||||||
|
debounceRequest={500}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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.usersEvents(), 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ function SideMenu(props: Props) {
|
||||||
[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.ACTIVITY]: () => withSiteId(routes.dataManagement.activity(), siteId),
|
||||||
|
[MENU.USERS_EVENTS]: () => withSiteId(routes.dataManagement.usersEvents(), siteId),
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (item: any) => {
|
const handleClick = (item: any) => {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ export const enum MENU {
|
||||||
EXIT = 'exit',
|
EXIT = 'exit',
|
||||||
SPOTS = 'spots',
|
SPOTS = 'spots',
|
||||||
ACTIVITY = 'activity',
|
ACTIVITY = 'activity',
|
||||||
|
USER_PAGE = 'user-page',
|
||||||
|
USERS_EVENTS = 'users-events',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const categories: Category[] = [
|
export const categories: Category[] = [
|
||||||
|
|
@ -104,7 +106,7 @@ export const categories: Category[] = [
|
||||||
{
|
{
|
||||||
title: 'Data Management',
|
title: 'Data Management',
|
||||||
key: 'data-management',
|
key: 'data-management',
|
||||||
items: [{ label: 'Activity', key: MENU.ACTIVITY, icon: 'square-mouse-pointer' }],
|
items: [{ label: 'Activity', key: MENU.ACTIVITY, icon: 'square-mouse-pointer' }, { label: 'Users and Events', key: MENU.USERS_EVENTS, icon: 'square-mouse-pointer' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Product Optimization',
|
title: 'Product Optimization',
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,8 @@ export const highlights = (): string => '/highlights';
|
||||||
|
|
||||||
export const dataManagement = {
|
export const dataManagement = {
|
||||||
activity: () => '/data-management/activity',
|
activity: () => '/data-management/activity',
|
||||||
|
userPage: (id = ':userId', hash?: string | number) => hashed(`/data-management/user/${id}`, hash),
|
||||||
|
usersEvents: () => '/data-management/users-and-events',
|
||||||
}
|
}
|
||||||
|
|
||||||
const REQUIRED_SITE_ID_ROUTES = [
|
const REQUIRED_SITE_ID_ROUTES = [
|
||||||
|
|
@ -197,6 +199,8 @@ const REQUIRED_SITE_ID_ROUTES = [
|
||||||
highlights(),
|
highlights(),
|
||||||
|
|
||||||
dataManagement.activity(),
|
dataManagement.activity(),
|
||||||
|
dataManagement.userPage(''),
|
||||||
|
dataManagement.usersEvents(),
|
||||||
];
|
];
|
||||||
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 => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue