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')),
|
||||
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')),
|
||||
};
|
||||
|
||||
const enhancedComponents: any = {
|
||||
|
|
@ -62,6 +64,8 @@ const enhancedComponents: any = {
|
|||
ScopeSetup: components.ScopeSetup,
|
||||
Highlights: components.HighlightsPure,
|
||||
Activity: components.ActivityPure,
|
||||
UserPage: components.UserPage,
|
||||
UsersEventsPage: components.UsersEventsPage,
|
||||
};
|
||||
|
||||
const withSiteId = routes.withSiteId;
|
||||
|
|
@ -112,7 +116,9 @@ const SCOPE_SETUP = routes.scopeSetup();
|
|||
const HIGHLIGHTS_PATH = routes.highlights();
|
||||
|
||||
const DATA_MANAGEMENT = {
|
||||
ACTIVITY: routes.dataManagement.activity()
|
||||
ACTIVITY: routes.dataManagement.activity(),
|
||||
USER_PAGE: routes.dataManagement.userPage(),
|
||||
USERS_EVENTS: routes.dataManagement.usersEvents(),
|
||||
}
|
||||
|
||||
function PrivateRoutes() {
|
||||
|
|
@ -302,6 +308,18 @@ function PrivateRoutes() {
|
|||
path={withSiteId(DATA_MANAGEMENT.ACTIVITY, siteIdList)}
|
||||
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]) => (
|
||||
<Redirect key={fr} exact strict from={fr} to={to} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ function EventDetailsModal({ ev, onClose }: { ev: EventData, onClose: () => void
|
|||
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-lg'}>Event</div>
|
||||
<div className={'font-semibold text-xl'}>Event</div>
|
||||
<div className={'p-2 cursor-pointer'} onClick={onClose}>
|
||||
<X size={16} />
|
||||
</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 (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -11,45 +11,52 @@ 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';
|
||||
|
||||
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 (
|
||||
page: number
|
||||
): Promise<{ list: any[]; total: number }> => {
|
||||
const total = 3000;
|
||||
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 });
|
||||
});
|
||||
};
|
||||
|
||||
function ActivityPage() {
|
||||
const { projectsStore } = useStore()
|
||||
const siteId = projectsStore.activeSiteId;
|
||||
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [hiddenCols, setHiddenCols] = React.useState([]);
|
||||
const { data, isPending } = useQuery({
|
||||
|
|
@ -106,22 +113,23 @@ function ActivityPage() {
|
|||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.userId.localeCompare(b.userId),
|
||||
render: (text) => (
|
||||
<div
|
||||
<Link
|
||||
to={withSiteId(dataManagement.userPage(text), siteId)}
|
||||
className={'link'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'City',
|
||||
dataIndex: 'userCity',
|
||||
key: 'userCity',
|
||||
dataIndex: 'userLocation',
|
||||
key: 'userLocation',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
sorter: (a, b) => a.userCity.localeCompare(b.userCity),
|
||||
sorter: (a, b) => a.userLocation.localeCompare(b.userLocation),
|
||||
},
|
||||
{
|
||||
title: 'Environment',
|
||||
|
|
@ -234,23 +242,23 @@ function ActivityPage() {
|
|||
options={[
|
||||
{ label: 'Past 24 Hours', value: 'DESC' },
|
||||
{ label: 'Weekly', value: 'ASC' },
|
||||
{ label: 'Other', value: 'Stuff' }
|
||||
{ label: 'Other', value: 'Stuff' },
|
||||
]}
|
||||
defaultValue={'DESC'}
|
||||
plain
|
||||
onChange={({ value }) => {
|
||||
console.log(value)
|
||||
console.log(value);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'DESC' },
|
||||
{ label: 'Oldest', value: 'ASC' }
|
||||
{ label: 'Oldest', value: 'ASC' },
|
||||
]}
|
||||
defaultValue={'DESC'}
|
||||
plain
|
||||
onChange={({ value }) => {
|
||||
console.log(value)
|
||||
console.log(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -289,4 +297,4 @@ function ActivityPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export default ActivityPage;
|
||||
export default observer(ActivityPage);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ interface DefaultFields {
|
|||
|
||||
export interface EventData {
|
||||
name: string;
|
||||
time: string;
|
||||
time: number;
|
||||
$_isAutoCapture: boolean;
|
||||
$_defaultFields: DefaultFields;
|
||||
$_customFields?: Record<string, any>;
|
||||
|
|
@ -14,10 +14,10 @@ export interface EventData {
|
|||
|
||||
export default class Event {
|
||||
name: string;
|
||||
time: string;
|
||||
time: number;
|
||||
defaultFields: DefaultFields = {
|
||||
userId: '',
|
||||
userCity: '',
|
||||
userLocation: '',
|
||||
userEnvironment: '',
|
||||
}
|
||||
customFields?: Record<string,any> = undefined;
|
||||
|
|
@ -35,7 +35,7 @@ export default class Event {
|
|||
isAutoCapture,
|
||||
}: {
|
||||
name: string;
|
||||
time: string;
|
||||
time: number;
|
||||
defaultFields: DefaultFields;
|
||||
customFields?: Record<string, any>;
|
||||
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 { Icon } from 'UI';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { withSiteId } from "App/routes";
|
||||
|
||||
interface Props {
|
||||
items: any;
|
||||
|
|
@ -8,6 +11,9 @@ interface Props {
|
|||
|
||||
function Breadcrumb(props: Props) {
|
||||
const { items } = props;
|
||||
const { projectsStore } = useStore();
|
||||
const siteId = projectsStore.activeSiteId;
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex items-center text-lg">
|
||||
{items.map((item: any, index: any) => {
|
||||
|
|
@ -28,7 +34,7 @@ function Breadcrumb(props: Props) {
|
|||
}
|
||||
return (
|
||||
<div key={index} className="color-gray-darkest hover:text-teal group flex items-center">
|
||||
<Link to={item.to} className="flex items-center default-hover">
|
||||
<Link to={item.withSiteId ? withSiteId(item.to, siteId) : item.to} className="flex items-center default-hover">
|
||||
{index === 0 && (
|
||||
<Icon name="chevron-left" size={16} className="mr-1 group-hover:fill-teal" />
|
||||
)}
|
||||
|
|
@ -42,4 +48,4 @@ function Breadcrumb(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default Breadcrumb;
|
||||
export default observer(Breadcrumb);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ export function getDateFromString(date: string, format = 'yyyy-MM-dd HH:mm:ss:SS
|
|||
return DateTime.fromISO(date).toFormat(format);
|
||||
}
|
||||
|
||||
export const formatTs = (ts: number, format: string): string => {
|
||||
return DateTime.fromMillis(ts).toFormat(format);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats a given duration.
|
||||
*
|
||||
|
|
@ -164,6 +169,9 @@ export const checkForRecent = (date: DateTime, format: string): string => {
|
|||
// Formatted
|
||||
return date.toFormat(format);
|
||||
};
|
||||
export const tsToCheckRecent = (ts: number, format: string): string => {
|
||||
return checkForRecent(DateTime.fromMillis(ts), format);
|
||||
}
|
||||
export const resentOrDate = (ts, short?: boolean) => {
|
||||
const date = DateTime.fromMillis(ts);
|
||||
const d = new Date();
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ function SideMenu(props: Props) {
|
|||
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
|
||||
[MENU.HIGHLIGHTS]: () => withSiteId(routes.highlights(''), siteId),
|
||||
[MENU.ACTIVITY]: () => withSiteId(routes.dataManagement.activity(), siteId),
|
||||
[MENU.USERS_EVENTS]: () => withSiteId(routes.dataManagement.usersEvents(), siteId),
|
||||
};
|
||||
|
||||
const handleClick = (item: any) => {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export const enum MENU {
|
|||
EXIT = 'exit',
|
||||
SPOTS = 'spots',
|
||||
ACTIVITY = 'activity',
|
||||
USER_PAGE = 'user-page',
|
||||
USERS_EVENTS = 'users-events',
|
||||
}
|
||||
|
||||
export const categories: Category[] = [
|
||||
|
|
@ -104,7 +106,7 @@ export const categories: Category[] = [
|
|||
{
|
||||
title: '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',
|
||||
|
|
|
|||
|
|
@ -149,6 +149,8 @@ export const highlights = (): string => '/highlights';
|
|||
|
||||
export const dataManagement = {
|
||||
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 = [
|
||||
|
|
@ -197,6 +199,8 @@ const REQUIRED_SITE_ID_ROUTES = [
|
|||
highlights(),
|
||||
|
||||
dataManagement.activity(),
|
||||
dataManagement.userPage(''),
|
||||
dataManagement.usersEvents(),
|
||||
];
|
||||
const routeNeedsSiteId = (path: string): boolean => REQUIRED_SITE_ID_ROUTES.some(r => path.startsWith(r));
|
||||
const siteIdToUrl = (siteId = ':siteId'): string => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue