diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index aba0340f0..6ed5c201d 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -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} /> + + {Object.entries(routes.redirects).map(([fr, to]) => ( ))} diff --git a/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx b/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx index c2331842a..50e183070 100644 --- a/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx +++ b/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx @@ -60,7 +60,7 @@ function EventDetailsModal({ ev, onClose }: { ev: EventData, onClose: () => void return (
-
Event
+
Event
@@ -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 ( => { 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) => ( -
{ e.stopPropagation(); }} > {text} -
+ ), }, { 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); }} /> { + console.log(value); + }} + /> +
+
+ +
+
+ ); +} + +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: , + onClick: () => console.log('confirm'), + }, + ]; + + const showAll = () => { + showModal(, { + width: 420, + right: true, + }); + }; + + return ( + <> + + +
+
+
+
+ {testUser.name.slice(0, 2)} +
+
+
{testUser.name}
+
{testUser.userId}
+
+
+
+
Distinct ID
+
+ {testUser.distinctId[0]} + {testUser.distinctId.length > 1 && ( + + Tracking IDs linked to this user +
+ } + trigger={'click'} + placement={'bottom'} + arrow={false} + content={ +
+ {testUser.distinctId.map((id) => ( +
+ {id} +
copy(id)} + > + +
+
+ ))} +
+ } + > +
+ +{testUser.distinctId.length - 1} +
+ + )} +
+
+
+
Location
+
{testUser.userLocation}
+
+
+
+ +{Object.keys(testUser.properties).length} properties +
+ +
+ +
+
+
+
+
+ +
Cohorts
+ {testUser.cohorts.map((cohort) => ( + {cohort} + ))} +
+ + + ); +} + +export default UserPage; diff --git a/frontend/app/components/DataManagement/UsersEvents/components/EventsByDay.tsx b/frontend/app/components/DataManagement/UsersEvents/components/EventsByDay.tsx new file mode 100644 index 000000000..77db59e26 --- /dev/null +++ b/frontend/app/components/DataManagement/UsersEvents/components/EventsByDay.tsx @@ -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; + onItemClick: (ev: any) => void; +}) { + return ( + <> + {Object.keys(byDays).map((date) => ( +
+
+ + {date} +
+ {byDays[date].map((ev) => ( +
onItemClick(ev)} + className={ + 'hover:bg-gray-lightest border-b cursor-pointer px-4 py-2 flex items-center group' + } + > +
{formatTs(ev.time, 'HH:mm:ss a')}
+
{ev.name}
+
+ +
+
+ ))} +
+ ))} + + ); +} + +export default EventsByDay; diff --git a/frontend/app/components/DataManagement/UsersEvents/components/Tag.tsx b/frontend/app/components/DataManagement/UsersEvents/components/Tag.tsx new file mode 100644 index 000000000..080640564 --- /dev/null +++ b/frontend/app/components/DataManagement/UsersEvents/components/Tag.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function Tag({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +export default Tag; diff --git a/frontend/app/components/DataManagement/UsersEvents/components/UserPropertiesModal.tsx b/frontend/app/components/DataManagement/UsersEvents/components/UserPropertiesModal.tsx new file mode 100644 index 000000000..c9afc493c --- /dev/null +++ b/frontend/app/components/DataManagement/UsersEvents/components/UserPropertiesModal.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Input, Button } from 'antd' +import { Pencil } from 'lucide-react' + +function UserPropertiesModal({ + properties, +}: { + properties: Record +}) { + return ( +
+
All User Properties
+ + {Object.entries(properties).map(([key, value]) => ( + + ))} +
+ ) +} + +function Property({ pkey, value, onSave }: { + pkey: string, + value: string, + onSave?: (key: string, value: string) => void +}) { + const [isEdit, setIsEdit] = React.useState(false) + + return ( +
+
{pkey}
+ {isEdit ? ( +
+ +
+ + +
+
+ ) : ( +
+ {value} +
setIsEdit(true)}> + +
+
+ )} +
+ ) +} + +export default UserPropertiesModal; \ No newline at end of file diff --git a/frontend/app/components/DataManagement/UsersEvents/data/User.ts b/frontend/app/components/DataManagement/UsersEvents/data/User.ts new file mode 100644 index 000000000..15c6afc2b --- /dev/null +++ b/frontend/app/components/DataManagement/UsersEvents/data/User.ts @@ -0,0 +1,35 @@ +export default class User { + name: string; + userId: string; + distinctId: string[]; + userLocation: string; + cohorts: string[]; + properties: Record; + updatedAt: number; + + constructor({ + name, + userId, + distinctId, + userLocation, + cohorts, + properties, + updatedAt + }: { + name: string; + userId: string; + distinctId: string[]; + userLocation: string; + cohorts: string[]; + properties: Record; + updatedAt: number; + }) { + this.name = name; + this.userId = userId; + this.distinctId = distinctId; + this.userLocation = userLocation; + this.cohorts = cohorts; + this.properties = properties; + this.updatedAt = updatedAt; + } +} diff --git a/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx b/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx index 9307f1020..72ad01735 100644 --- a/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx +++ b/frontend/app/components/shared/Breadcrumb/Breadcrumb.tsx @@ -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 (
{items.map((item: any, index: any) => { @@ -28,7 +34,7 @@ function Breadcrumb(props: Props) { } return (
- + {index === 0 && ( )} @@ -42,4 +48,4 @@ function Breadcrumb(props: Props) { ); } -export default Breadcrumb; +export default observer(Breadcrumb); diff --git a/frontend/app/date.ts b/frontend/app/date.ts index e9a5982ec..dd9cd3165 100644 --- a/frontend/app/date.ts +++ b/frontend/app/date.ts @@ -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(); diff --git a/frontend/app/layout/SideMenu.tsx b/frontend/app/layout/SideMenu.tsx index 750046087..9b7f7384c 100644 --- a/frontend/app/layout/SideMenu.tsx +++ b/frontend/app/layout/SideMenu.tsx @@ -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) => { diff --git a/frontend/app/layout/data.ts b/frontend/app/layout/data.ts index fe7d9a7ae..5c626cb61 100644 --- a/frontend/app/layout/data.ts +++ b/frontend/app/layout/data.ts @@ -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', diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts index 25eb70c14..f867a4c34 100644 --- a/frontend/app/routes.ts +++ b/frontend/app/routes.ts @@ -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 => {