diff --git a/apps/api/src/controllers/live.controller.ts b/apps/api/src/controllers/live.controller.ts index 6847bfe7..9a08cc01 100644 --- a/apps/api/src/controllers/live.controller.ts +++ b/apps/api/src/controllers/live.controller.ts @@ -9,6 +9,7 @@ import { getEvents, getLiveVisitors, getProfileById, + getProfileByIdCached, TABLE_NAMES, transformMinimalEvent, } from '@openpanel/db'; @@ -144,7 +145,10 @@ export async function wsProjectEvents( const message = async (channel: string, message: string) => { const event = getSuperJson(message); if (event?.projectId === params.projectId) { - const profile = await getProfileById(event.profileId, event.projectId); + const profile = await getProfileByIdCached( + event.profileId, + event.projectId + ); connection.socket.send( superjson.stringify( access diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 4213005f..884d5905 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -64,6 +64,7 @@ "embla-carousel-react": "8.0.0-rc22", "flag-icons": "^7.1.0", "framer-motion": "^11.0.28", + "geist": "^1.3.1", "hamburger-react": "^2.5.0", "input-otp": "^1.2.4", "javascript-time-ago": "^2.5.9", diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx index 7a874917..caed65bb 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx @@ -31,15 +31,16 @@ import { getDefaultIntervalByRange, timeWindows, } from '@openpanel/constants'; -import type { getReportsByDashboardId } from '@openpanel/db'; +import type { getReportsByDashboardId, IServiceDashboard } from '@openpanel/db'; import { OverviewReportRange } from '../../overview-sticky-header'; interface ListReportsProps { reports: Awaited>; + dashboard: IServiceDashboard; } -export function ListReports({ reports }: ListReportsProps) { +export function ListReports({ reports, dashboard }: ListReportsProps) { const router = useRouter(); const params = useAppParams<{ dashboardId: string }>(); const { range, startDate, endDate } = useOverviewOptions(); @@ -52,25 +53,28 @@ export function ListReports({ reports }: ListReportsProps) { }); return ( <> - - - - -
+
+

{dashboard.name}

+
+ + +
+
+
{reports.map((report) => { const chartRange = report.range; return ( @@ -83,7 +87,7 @@ export function ListReports({ reports }: ListReportsProps) {
{report.name}
{chartRange !== null && ( -
+
- - - + + + ); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx index e39bf721..9ddc769a 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx @@ -4,23 +4,19 @@ import { Button } from '@/components/ui/button'; import { pushModal } from '@/modals'; import { PlusIcon } from 'lucide-react'; -import { StickyBelowHeader } from '../../layout-sticky-below-header'; - export function HeaderDashboards() { return ( - -
-
- -
- +
+

Dashboards

+ +
); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx index 9456a94d..07e24066 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx @@ -1,8 +1,6 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; import FullPageLoadingState from '@/components/full-page-loading-state'; +import { Padding } from '@/components/ui/padding'; import withSuspense from '@/hocs/with-suspense'; -import type { LucideIcon } from 'lucide-react'; -import { Loader2Icon } from 'lucide-react'; import { getDashboardsByProjectId } from '@openpanel/db'; @@ -16,7 +14,12 @@ interface Props { const ListDashboardsServer = async ({ projectId }: Props) => { const dashboards = await getDashboardsByProjectId(projectId); - return ; + return ( + + + ; + + ); }; export default withSuspense(ListDashboardsServer, FullPageLoadingState); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/list-dashboards.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/list-dashboards.tsx index 999bd125..c9d04503 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/list-dashboards.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/list-dashboards.tsx @@ -75,7 +75,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) { return ( <> -
+
{dashboards.map((item) => { const visibleReports = item.reports.slice( 0, @@ -88,16 +88,16 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) { href={`/${organizationSlug}/${projectId}/dashboards/${item.id}`} className="flex flex-col p-4 @container" > -
+
{item.name}
-
+
{format(item.updatedAt, 'HH:mm · MMM d')}
{visibleReports.map((report) => { @@ -114,26 +114,26 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) { return (
- -
+ +
{report.name}
); })} {item.reports.length > 6 && ( -
- -
+
+ +
{item.reports.length - 5} more
)}
- {/* + {/* {item.reports.length} reports diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx index d169e82c..8787f9c2 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx @@ -1,23 +1,11 @@ -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; - import ListDashboardsServer from './list-dashboards'; -import { HeaderDashboards } from './list-dashboards/header'; interface PageProps { params: { projectId: string; - organizationSlug: string; }; } -export default function Page({ - params: { projectId, organizationSlug }, -}: PageProps) { - return ( - <> - - - - - ); +export default function Page({ params: { projectId } }: PageProps) { + return ; } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts.tsx new file mode 100644 index 00000000..7d89772e --- /dev/null +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts.tsx @@ -0,0 +1,181 @@ +'use client'; + +import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; +import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; +import { ChartRootShortcut } from '@/components/report/chart'; +import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; +import { + useEventQueryFilters, + useEventQueryNamesFilter, +} from '@/hooks/useEventQueryFilters'; + +import type { IChartEvent } from '@openpanel/validation'; + +interface Props { + projectId: string; +} + +function Charts({ projectId }: Props) { + const [filters] = useEventQueryFilters(); + const [events] = useEventQueryNamesFilter(); + const fallback: IChartEvent[] = [ + { + id: 'A', + name: '*', + displayName: 'All events', + segment: 'event', + filters: filters ?? [], + }, + ]; + + return ( +
+
+ + +
+
+ + + Events per day + + + 0 + ? events.map((name) => ({ + id: name, + name, + displayName: name, + segment: 'event', + filters: filters ?? [], + })) + : fallback + } + /> + + + + + Event distribution + + + 0 + ? events.map((name) => ({ + id: name, + name, + displayName: name, + segment: 'event', + filters: filters ?? [], + })) + : [ + { + id: 'A', + name: '*', + displayName: 'All events', + segment: 'event', + filters: filters ?? [], + }, + ] + } + /> + + + + + Event distribution + + + 0 + ? events.map((name) => ({ + id: name, + name, + displayName: name, + segment: 'event', + filters: filters ?? [], + })) + : [ + { + id: 'A', + name: '*', + displayName: 'All events', + segment: 'event', + filters: filters ?? [], + }, + ] + } + /> + + + + + Event distribution + + + 0 + ? events.map((name) => ({ + id: name, + name, + displayName: name, + segment: 'event', + filters: filters ?? [], + })) + : [ + { + id: 'A', + name: '*', + displayName: 'All events', + segment: 'event', + filters: filters ?? [], + }, + ] + } + /> + + +
+
+ ); +} + +export default Charts; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts/events-per-day-chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts/events-per-day-chart.tsx deleted file mode 100644 index 6834e054..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts/events-per-day-chart.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ChartRootShortcut } from '@/components/report/chart'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; - -import type { IChartEvent } from '@openpanel/validation'; - -interface Props { - projectId: string; - events?: string[]; - filters?: any[]; -} - -export function EventsPerDayChart({ projectId, filters, events }: Props) { - const fallback: IChartEvent[] = [ - { - id: 'A', - name: '*', - displayName: 'All events', - segment: 'event', - filters: filters ?? [], - }, - ]; - - return ( - - - Events per day - - - 0 - ? events.map((name) => ({ - id: name, - name, - displayName: name, - segment: 'event', - filters: filters ?? [], - })) - : fallback - } - /> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx new file mode 100644 index 00000000..67ac16e4 --- /dev/null +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { EventsTable } from '@/components/events/table'; +import { api } from '@/trpc/client'; + +type Props = { + projectId: string; + profileId?: string; +}; + +const Conversions = ({ projectId }: Props) => { + const query = api.event.conversions.useQuery( + { + projectId, + }, + { + keepPreviousData: true, + } + ); + + return ; +}; + +export default Conversions; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-conversions-list/event-conversions-list.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-conversions-list/event-conversions-list.tsx deleted file mode 100644 index 34ec078a..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-conversions-list/event-conversions-list.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { Fragment } from 'react'; -import { Widget, WidgetHead } from '@/components/widget'; -import { isSameDay } from 'date-fns'; - -import type { IServiceEvent } from '@openpanel/db'; - -import { EventListItem } from '../event-list/event-list-item'; - -function showDateHeader(a: Date, b?: Date) { - if (!b) return true; - return !isSameDay(a, b); -} - -interface EventListProps { - data: IServiceEvent[]; -} -export function EventConversionsList({ data }: EventListProps) { - return ( - - -
Conversions
-
-
- {data.map((item, index, list) => ( - - {showDateHeader(item.createdAt, list[index - 1]?.createdAt) && ( -
-
-
- {item.createdAt.toLocaleDateString()} -
-
-
- )} - -
- ))} -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-conversions-list/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-conversions-list/index.tsx deleted file mode 100644 index fb204124..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-conversions-list/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { escape } from 'sqlstring'; - -import { db, getEvents, TABLE_NAMES } from '@openpanel/db'; - -import { EventConversionsList } from './event-conversions-list'; - -interface Props { - projectId: string; -} - -async function EventConversionsListServer({ projectId }: Props) { - const conversions = await db.eventMeta.findMany({ - where: { - projectId, - conversion: true, - }, - }); - - if (conversions.length === 0) { - return null; - } - - const events = await getEvents( - `SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND name IN (${conversions.map((c) => escape(c.name)).join(', ')}) ORDER BY created_at DESC LIMIT 20;`, - { - profile: true, - meta: true, - } - ); - - return ; -} - -export default withLoadingWidget(EventConversionsListServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-details.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-details.tsx deleted file mode 100644 index 2d541d66..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-details.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { useState } from 'react'; -import type { Dispatch, SetStateAction } from 'react'; -import { ChartRootShortcut } from '@/components/report/chart'; -import { Button } from '@/components/ui/button'; -import { KeyValue } from '@/components/ui/key-value'; -import { - Sheet, - SheetContent, - SheetFooter, - SheetHeader, - SheetTitle, -} from '@/components/ui/sheet'; -import { - useEventQueryFilters, - useEventQueryNamesFilter, -} from '@/hooks/useEventQueryFilters'; -import { round } from 'mathjs'; - -import type { IServiceEvent } from '@openpanel/db'; - -import { EventEdit } from './event-edit'; - -interface Props { - event: IServiceEvent; - open: boolean; - setOpen: Dispatch>; -} - -export function EventDetails({ event, open, setOpen }: Props) { - const { name } = event; - const [isEditOpen, setIsEditOpen] = useState(false); - const [, setFilter] = useEventQueryFilters({ shallow: false }); - const [, setEvents] = useEventQueryNamesFilter({ shallow: false }); - - const common = [ - { - name: 'Origin', - value: event.origin, - }, - { - name: 'Duration', - value: event.duration ? round(event.duration / 1000, 1) : undefined, - }, - { - name: 'Referrer', - value: event.referrer, - onClick() { - setFilter('referrer', event.referrer ?? ''); - }, - }, - { - name: 'Referrer name', - value: event.referrerName, - onClick() { - setFilter('referrer_name', event.referrerName ?? ''); - }, - }, - { - name: 'Referrer type', - value: event.referrerType, - onClick() { - setFilter('referrer_type', event.referrerType ?? ''); - }, - }, - { - name: 'Brand', - value: event.brand, - onClick() { - setFilter('brand', event.brand ?? ''); - }, - }, - { - name: 'Model', - value: event.model, - onClick() { - setFilter('model', event.model ?? ''); - }, - }, - { - name: 'Browser', - value: event.browser, - onClick() { - setFilter('browser', event.browser ?? ''); - }, - }, - { - name: 'Browser version', - value: event.browserVersion, - onClick() { - setFilter('browser_version', event.browserVersion ?? ''); - }, - }, - { - name: 'OS', - value: event.os, - onClick() { - setFilter('os', event.os ?? ''); - }, - }, - { - name: 'OS version', - value: event.osVersion, - onClick() { - setFilter('os_version', event.osVersion ?? ''); - }, - }, - { - name: 'City', - value: event.city, - onClick() { - setFilter('city', event.city ?? ''); - }, - }, - { - name: 'Region', - value: event.region, - onClick() { - setFilter('region', event.region ?? ''); - }, - }, - { - name: 'Country', - value: event.country, - onClick() { - setFilter('country', event.country ?? ''); - }, - }, - { - name: 'Device', - value: event.device, - onClick() { - setFilter('device', event.device ?? ''); - }, - }, - ].filter((item) => typeof item.value === 'string' && item.value); - - const properties = Object.entries(event.properties) - .map(([name, value]) => ({ - name, - value: value as string | number | undefined, - })) - .filter((item) => typeof item.value === 'string' && item.value); - - return ( - <> - - -
-
- - {name.replace('_', ' ')} - - - {properties.length > 0 && ( -
-
Params
-
- {properties.map((item) => ( - { - setFilter( - `properties.${item.name}`, - item.value ? String(item.value) : '', - 'is' - ); - }} - /> - ))} -
-
- )} -
-
Common
-
- {common.map((item) => ( - item.onClick?.()} - /> - ))} -
-
- -
-
-
Similar events
- -
- -
-
-
- - - -
-
- - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-icon.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-icon.tsx index 0bfa9cda..d8220eee 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-icon.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-icon.tsx @@ -21,7 +21,6 @@ const variants = cva('flex shrink-0 items-center justify-center rounded-full', { type EventIconProps = VariantProps & { name: string; meta?: EventMeta; - projectId: string; className?: string; }; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-list-item.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-list-item.tsx index 05262267..6ed0db7c 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-list-item.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-list-item.tsx @@ -1,17 +1,16 @@ 'use client'; -import { useState } from 'react'; import { SerieIcon } from '@/components/report/chart/SerieIcon'; import { Tooltiper } from '@/components/ui/tooltip'; import { useAppParams } from '@/hooks/useAppParams'; import { useNumber } from '@/hooks/useNumerFormatter'; +import { pushModal } from '@/modals'; import { cn } from '@/utils/cn'; import { getProfileName } from '@/utils/getters'; import Link from 'next/link'; import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db'; -import { EventDetails } from './event-details'; import { EventIcon } from './event-icon'; type EventListItemProps = IServiceEventMinimal | IServiceEvent; @@ -20,7 +19,6 @@ export function EventListItem(props: EventListItemProps) { const { organizationSlug, projectId } = useAppParams(); const { createdAt, name, path, duration, meta } = props; const profile = 'profile' in props ? props.profile : null; - const [isDetailsOpen, setIsDetailsOpen] = useState(false); const number = useNumber(); @@ -52,17 +50,12 @@ export function EventListItem(props: EventListItemProps) { return ( <> - {!isMinimal && ( - - )} - - ) : ( - <> - {filters.length ? ( -

Could not find any events with your filter

- ) : ( -

We have not received any events yet

- )} - - )} - - ) : ( - <> -
- {data.map((item, index, list) => ( - - {showDateHeader(item.createdAt, list[index - 1]?.createdAt) && ( -
- {index === 0 ? :
} -
-
- {item.createdAt.toLocaleDateString()} -
- {index === 0 && ( - - )} -
-
- )} - - - ))} -
- - - )} - - ); -} - -export default EventList; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-listener.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-listener.tsx index feb0bd0a..02e59c48 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-listener.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-listener.tsx @@ -6,11 +6,10 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { useAppParams } from '@/hooks/useAppParams'; -import { useDebounceVal } from '@/hooks/useDebounceVal'; +import { useDebounceState } from '@/hooks/useDebounceState'; import useWS from '@/hooks/useWS'; import { cn } from '@/utils/cn'; import dynamic from 'next/dynamic'; -import { useRouter } from 'next/navigation'; import type { IServiceEventMinimal } from '@openpanel/db'; @@ -19,10 +18,13 @@ const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), { loading: () =>
0
, }); -export default function EventListener() { - const router = useRouter(); +export default function EventListener({ + onRefresh, +}: { + onRefresh: () => void; +}) { const { projectId } = useAppParams(); - const counter = useDebounceVal(0, 1000, { + const counter = useDebounceState(0, 1000, { maxWait: 5000, }); @@ -38,9 +40,9 @@ export default function EventListener() {
- )} - {dashboards.length > 0 && ( -
-
Your dashboards
-
- {dashboards.map((item) => ( - - ))} -
+
+ {dashboards.map((item) => ( + + ))}
- )} +
); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-project-selector.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-project-selector.tsx index 41dbaf2f..b16559b9 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-project-selector.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-project-selector.tsx @@ -1,45 +1,144 @@ 'use client'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; import { Combobox } from '@/components/ui/combobox'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { useAppParams } from '@/hooks/useAppParams'; +import { pushModal } from '@/modals'; +import { cn } from '@/utils/cn'; +import { + Building2Icon, + CheckIcon, + ChevronsUpDown, + ChevronsUpDownIcon, + PlusIcon, +} from 'lucide-react'; import { usePathname, useRouter } from 'next/navigation'; -import type { getProjectsByOrganizationSlug } from '@openpanel/db'; +import type { + getCurrentOrganizations, + getProjectsByOrganizationSlug, +} from '@openpanel/db'; interface LayoutProjectSelectorProps { projects: Awaited>; + organizations?: Awaited>; + align?: 'start' | 'end'; } export default function LayoutProjectSelector({ projects, + organizations, + align = 'start', }: LayoutProjectSelectorProps) { const router = useRouter(); const { organizationSlug, projectId } = useAppParams(); const pathname = usePathname() || ''; + const [open, setOpen] = useState(false); + + const changeProject = (newProjectId: string) => { + if (organizationSlug && projectId) { + const split = pathname + .replace( + `/${organizationSlug}/${projectId}`, + `/${organizationSlug}/${newProjectId}` + ) + .split('/'); + // slicing here will remove everything after /{orgId}/{projectId}/dashboards [slice here] /xxx/xxx/xxx + router.push(split.slice(0, 4).join('/')); + } else { + router.push(`/${organizationSlug}/${newProjectId}`); + } + }; + + const changeOrganization = (newOrganizationId: string) => { + router.push(`/${newOrganizationId}`); + }; return ( -
- { - if (organizationSlug && projectId) { - const split = pathname.replace(projectId, value).split('/'); - // slicing here will remove everything after /{orgId}/{projectId}/dashboards [slice here] /xxx/xxx/xxx - router.push(split.slice(0, 4).join('/')); - } else { - router.push(`/${organizationSlug}/${value}`); - } - }} - value={projectId} - items={ - projects.map((item) => ({ - label: item.name, - value: item.id, - })) ?? [] - } - /> -
+ + + + + + Projects + + {projects.map((project) => ( + changeProject(project.id)} + > + {project.name} + {project.id === projectId && ( + + + + )} + + ))} + + pushModal('AddProject')} + > + Create new project + + + + + + {!!organizations && ( + <> + + Organizations + + {organizations.map((organization) => ( + changeOrganization(organization.id)} + > + {organization.name} + {organization.id === organizationSlug && ( + + + + )} + + ))} + + + New organization + + + + + + + )} + + ); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx index 1ce9fb75..b2b1b80a 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx @@ -1,30 +1,34 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Logo } from '@/components/logo'; -import { buttonVariants } from '@/components/ui/button'; +import { LogoSquare } from '@/components/logo'; +import SettingsToggle from '@/components/settings-toggle'; +import { Button } from '@/components/ui/button'; import { cn } from '@/utils/cn'; import { Rotate as Hamburger } from 'hamburger-react'; -import { PlusIcon } from 'lucide-react'; -import Link from 'next/link'; +import { MenuIcon, XIcon } from 'lucide-react'; import { usePathname } from 'next/navigation'; -import type { IServiceDashboards, IServiceOrganization } from '@openpanel/db'; +import type { + getProjectsByOrganizationSlug, + IServiceDashboards, + IServiceOrganization, +} from '@openpanel/db'; import LayoutMenu from './layout-menu'; -import LayoutOrganizationSelector from './layout-organization-selector'; +import LayoutProjectSelector from './layout-project-selector'; interface LayoutSidebarProps { organizations: IServiceOrganization[]; dashboards: IServiceDashboards; organizationSlug: string; projectId: string; + projects: Awaited>; } export function LayoutSidebar({ organizations, dashboards, - organizationSlug, - projectId, + projects, }: LayoutSidebarProps) { const [active, setActive] = useState(false); const pathname = usePathname(); @@ -52,30 +56,28 @@ export function LayoutSidebar({ )} >
- +
-
- - - +
+ + +
- {/* Placeholder for LayoutOrganizationSelector */} -
-
- - - Create a report - - -
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx index 8ce44789..41875e12 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx @@ -12,7 +12,7 @@ export function StickyBelowHeader({ return (
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx index 4be142c7..157f523b 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx @@ -6,6 +6,7 @@ import { getDashboardsByProjectId, } from '@openpanel/db'; +import LayoutContent from './layout-content'; import { LayoutSidebar } from './layout-sidebar'; import SideEffects from './side-effects'; @@ -46,9 +47,15 @@ export default async function AppLayout({ return (
-
{children}
+ {children}
); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx index 15df48a2..7589b4ef 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx @@ -1,39 +1,15 @@ -import DarkModeToggle from '@/components/dark-mode-toggle'; -import withSuspense from '@/hocs/with-suspense'; - -import { getCurrentProjects } from '@openpanel/db'; - -import LayoutProjectSelector from './layout-project-selector'; - interface PageLayoutProps { title: React.ReactNode; - organizationSlug: string; } -async function PageLayout({ title, organizationSlug }: PageLayoutProps) { - const projects = await getCurrentProjects(organizationSlug); - +function PageLayout({ title }: PageLayoutProps) { return ( <> -
+
{title}
-
-
- -
- {projects.length > 0 && } -
); } -const Loading = ({ title }: PageLayoutProps) => ( - <> -
-
{title}
-
- -); - -export default withSuspense(PageLayout, Loading); +export default PageLayout; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page.tsx index 833cce44..ad1f6a1f 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page.tsx @@ -1,4 +1,3 @@ -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; import ServerLiveCounter from '@/components/overview/live-counter'; @@ -10,7 +9,6 @@ import OverviewTopPages from '@/components/overview/overview-top-pages'; import OverviewTopSources from '@/components/overview/overview-top-sources'; import OverviewMetrics from '../../../../components/overview/overview-metrics'; -import { StickyBelowHeader } from './layout-sticky-below-header'; import { OverviewReportRange } from './overview-sticky-header'; interface PageProps { @@ -20,14 +18,11 @@ interface PageProps { }; } -export default function Page({ - params: { organizationSlug, projectId }, -}: PageProps) { +export default function Page({ params: { projectId } }: PageProps) { return ( <> - - -
+
+
@@ -38,8 +33,8 @@ export default function Page({
- -
+
+
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/most-events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/most-events.tsx index ffb6ebff..eb9cd2e9 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/most-events.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/most-events.tsx @@ -18,12 +18,12 @@ const MostEvents = ({ data }: Props) => { {data.slice(0, 5).map((item) => (
-
+
{item.name}
{item.count}
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx index a58400e1..9484bf65 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx @@ -1,25 +1,17 @@ -import { start } from 'repl'; -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; import ClickToCopy from '@/components/click-to-copy'; import { ListPropertiesIcon } from '@/components/events/list-properties-icon'; import { ProfileAvatar } from '@/components/profiles/profile-avatar'; -import { - eventQueryFiltersParser, - eventQueryNamesFilter, -} from '@/hooks/useEventQueryFilters'; +import { Padding } from '@/components/ui/padding'; import { getProfileName } from '@/utils/getters'; import { notFound } from 'next/navigation'; -import { parseAsInteger } from 'nuqs'; -import type { GetEventListOptions } from '@openpanel/db'; -import { getProfileById } from '@openpanel/db'; +import { getProfileByIdCached } from '@openpanel/db'; -import EventListServer from '../../events/event-list'; -import { StickyBelowHeader } from '../../layout-sticky-below-header'; import MostEventsServer from './most-events'; import PopularRoutesServer from './popular-routes'; import ProfileActivityServer from './profile-activity'; import ProfileCharts from './profile-charts'; +import Events from './profile-events'; import ProfileMetrics from './profile-metrics'; interface PageProps { @@ -38,66 +30,50 @@ interface PageProps { } export default async function Page({ - params: { projectId, profileId, organizationSlug }, - searchParams, + params: { projectId, profileId }, }: PageProps) { - const eventListOptions: GetEventListOptions = { - projectId, - profileId, - take: 50, - cursor: - parseAsInteger.parseServerSide(searchParams.cursor ?? '') ?? undefined, - events: eventQueryNamesFilter.parseServerSide(searchParams.events ?? ''), - filters: - eventQueryFiltersParser.parseServerSide(searchParams.f ?? '') ?? - undefined, - }; - const profile = await getProfileById(profileId, projectId); + const profile = await getProfileByIdCached(profileId, projectId); if (!profile) { return notFound(); } return ( - <> - } /> - -
- -
- -

- {getProfileName(profile)} -

-
-
- -
-
+ +
+ +
+ +

+ {getProfileName(profile)} +

+
- - -
-
-
+
+
+
+
+ +
+
-
+
-
+
- +
- + ); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/popular-routes.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/popular-routes.tsx index b31fd8f7..687df3b5 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/popular-routes.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/popular-routes.tsx @@ -18,12 +18,12 @@ const PopularRoutes = ({ data }: Props) => { {data.slice(0, 5).map((item) => (
-
+
{item.path}
{item.count}
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx index e14e7b36..783fd31d 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx @@ -15,6 +15,7 @@ import { endOfMonth, format, formatISO, + isSameMonth, startOfMonth, subMonths, } from 'date-fns'; @@ -43,19 +44,72 @@ const ProfileActivity = ({ data }: Props) => {
- -
+ +
-
+
+ {format(subMonths(startDate, 3), 'MMMM yyyy')} +
+
+ {eachDayOfInterval({ + start: startOfMonth(subMonths(startDate, 3)), + end: endOfMonth(subMonths(startDate, 3)), + }).map((date) => { + const hit = data.find((item) => + item.date.includes( + formatISO(date, { representation: 'date' }) + ) + ); + return ( +
+ ); + })} +
+
+
+
+ {format(subMonths(startDate, 2), 'MMMM yyyy')} +
+
+ {eachDayOfInterval({ + start: startOfMonth(subMonths(startDate, 2)), + end: endOfMonth(subMonths(startDate, 2)), + }).map((date) => { + const hit = data.find((item) => + item.date.includes( + formatISO(date, { representation: 'date' }) + ) + ); + return ( +
+ ); + })} +
+
+
+
{format(subMonths(startDate, 1), 'MMMM yyyy')}
-
+
{eachDayOfInterval({ start: startOfMonth(subMonths(startDate, 1)), end: endOfMonth(subMonths(startDate, 1)), @@ -78,8 +132,8 @@ const ProfileActivity = ({ data }: Props) => {
-
{format(startDate, 'MMMM yyyy')}
-
+
{format(startDate, 'MMMM yyyy')}
+
{eachDayOfInterval({ start: startDate, end: endDate, diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx index 49ead233..6d01f60a 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx @@ -80,19 +80,19 @@ const ProfileCharts = ({ profileId, projectId }: Props) => { return ( <> - + Page views - + - + Events per day - + diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx new file mode 100644 index 00000000..e65e78e3 --- /dev/null +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { TableButtons } from '@/components/data-table'; +import { EventsTable } from '@/components/events/table'; +import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; +import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; +import { + useEventQueryFilters, + useEventQueryNamesFilter, +} from '@/hooks/useEventQueryFilters'; +import { api } from '@/trpc/client'; +import { Loader2Icon } from 'lucide-react'; +import { parseAsInteger, useQueryState } from 'nuqs'; + +import { GetEventListOptions } from '@openpanel/db'; + +type Props = { + projectId: string; + profileId: string; +}; + +const Events = ({ projectId, profileId }: Props) => { + const [filters] = useEventQueryFilters(); + const [eventNames] = useEventQueryNamesFilter(); + const [cursor, setCursor] = useQueryState( + 'cursor', + parseAsInteger.withDefault(0) + ); + const query = api.event.events.useQuery( + { + cursor, + projectId, + take: 50, + events: eventNames, + filters, + profileId, + }, + { + keepPreviousData: true, + } + ); + + return ( +
+ + + + {query.isRefetching && ( +
+ +
+ )} +
+ +
+ ); +}; + +export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx index 86beb12a..36900b68 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx @@ -1,17 +1,18 @@ import withSuspense from '@/hocs/with-suspense'; +import type { IServiceProfile } from '@openpanel/db'; import { getProfileMetrics } from '@openpanel/db'; import ProfileMetrics from './profile-metrics'; type Props = { projectId: string; - profileId: string; + profile: IServiceProfile; }; -const ProfileMetricsServer = async ({ projectId, profileId }: Props) => { - const data = await getProfileMetrics(profileId, projectId); - return ; +const ProfileMetricsServer = async ({ projectId, profile }: Props) => { + const data = await getProfileMetrics(profile.id, projectId); + return ; }; export default withSuspense(ProfileMetricsServer, () => null); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx index 79d0f80b..da7e3659 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx @@ -1,66 +1,115 @@ 'use client'; +import { ListPropertiesIcon } from '@/components/events/list-properties-icon'; import { useNumber } from '@/hooks/useNumerFormatter'; -import { utc } from '@/utils/date'; +import { cn } from '@/utils/cn'; +import { formatDateTime, utc } from '@/utils/date'; import { formatDistanceToNow } from 'date-fns'; +import { parseAsStringEnum, useQueryState } from 'nuqs'; -import type { IProfileMetrics } from '@openpanel/db'; +import type { IProfileMetrics, IServiceProfile } from '@openpanel/db'; type Props = { data: IProfileMetrics; + profile: IServiceProfile; }; -const ProfileMetrics = ({ data }: Props) => { +function Card({ title, value }: { title: string; value: string }) { + return ( +
+
{title}
+
{value}
+
+ ); +} + +function Info({ title, value }: { title: string; value: string }) { + return ( +
+
{title}
+
{value || '-'}
+
+ ); +} + +const ProfileMetrics = ({ data, profile }: Props) => { + const [tab, setTab] = useQueryState( + 'tab', + parseAsStringEnum(['profile', 'properties']).withDefault('profile') + ); const number = useNumber(); return ( -
-
-
- First seen -
-
- {formatDistanceToNow(utc(data.firstSeen))} -
-
-
-
- Last seen -
-
- {formatDistanceToNow(utc(data.lastSeen))} -
-
-
-
- Sessions -
-
- {number.format(data.sessions)} -
-
-
-
- Avg. Session -
-
- {number.formatWithUnit(data.durationAvg / 1000, 'min')} -
-
-
-
- P90. Session -
-
- {number.formatWithUnit(data.durationP90 / 1000, 'min')} -
-
-
-
- Page views -
-
- {number.format(data.screenViews)} +
+
+
+
+ +
+ +
+
+ {tab === 'profile' && ( + <> + + + + + + + + )} + {tab === 'properties' && ( + <> + {Object.entries(profile.properties) + .filter(([key, value]) => value !== undefined) + .map(([key, value]) => ( + + ))} + + )} +
+ + + + + +
); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx index 73f726f9..93a3a3f7 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx @@ -1,50 +1,45 @@ -import { Suspense } from 'react'; -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; -import { eventQueryFiltersParser } from '@/hooks/useEventQueryFilters'; -import { parseAsInteger } from 'nuqs'; +import { PageTabs, PageTabsLink } from '@/components/page-tabs'; +import { Padding } from '@/components/ui/padding'; +import { parseAsStringEnum } from 'nuqs'; -import LastActiveUsersServer from '../retention/last-active-users'; -import ProfileListServer from './profile-list'; -import ProfileTopServer from './profile-top'; +import PowerUsers from './power-users'; +import Profiles from './profiles'; interface PageProps { params: { - organizationSlug: string; projectId: string; + organizationSlug: string; }; - searchParams: { - f?: string; - cursor?: string; - }; + searchParams: Record; } -const nuqsOptions = { - shallow: false, -}; - export default function Page({ - params: { organizationSlug, projectId }, - searchParams: { cursor, f }, + params: { projectId }, + searchParams, }: PageProps) { + const tab = parseAsStringEnum(['profiles', 'power-users']) + .withDefault('profiles') + .parseServerSide(searchParams.tab); + return ( <> - -
- -
- - + +
+ + + Profiles + + + Power users + +
-
+ {tab === 'profiles' && } + {tab === 'power-users' && } + ); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx new file mode 100644 index 00000000..de026da1 --- /dev/null +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { ProfilesTable } from '@/components/profiles/table'; +import { api } from '@/trpc/client'; +import { parseAsInteger, useQueryState } from 'nuqs'; + +type Props = { + projectId: string; + profileId?: string; +}; + +const Events = ({ projectId }: Props) => { + const [cursor, setCursor] = useQueryState( + 'cursor', + parseAsInteger.withDefault(0) + ); + const query = api.profile.powerUsers.useQuery( + { + cursor, + projectId, + take: 50, + // filters, + }, + { + keepPreviousData: true, + } + ); + + return ( +
+ +
+ ); +}; + +export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx index 6c5bb1c5..9fe959a2 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx @@ -61,7 +61,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
{res.map(renderItem)}
-
DAYS
+
DAYS
); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-list/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-list/index.tsx deleted file mode 100644 index b3548c1c..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-list/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; - -import { getProfileList, getProfileListCount } from '@openpanel/db'; -import type { IChartEventFilter } from '@openpanel/validation'; - -import { ProfileList } from './profile-list'; - -interface Props { - projectId: string; - cursor?: number; - filters?: IChartEventFilter[]; -} - -const limit = 40; - -async function ProfileListServer({ projectId, cursor, filters }: Props) { - const [profiles, count] = await Promise.all([ - getProfileList({ - projectId, - take: limit, - cursor, - filters, - }), - getProfileListCount({ - projectId, - filters, - }), - ]); - return ; -} - -export default withLoadingWidget(ProfileListServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-list/profile-list.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-list/profile-list.tsx deleted file mode 100644 index 6cc6f2b8..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-list/profile-list.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client'; - -import { ListPropertiesIcon } from '@/components/events/list-properties-icon'; -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { Pagination } from '@/components/pagination'; -import { ProfileAvatar } from '@/components/profiles/profile-avatar'; -import { Button } from '@/components/ui/button'; -import { Tooltiper } from '@/components/ui/tooltip'; -import { Widget, WidgetHead } from '@/components/widget'; -import { WidgetTable } from '@/components/widget-table'; -import { useAppParams } from '@/hooks/useAppParams'; -import { useCursor } from '@/hooks/useCursor'; -import { getProfileName } from '@/utils/getters'; -import { UsersIcon } from 'lucide-react'; -import Link from 'next/link'; - -import type { IServiceProfile } from '@openpanel/db'; - -interface ProfileListProps { - data: IServiceProfile[]; - count: number; - limit?: number; -} -export function ProfileList({ data, count, limit = 50 }: ProfileListProps) { - const { organizationSlug, projectId } = useAppParams(); - const { cursor, setCursor, loading } = useCursor(); - return ( - - -
Profiles
- -
- {data.length ? ( - <> - item.id} - columns={[ - { - name: 'Name', - render(profile) { - return ( - - - {getProfileName(profile)} - - ); - }, - }, - { - name: '', - render(profile) { - return ; - }, - }, - { - name: 'Last seen', - render(profile) { - return ( - -
- {profile.createdAt.toLocaleTimeString()} -
-
- ); - }, - }, - ]} - /> -
- -
- - ) : ( - - {cursor !== 0 ? ( - <> -

Looks like you have reached the end of the list

- - - ) : ( -

Looks like there are no profiles here

- )} -
- )} -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-top/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-top/index.tsx deleted file mode 100644 index 337edc1e..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-top/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { ListPropertiesIcon } from '@/components/events/list-properties-icon'; -import { ProfileAvatar } from '@/components/profiles/profile-avatar'; -import { Widget, WidgetHead } from '@/components/widget'; -import { WidgetTable } from '@/components/widget-table'; -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { getProfileName } from '@/utils/getters'; -import Link from 'next/link'; -import { escape } from 'sqlstring'; - -import { chQuery, getProfiles, TABLE_NAMES } from '@openpanel/db'; - -interface Props { - projectId: string; - organizationSlug: string; -} - -async function ProfileTopServer({ organizationSlug, projectId }: Props) { - // Days since last event from users - // group by days - const res = await chQuery<{ profile_id: string; count: number }>( - `SELECT profile_id, count(*) as count from ${TABLE_NAMES.events} where profile_id != '' and project_id = ${escape(projectId)} group by profile_id order by count() DESC LIMIT 50` - ); - const profiles = await getProfiles(res.map((r) => r.profile_id)); - const list = res.map((item) => { - return { - count: item.count, - ...(profiles.find((p) => p.id === item.profile_id)! ?? {}), - }; - }); - - return ( - - -
Power users
-
- !!item.id)} - keyExtractor={(item) => item.id} - columns={[ - { - name: 'Name', - render(profile) { - return ( - - - {getProfileName(profile)} - - ); - }, - }, - { - name: '', - render(profile) { - return ; - }, - }, - { - name: 'Events', - render(profile) { - return profile.count; - }, - }, - ]} - /> -
- ); -} - -export default withLoadingWidget(ProfileTopServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx new file mode 100644 index 00000000..5c75be0a --- /dev/null +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { ProfilesTable } from '@/components/profiles/table'; +import { api } from '@/trpc/client'; +import { parseAsInteger, useQueryState } from 'nuqs'; + +type Props = { + projectId: string; + profileId?: string; +}; + +const Events = ({ projectId }: Props) => { + const [cursor, setCursor] = useQueryState( + 'cursor', + parseAsInteger.withDefault(0) + ); + const query = api.profile.list.useQuery( + { + cursor, + projectId, + take: 50, + // filters, + }, + { + keepPreviousData: true, + } + ); + + return ( +
+ +
+ ); +}; + +export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx index 1349d064..da382e56 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx @@ -99,7 +99,7 @@ const Map = ({ markers }: Props) => {
@@ -123,8 +123,8 @@ const Map = ({ markers }: Props) => { )) diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx index 0f8b9ab1..082eb086 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx @@ -18,23 +18,19 @@ type Props = { projectId: string; }; }; -export default function Page({ - params: { projectId, organizationSlug }, -}: Props) { +export default function Page({ params: { projectId } }: Props) { return ( <> - } - {...{ projectId, organizationSlug }} - /> -
-
+ +
+ +
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx index a5009da7..a910673a 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx @@ -83,7 +83,7 @@ export function RealtimeLiveHistogram({ {staticArray.map((percent, i) => (
))} @@ -103,7 +103,7 @@ export function RealtimeLiveHistogram({
import('react-animated-numbers'), { function Wrapper({ children, count }: WrapperProps) { return (
-
+
Unique vistors last 30 minutes
-
+
({ diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx index 6e24e274..4e9a7bc3 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx @@ -1,6 +1,5 @@ import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; import EditReportName from '@/components/report/edit-report-name'; -import { Pencil } from 'lucide-react'; import { notFound } from 'next/navigation'; import { getReportById } from '@openpanel/db'; @@ -9,15 +8,12 @@ import ReportEditor from '../report-editor'; interface PageProps { params: { - organizationSlug: string; projectId: string; reportId: string; }; } -export default async function Page({ - params: { reportId, organizationSlug }, -}: PageProps) { +export default async function Page({ params: { reportId } }: PageProps) { const report = await getReportById(reportId); if (!report) { @@ -26,10 +22,7 @@ export default async function Page({ return ( <> - } - /> + } /> ); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx index 28d80493..9ac0223e 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx @@ -1,23 +1,12 @@ import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; import EditReportName from '@/components/report/edit-report-name'; -import { Pencil } from 'lucide-react'; import ReportEditor from './report-editor'; -interface PageProps { - params: { - organizationSlug: string; - projectId: string; - }; -} - -export default function Page({ params: { organizationSlug } }: PageProps) { +export default function Page() { return ( <> - } - /> + } /> ); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx index 859dec25..b2db1eb0 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx @@ -23,15 +23,15 @@ function Tooltip(props: any) { return null; } return ( -
+
-
+
Days since last seen
{payload.days}
-
Active users
+
Active users
{payload.users}
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx index 4c085ebb..da39f6a6 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx @@ -1,7 +1,7 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Padding } from '@/components/ui/padding'; import { AlertCircleIcon } from 'lucide-react'; -import PageLayout from '../page-layout'; import LastActiveUsersServer from './last-active-users'; import RollingActiveUsers from './rolling-active-users'; import UsersRetentionSeries from './users-retention-series'; @@ -9,16 +9,15 @@ import WeeklyCohortsServer from './weekly-cohorts'; type Props = { params: { - organizationSlug: string; projectId: string; }; }; -const Retention = ({ params: { projectId, organizationSlug } }: Props) => { +const Retention = ({ params: { projectId } }: Props) => { return ( - <> - -
+ +

Retention

+
Experimental feature @@ -59,7 +58,7 @@ const Retention = ({ params: { projectId, organizationSlug } }: Props) => {
- +
); }; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx index 1cd88179..2cdee07c 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx @@ -29,20 +29,20 @@ function Tooltip(props: any) { return null; } return ( -
-
{payload.date}
+
+
{payload.date}
-
+
Monthly active users
{payload.mau}
-
Weekly active users
+
Weekly active users
{payload.wau}
-
Daily active users
+
Daily active users
{payload.dau}
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx index b7001730..be03e574 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx @@ -33,20 +33,20 @@ function Tooltip({ payload }: any) { return null; } return ( -
+
{formatDate(new Date(date))}
-
Active Users
+
Active Users
{active_users}
-
Retained Users
+
Retained Users
{retained_users}
-
Retention
+
Retention
{round(retention, 2)}%
diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx index 9c62032e..1fa9b1b5 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx @@ -15,7 +15,7 @@ const Cell = ({ value, ratio }: { value: number; ratio: number }) => { className={cn('relative h-8 border', ratio !== 0 && 'border-background')} >
{value}
@@ -76,7 +76,7 @@ const WeeklyCohortsServer = async ({ projectId }: Props) => { {res.map((row) => ( - + {row.first_seen} -

+

Leave empty to give access to all projects

diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx index dfa64e75..247eceb0 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx @@ -1,6 +1,9 @@ +import { TableButtons } from '@/components/data-table'; +import { InvitesTable } from '@/components/settings/invites'; + import { getInvites, getProjectsByOrganizationSlug } from '@openpanel/db'; -import Invites from './invites'; +import CreateInvite from './create-invite'; interface Props { organizationSlug: string; @@ -12,7 +15,14 @@ const InvitesServer = async ({ organizationSlug }: Props) => { getProjectsByOrganizationSlug(organizationSlug), ]); - return ; + return ( +
+ + + + +
+ ); }; export default InvitesServer; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/invites.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/invites.tsx deleted file mode 100644 index ef37b7ca..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/invites.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import { TooltipComplete } from '@/components/tooltip-complete'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { Widget, WidgetHead } from '@/components/widget'; -import { api } from '@/trpc/client'; -import { MoreHorizontalIcon } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { pathOr } from 'ramda'; -import { toast } from 'sonner'; - -import type { IServiceInvite, IServiceProject } from '@openpanel/db'; - -import CreateInvite from './create-invite'; - -interface Props { - invites: IServiceInvite[]; - projects: IServiceProject[]; -} - -const Invites = ({ invites, projects }: Props) => { - return ( - - - Invites - - - - - - Mail - Role - Created - Access - More - - - - {invites.map((item) => { - return ; - })} - -
-
- ); -}; - -interface ItemProps extends IServiceInvite { - projects: IServiceProject[]; -} - -function Item({ id, email, role, createdAt, projects, meta }: ItemProps) { - const router = useRouter(); - const access = pathOr([], ['access'], meta); - const revoke = api.organization.revokeInvite.useMutation({ - onSuccess() { - toast.success(`Invite for ${email} revoked`); - router.refresh(); - }, - onError() { - toast.error(`Failed to revoke invite for ${email}`); - }, - }); - return ( - - {email} - {role} - - - {new Date(createdAt).toLocaleDateString()} - - - - {access.map((id) => { - const project = projects.find((p) => p.id === id); - if (!project) { - return ( - - Unknown - - ); - } - return ( - - {project.name} - - ); - })} - {access.length === 0 && ( - All projects - )} - - - - - -
- -
-
- - - What is a project - - A project can be a website, mobile app or any other application - that you want to track event for. Each project can have one or - more clients. The client is used to send events to the project. - - - - {projects.map((project) => { - const pClients = clients.filter( - (client) => client.projectId === project.id - ); - return ( - - -
- {project.name} - - {pClients.length > 0 - ? `(${pClients.length} clients)` - : 'No clients created yet'} - -
-
-
- - -
- {pClients.map((item) => { - return ( -
+

Projects

+ +
+
+ + + What is a project + + A project can be a website, mobile app or any other application that + you want to track event for. Each project can have one or more + clients. The client is used to send events to the project. + + + + {projects.map((project) => { + const pClients = clients.filter( + (client) => client.projectId === project.id + ); + return ( + + +
+ {project.name} + + {pClients.length > 0 + ? `(${pClients.length} clients)` + : 'No clients created yet'} + +
+
+
+ + +
+ {pClients.map((item) => { + return ( +
+
{item.name}
+ -
{item.name}
- - Client ID: ...{item.id.slice(-12)} - -
- {item.cors && - item.cors !== '*' && - `Website: ${item.cors}`} -
-
- -
+ Client ID: ...{item.id.slice(-12)} +
+
+ {item.cors && + item.cors !== '*' && + `Website: ${item.cors}`}
- ); - })} - -
- - - ); - })} - -
+
+ +
+
+ ); + })} + +
+
+
+ ); + })} +
); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx index c968f620..a0242f63 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx @@ -1,4 +1,5 @@ import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; +import { Padding } from '@/components/ui/padding'; import { getClientsByOrganizationSlug, @@ -22,9 +23,8 @@ export default async function Page({ ]); return ( - <> - + - + ); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx index 85456771..d493af55 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx @@ -1,9 +1,9 @@ 'use client'; -import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header'; import { DataTable } from '@/components/data-table'; import { columns } from '@/components/references/table'; import { Button } from '@/components/ui/button'; +import { Padding } from '@/components/ui/padding'; import { pushModal } from '@/modals'; import { PlusIcon } from 'lucide-react'; @@ -15,19 +15,15 @@ interface ListProjectsProps { export default function ListReferences({ data }: ListProjectsProps) { return ( - <> - -
-
- -
- -
- + +
+

References

+
- + +
); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx index cd2593a0..c506cdec 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx @@ -1,5 +1,3 @@ -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; - import { getReferences } from '@openpanel/db'; import ListReferences from './list-references'; @@ -11,9 +9,7 @@ interface PageProps { }; } -export default async function Page({ - params: { organizationSlug, projectId }, -}: PageProps) { +export default async function Page({ params: { projectId } }: PageProps) { const references = await getReferences({ where: { projectId, @@ -24,7 +20,6 @@ export default async function Page({ return ( <> - ); diff --git a/apps/dashboard/src/app/(onboarding)/layout.tsx b/apps/dashboard/src/app/(onboarding)/layout.tsx index a4108d55..af1c0e11 100644 --- a/apps/dashboard/src/app/(onboarding)/layout.tsx +++ b/apps/dashboard/src/app/(onboarding)/layout.tsx @@ -20,9 +20,9 @@ const Page = ({ children }: Props) => {
-
+
-
+
Welcome to Openpanel
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-app.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-app.tsx index 2e65020b..b76f828e 100644 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-app.tsx +++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-app.tsx @@ -15,7 +15,7 @@ const ConnectApp = ({ client }: Props) => { App
-

+

Pick a framework below to get started.

@@ -30,7 +30,7 @@ const ConnectApp = ({ client }: Props) => { className="flex items-center gap-4 rounded-md border p-2 py-2 text-left" key={framework.name} > -
+
{ ))}
-

+

Missing a framework?{' '} { Backend

-

+

Pick a framework below to get started.

@@ -30,7 +30,7 @@ const ConnectBackend = ({ client }: Props) => { className="flex items-center gap-4 rounded-md border p-2 py-2 text-left" key={framework.name} > - -

+

Pick a framework below to get started.

@@ -30,7 +30,7 @@ const ConnectWeb = ({ client }: Props) => { className="flex items-center gap-4 rounded-md border p-2 py-2 text-left" key={framework.name} > -
+
{ ))}
-

+

Missing a framework?{' '} {

-
+
Connection status: {renderBadge()}
@@ -81,13 +81,13 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => { {isConnected ? (
{events.length > 5 && ( -
+
{' '} {events.length - 5} more events
)} {events.slice(-5).map((event, index) => ( -
+
{' '} {event.name}{' '} @@ -97,7 +97,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => { ))}
) : ( -
+
Verify that your events works before submitting any changes to App Store/Google Play
@@ -105,7 +105,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
-
+
You can{' '}
diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx index 71563cc6..2f4c1ebb 100644 --- a/apps/dashboard/src/app/layout.tsx +++ b/apps/dashboard/src/app/layout.tsx @@ -6,6 +6,9 @@ import Providers from './providers'; import '@/styles/globals.css'; import '/node_modules/flag-icons/css/flag-icons.min.css'; +import { GeistMono } from 'geist/font/mono'; +import { GeistSans } from 'geist/font/sans'; + export const metadata = { title: 'Overview - Openpanel.dev', }; @@ -25,7 +28,11 @@ export default function RootLayout({ return ( {cors && ( -

+

You will only need the secret if you want to send server events.

)} @@ -25,7 +25,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) { {cors && (
-
+
{cors}
diff --git a/apps/dashboard/src/components/clients/table.tsx b/apps/dashboard/src/components/clients/table.tsx index 68dfe4c1..12d26213 100644 --- a/apps/dashboard/src/components/clients/table.tsx +++ b/apps/dashboard/src/components/clients/table.tsx @@ -13,7 +13,7 @@ export const columns: ColumnDef[] = [ return (
{row.original.name}
-
+
{row.original.project?.name ?? 'No project'}
diff --git a/apps/dashboard/src/components/color-square.tsx b/apps/dashboard/src/components/color-square.tsx index 61fbf8d1..cbdd63fd 100644 --- a/apps/dashboard/src/components/color-square.tsx +++ b/apps/dashboard/src/components/color-square.tsx @@ -7,7 +7,7 @@ export function ColorSquare({ children, className }: ColorSquareProps) { return (
diff --git a/apps/dashboard/src/components/data-table.tsx b/apps/dashboard/src/components/data-table.tsx index a4f24418..162c86ba 100644 --- a/apps/dashboard/src/components/data-table.tsx +++ b/apps/dashboard/src/components/data-table.tsx @@ -1,5 +1,6 @@ 'use client'; +import { cn } from '@/utils/cn'; import { flexRender, getCoreRowModel, @@ -7,20 +8,27 @@ import { } from '@tanstack/react-table'; import type { ColumnDef } from '@tanstack/react-table'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from './ui/table'; +import { Grid, GridBody, GridCell, GridHeader, GridRow } from './grid-table'; interface DataTableProps { columns: ColumnDef[]; data: TData[]; } +export function TableButtons({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + export function DataTable({ columns, data }: DataTableProps) { const table = useReactTable({ data, @@ -29,47 +37,45 @@ export function DataTable({ columns, data }: DataTableProps) { }); return ( - - + + {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + ))} - - + + {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - {row.getVisibleCells().map((cell) => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} - + ))} - + )) ) : ( - - - No results. - - + + +
No results.
+
+
)} -
-
+ + ); } diff --git a/apps/dashboard/src/components/events/table/columns.tsx b/apps/dashboard/src/components/events/table/columns.tsx new file mode 100644 index 00000000..0f36da45 --- /dev/null +++ b/apps/dashboard/src/components/events/table/columns.tsx @@ -0,0 +1,145 @@ +import { EventIcon } from '@/app/(app)/[organizationSlug]/[projectId]/events/event-list/event-icon'; +import { ProjectLink } from '@/components/links'; +import { SerieIcon } from '@/components/report/chart/SerieIcon'; +import { TooltipComplete } from '@/components/tooltip-complete'; +import { useNumber } from '@/hooks/useNumerFormatter'; +import { pushModal } from '@/modals'; +import { formatDateTime, formatTime } from '@/utils/date'; +import { getProfileName } from '@/utils/getters'; +import type { ColumnDef } from '@tanstack/react-table'; +import { isToday } from 'date-fns'; + +import type { IServiceEvent } from '@openpanel/db'; + +export function useColumns() { + const number = useNumber(); + const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Name', + cell({ row }) { + const { name, path, duration } = row.original; + const renderName = () => { + if (name === 'screen_view') { + if (path.includes('/')) { + return {path}; + } + + return ( + <> + Screen: + {path} + + ); + } + + return name.replace(/_/g, ' '); + }; + + const renderDuration = () => { + if (name === 'screen_view') { + return ( + + {number.shortWithUnit(duration / 1000, 'min')} + + ); + } + + return null; + }; + + return ( +
+ + + + {renderDuration()} + +
+ ); + }, + }, + { + accessorKey: 'country', + header: 'Country', + cell({ row }) { + const { country, city } = row.original; + return ( +
+ + {city} +
+ ); + }, + }, + { + accessorKey: 'os', + header: 'OS', + cell({ row }) { + const { os } = row.original; + return ( +
+ + {os} +
+ ); + }, + }, + { + accessorKey: 'browser', + header: 'Browser', + cell({ row }) { + const { browser } = row.original; + return ( +
+ + {browser} +
+ ); + }, + }, + { + accessorKey: 'profileId', + header: 'Profile', + cell({ row }) { + const { profile } = row.original; + if (!profile) { + return null; + } + return ( + + {getProfileName(profile)} + + ); + }, + }, + { + accessorKey: 'createdAt', + header: 'Created at', + cell({ row }) { + const date = row.original.createdAt; + return ( +
{isToday(date) ? formatTime(date) : formatDateTime(date)}
+ ); + }, + }, + ]; + + return columns; +} diff --git a/apps/dashboard/src/components/events/table/index.tsx b/apps/dashboard/src/components/events/table/index.tsx new file mode 100644 index 00000000..875565e2 --- /dev/null +++ b/apps/dashboard/src/components/events/table/index.tsx @@ -0,0 +1,71 @@ +import type { Dispatch, SetStateAction } from 'react'; +import { DataTable } from '@/components/data-table'; +import { FullPageEmptyState } from '@/components/full-page-empty-state'; +import { Pagination } from '@/components/pagination'; +import { Button } from '@/components/ui/button'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { GanttChartIcon } from 'lucide-react'; + +import type { IServiceEvent } from '@openpanel/db'; + +import { useColumns } from './columns'; + +type Props = + | { + query: UseQueryResult; + } + | { + query: UseQueryResult; + cursor: number; + setCursor: Dispatch>; + }; + +export const EventsTable = ({ query, ...props }: Props) => { + const columns = useColumns(); + const { data, isFetching, isLoading } = query; + + if (isLoading) { + return ( +
+
+
+
+
+
+
+ ); + } + + if (data?.length === 0) { + return ( + +

Could not find any events

+ {'cursor' in props && props.cursor !== 0 && ( + + )} +
+ ); + } + + return ( + <> + + {'cursor' in props && ( + + )} + + ); +}; diff --git a/apps/dashboard/src/components/forms/checkbox-item.tsx b/apps/dashboard/src/components/forms/checkbox-item.tsx index 00cd83b9..3f24f3e8 100644 --- a/apps/dashboard/src/components/forms/checkbox-item.tsx +++ b/apps/dashboard/src/components/forms/checkbox-item.tsx @@ -24,7 +24,7 @@ export const CheckboxItem = forwardRef(