ui: start event analytics

This commit is contained in:
nick-delirium 2025-01-28 09:56:32 +01:00
parent 6360b9a580
commit 6097af4839
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
12 changed files with 330 additions and 35 deletions

View file

@ -37,6 +37,7 @@ const components: any = {
SpotPure: lazy(() => import('Components/Spots/SpotPlayer')),
ScopeSetup: lazy(() => import('Components/ScopeForm')),
HighlightsPure: lazy(() => import('Components/Highlights/HighlightsList')),
ActivityPure: lazy(() => import('Components/DataManagement/Activity/Page')),
};
const enhancedComponents: any = {
@ -60,6 +61,7 @@ const enhancedComponents: any = {
Spot: components.SpotPure,
ScopeSetup: components.ScopeSetup,
Highlights: components.HighlightsPure,
Activity: components.ActivityPure,
};
const withSiteId = routes.withSiteId;
@ -109,6 +111,10 @@ const SCOPE_SETUP = routes.scopeSetup();
const HIGHLIGHTS_PATH = routes.highlights();
const DATA_MANAGEMENT = {
ACTIVITY: routes.dataManagement.activity()
}
function PrivateRoutes() {
const { projectsStore, userStore, integrationsStore } = useStore();
const onboarding = userStore.onboarding;
@ -290,6 +296,12 @@ function PrivateRoutes() {
path={withSiteId(LIVE_SESSION_PATH, siteIdList)}
component={enhancedComponents.LiveSession}
/>
<Route
exact
strict
path={withSiteId(DATA_MANAGEMENT.ACTIVITY, siteIdList)}
component={enhancedComponents.Activity}
/>
{Object.entries(routes.redirects).map(([fr, to]) => (
<Redirect key={fr} exact strict from={fr} to={to} />
))}

View file

@ -0,0 +1,182 @@
import React from 'react';
import { EventsList, FilterList } from 'Shared/Filters/FilterList';
import { Table, Dropdown } from 'antd';
import { MoreOutlined } from '@ant-design/icons';
import { numberWithCommas } from 'App/utils';
import { Pagination } from 'UI';
import Event from './data/Event';
function ActivityPage() {
const [hiddenCols, setHiddenCols] = React.useState([]);
const appliedFilter = { filters: [] };
const onAddFilter = () => {};
const onUpdateFilter = () => {};
const onRemoveFilter = () => {};
const onChangeEventsOrder = () => {};
const saveRequestPayloads = () => {};
const onFilterMove = () => {};
const [editCols, setEditCols] = React.useState(false);
const dropdownItems = [
{
label: 'Show/Hide Columns',
key: 'edit-columns',
onClick: () => setEditCols(true),
}
]
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'}>
{row.$_isAutoCapture && (
<span className={'text-gray-500'}>[auto]</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) => <div className={'link'}>{text}</div>,
},
{
title: 'City',
dataIndex: 'userCity',
key: 'userCity',
showSorterTooltip: { target: 'full-header' },
sorter: (a, b) => a.userCiry.localeCompare(b.userCity),
},
{
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'}>
<div className={'cursor-pointer'}>
<MoreOutlined />
</div>
</Dropdown>
),
dataIndex: '$__opts__$',
key: '$__opts__$',
width: 50,
},
];
const shownCols = columns.map((col) => ({
...col,
hidden: hiddenCols.includes(col.key),
}));
const page = 1;
const limit = 100;
const total = 3000;
const testEv = new Event(
'test ev',
Date.now(),
{ userId: '123', userCity: 'NY', userEnvironment: 'Mac OS' },
{},
false
);
const testAutoEv = new Event(
'test auto ev',
Date.now(),
{ userId: '123', userCity: 'NY', userEnvironment: 'Mac OS' },
{},
true
);
const list = [testEv.toData(), testAutoEv.toData()];
const onPageChange = () => {};
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={
'bg-white rounded-xl shadow border flex flex-col overflow-hidden'
}
>
<div className={'px-4 py-2 font-semibold text-lg'}>
All users activity
</div>
<Table dataSource={list} pagination={false} columns={shownCols} />
<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>
{' events.'}
</div>
<Pagination
page={page}
total={total}
onPageChange={onPageChange}
limit={limit}
debounceRequest={500}
/>
</div>
</div>
</div>
);
}
export default ActivityPage;

View file

@ -0,0 +1,48 @@
interface DefaultFields {
userId: string;
userCity: string;
userEnvironment: string;
}
export default class Event {
name: string;
time: string;
defaultFields: DefaultFields = {
userId: '',
userCity: '',
userEnvironment: '',
}
customFields?: Record<string,any> = undefined;
readonly $_isAutoCapture;
constructor(name: string, time: string, defaultFields: DefaultFields, customFields?: Record<string, any>, isAutoCapture = false) {
this.name = name;
this.time = time;
this.defaultFields = defaultFields;
this.customFields = customFields;
this.$_isAutoCapture = isAutoCapture;
}
toJSON() {
const obj = this.toData();
return JSON.stringify(obj, 4);
}
toData() {
const obj: any = {
name: this.name,
time: this.time,
$_isAutoCapture: this.$_isAutoCapture,
}
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

@ -53,7 +53,7 @@ function FunnelTable(props: Props) {
if (props.compData) {
tableData.push({
conversion: props.compData.funnel.totalConversionsPercentage,
})
});
const compFunnel = props.compData.funnel;
compFunnel.stages.forEach((st, ind) => {
tableData[1]['st_' + ind] = st.count;
@ -71,9 +71,7 @@ function FunnelTable(props: Props) {
pagination={false}
size={'middle'}
scroll={{ x: 'max-content' }}
rowClassName={(_, index) => (
index > 0 ? 'opacity-70' : ''
)}
rowClassName={(_, index) => (index > 0 ? 'opacity-70' : '')}
/>
<TableExporter
tableColumns={tableProps}
@ -101,7 +99,6 @@ export function TableExporter({
}) {
const onClick = () => exportAntCsv(tableColumns, tableData, filename);
return (
<Tooltip title='Export Data to CSV'>
<div
className={`absolute ${top ? top : 'top-0'} ${
right ? right : '-right-1'
@ -111,17 +108,16 @@ export function TableExporter({
items={[{ icon: 'download', text: 'Export to CSV', onClick }]}
bold
customTrigger={
<div
className={
'flex items-center justify-center bg-gradient-to-r from-[#fafafa] to-neutral-200 cursor-pointer rounded-lg h-[38px] w-[38px] btn-export-table-data'
}
>
<div
className={
'flex items-center justify-center bg-gradient-to-r from-[#fafafa] to-neutral-200 cursor-pointer rounded-lg h-[38px] w-[38px] btn-export-table-data'
}
>
<EllipsisVertical size={16} />
</div>
}
/>
</div>
</Tooltip>
);
}

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 React, { useEffect } from 'react';
import { Button } from 'antd';
@ -27,6 +27,8 @@ interface Props {
mergeUp?: boolean;
borderless?: boolean;
cannotAdd?: boolean;
heading?: React.ReactNode;
isLive?: boolean;
}
export const FilterList = observer((props: Props) => {
@ -39,7 +41,8 @@ export const FilterList = observer((props: Props) => {
onAddFilter,
readonly,
borderless,
excludeCategory
excludeCategory,
isLive
} = props;
const filters = filter.filters;
@ -70,6 +73,7 @@ export const FilterList = observer((props: Props) => {
disabled={readonly}
excludeFilterKeys={excludeFilterKeys}
excludeCategory={excludeCategory}
isLive={isLive}
>
<Button
icon={<Filter size={16} strokeWidth={1} />}
@ -97,6 +101,7 @@ export const FilterList = observer((props: Props) => {
isFilter={true}
filterIndex={filterIndex}
filter={filter}
isLive={isLive}
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
onRemoveFilter={() => onRemoveFilter(filterIndex)}
excludeFilterKeys={excludeFilterKeys}
@ -213,9 +218,10 @@ export const EventsList = observer((props: Props) => {
marginBottom: props.mergeDown ? '-1px' : undefined
}}
>
{props.heading ? props.heading : null}
<div className="flex items-center mb-2 gap-2">
<div className="font-medium">Events</div>
{cannotAdd ? null : (
{filters.length === 0 || cannotAdd ? null : (
<FilterSelection
mode={'events'}
filter={undefined}
@ -224,12 +230,12 @@ export const EventsList = observer((props: Props) => {
excludeCategory={excludeCategory}
>
<Button
icon={<Plus size={16} strokeWidth={1} />}
icon={<SquareDashedMousePointer size={16} strokeWidth={1} />}
type="default"
size={'small'}
className="btn-add-event"
>
Add
Select Event
</Button>
</FilterSelection>
)}
@ -243,6 +249,28 @@ export const EventsList = observer((props: Props) => {
</div>
</div>
<div className={'flex flex-col '}>
{filters.length === 0 ? (
<div className={'flex items-center gap-2 mb-2'}>
<div className={'bg-gray-lighter rounded-full leading-none flex items-center justify-center w-5 h-5'}>
<div>1</div>
</div>
<FilterSelection
mode={'events'}
filter={undefined}
onFilterClick={onAddFilter}
excludeCategory={excludeCategory}
>
<Button
icon={<SquareDashedMousePointer size={16} strokeWidth={1} />}
type="default"
size={'small'}
className='btn-add-event'
>
Select Event
</Button>
</FilterSelection>
</div>
) : null}
{filters.map((filter: any, filterIndex: number) =>
filter.isEvent ? (
<div

View file

@ -12,7 +12,7 @@ interface Props {
function Square_mouse_pointer(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6" fill="none"/></svg>
);
}

View file

@ -152,6 +152,7 @@ function SideMenu(props: Props) {
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
[MENU.HIGHLIGHTS]: () => withSiteId(routes.highlights(''), siteId),
[MENU.ACTIVITY]: () => withSiteId(routes.dataManagement.activity(), siteId),
};
const handleClick = (item: any) => {

View file

@ -53,6 +53,7 @@ export const enum MENU {
SUPPORT = 'support',
EXIT = 'exit',
SPOTS = 'spots',
ACTIVITY = 'activity',
}
export const categories: Category[] = [
@ -61,26 +62,27 @@ export const categories: Category[] = [
key: 'replays',
items: [
{ label: 'Sessions', key: MENU.SESSIONS, icon: 'collection-play' },
{ label: 'Recommendations', key: MENU.RECOMMENDATIONS, icon: 'magic', hidden: true },
{
label: 'Recommendations',
key: MENU.RECOMMENDATIONS,
icon: 'magic',
hidden: true,
},
{ label: 'Vault', key: MENU.VAULT, icon: 'safe', hidden: true },
{ label: 'Bookmarks', key: MENU.BOOKMARKS, icon: 'bookmark' },
//{ label: 'Notes', key: MENU.NOTES, icon: 'stickies' },
{ label: 'Highlights', key: MENU.HIGHLIGHTS, icon: 'chat-square-quote' }
]
{ label: 'Highlights', key: MENU.HIGHLIGHTS, icon: 'chat-square-quote' },
],
},
{
title: '',
key: 'spot',
items: [
{ label: 'Spots', key: MENU.SPOTS, icon: 'orspotOutline' },
]
items: [{ label: 'Spots', key: MENU.SPOTS, icon: 'orspotOutline' }],
},
{
title: '',
key: 'assist',
items: [
{ label: 'Co-Browse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
]
items: [{ label: 'Co-Browse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' }],
},
{
title: 'Analytics',
@ -96,25 +98,39 @@ export const categories: Category[] = [
// { label: 'Resource Monitoring', key: MENU.RESOURCE_MONITORING }
// ]
// },
{ label: 'Alerts', key: MENU.ALERTS, icon: 'bell' }
]
{ label: 'Alerts', key: MENU.ALERTS, icon: 'bell' },
],
},
{
title: 'Data Management',
key: 'data-management',
items: [{ label: 'Activity', key: MENU.ACTIVITY, icon: 'square-mouse-pointer' }],
},
{
title: 'Product Optimization',
key: 'product-optimization',
items: [
{ label: 'Feature Flags', key: MENU.FEATURE_FLAGS, icon: 'toggles' },
{ label: 'Usability Tests', key: MENU.USABILITY_TESTS, icon: 'clipboard-check' },
]
{
label: 'Usability Tests',
key: MENU.USABILITY_TESTS,
icon: 'clipboard-check',
},
],
},
{
title: '',
key: 'other',
items: [
{ label: 'Preferences', key: MENU.PREFERENCES, icon: 'sliders', leading: 'chevron-right' },
{ label: 'Support', key: MENU.SUPPORT, icon: 'question-circle' }
]
}
{
label: 'Preferences',
key: MENU.PREFERENCES,
icon: 'sliders',
leading: 'chevron-right',
},
{ label: 'Support', key: MENU.SUPPORT, icon: 'question-circle' },
],
},
];
export const preferences: Category[] = [

View file

@ -148,6 +148,10 @@ export const scopeSetup = (): string => '/scope-setup';
export const highlights = (): string => '/highlights';
export const dataManagement = {
activity: () => '/data-management/activity',
}
const REQUIRED_SITE_ID_ROUTES = [
liveSession(''),
session(''),

View file

@ -403,4 +403,8 @@ svg {
background-color: #c6c6c6;
border-radius: 4px;
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="evenodd" fillRule="evenodd"'
)
.replaceAll(`stroke="no-fill"`, 'fill="none"')
.replaceAll(/fill-rule/g, 'fillRule')
.replaceAll(/fill-opacity/g, 'fillOpacity')
.replaceAll(/stop-color/g, 'stopColor')