chore(root): migrate to biome
This commit is contained in:
@@ -30,7 +30,7 @@ import {
|
||||
getDefaultIntervalByRange,
|
||||
timeWindows,
|
||||
} from '@openpanel/constants';
|
||||
import type { getReportsByDashboardId, IServiceDashboard } from '@openpanel/db';
|
||||
import type { IServiceDashboard, getReportsByDashboardId } from '@openpanel/db';
|
||||
|
||||
import { OverviewReportRange } from '../../overview-sticky-header';
|
||||
|
||||
@@ -64,7 +64,7 @@ export function ListReports({ reports, dashboard }: ListReportsProps) {
|
||||
params.projectId
|
||||
}/reports?${new URLSearchParams({
|
||||
dashboardId: params.dashboardId,
|
||||
}).toString()}`
|
||||
}).toString()}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -163,7 +163,7 @@ export function ListReports({ reports, dashboard }: ListReportsProps) {
|
||||
params.projectId
|
||||
}/reports?${new URLSearchParams({
|
||||
dashboardId: params.dashboardId,
|
||||
}).toString()}`
|
||||
}).toString()}`,
|
||||
)
|
||||
}
|
||||
className="mt-14"
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
|
||||
{dashboards.map((item) => {
|
||||
const visibleReports = item.reports.slice(
|
||||
0,
|
||||
item.reports.length > 6 ? 5 : 6
|
||||
item.reports.length > 6 ? 5 : 6,
|
||||
);
|
||||
return (
|
||||
<Card key={item.id} hover>
|
||||
@@ -97,7 +97,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'mt-4 grid gap-2',
|
||||
'grid-cols-1 @sm:grid-cols-2'
|
||||
'grid-cols-1 @sm:grid-cols-2',
|
||||
)}
|
||||
>
|
||||
{visibleReports.map((report) => {
|
||||
@@ -145,6 +145,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
|
||||
<CardActions>
|
||||
<CardActionsItem className="w-full" asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
pushModal('EditDashboard', item);
|
||||
}}
|
||||
@@ -155,6 +156,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
|
||||
</CardActionsItem>
|
||||
<CardActionsItem className="w-full text-destructive" asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
deletion.mutate({
|
||||
id: item.id,
|
||||
|
||||
@@ -15,7 +15,7 @@ const Conversions = ({ projectId }: Props) => {
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return <EventsTable query={query} />;
|
||||
|
||||
@@ -23,7 +23,7 @@ const Events = ({ projectId, profileId }: Props) => {
|
||||
const [eventNames] = useEventQueryNamesFilter();
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsInteger.withDefault(0)
|
||||
parseAsInteger.withDefault(0),
|
||||
);
|
||||
const query = api.event.events.useQuery(
|
||||
{
|
||||
@@ -36,7 +36,7 @@ const Events = ({ projectId, profileId }: Props) => {
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -27,16 +27,16 @@ export default function Page({
|
||||
<Padding>
|
||||
<div className="mb-4">
|
||||
<PageTabs>
|
||||
<PageTabsLink href={`?tab=events`} isActive={tab === 'events'}>
|
||||
<PageTabsLink href={'?tab=events'} isActive={tab === 'events'}>
|
||||
Events
|
||||
</PageTabsLink>
|
||||
<PageTabsLink
|
||||
href={`?tab=conversions`}
|
||||
href={'?tab=conversions'}
|
||||
isActive={tab === 'conversions'}
|
||||
>
|
||||
Conversions
|
||||
</PageTabsLink>
|
||||
<PageTabsLink href={`?tab=charts`} isActive={tab === 'charts'}>
|
||||
<PageTabsLink href={'?tab=charts'} isActive={tab === 'charts'}>
|
||||
Charts
|
||||
</PageTabsLink>
|
||||
</PageTabs>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { pushModal } from '@/modals';
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
import type { LucideProps } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import type { IServiceDashboards } from '@openpanel/db';
|
||||
|
||||
@@ -42,7 +42,7 @@ function LinkWithIcon({
|
||||
className={cn(
|
||||
'text-text flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200',
|
||||
active && 'bg-def-200',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function LayoutOrganizationSelector({
|
||||
const router = useRouter();
|
||||
|
||||
const organization = organizations.find(
|
||||
(item) => item.id === params.organizationSlug
|
||||
(item) => item.id === params.organizationSlug,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import {
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
PlusIcon,
|
||||
} from 'lucide-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type {
|
||||
getCurrentOrganizations,
|
||||
@@ -50,7 +50,7 @@ export default function LayoutProjectSelector({
|
||||
const split = pathname
|
||||
.replace(
|
||||
`/${organizationSlug}/${projectId}`,
|
||||
`/${organizationSlug}/${newProjectId}`
|
||||
`/${organizationSlug}/${newProjectId}`,
|
||||
)
|
||||
.split('/');
|
||||
// slicing here will remove everything after /{orgId}/{projectId}/dashboards [slice here] /xxx/xxx/xxx
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LogoSquare } from '@/components/logo';
|
||||
import SettingsToggle from '@/components/settings-toggle';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -8,11 +7,12 @@ import { cn } from '@/utils/cn';
|
||||
import { Rotate as Hamburger } from 'hamburger-react';
|
||||
import { MenuIcon, XIcon } from 'lucide-react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type {
|
||||
getProjectsByOrganizationSlug,
|
||||
IServiceDashboards,
|
||||
IServiceOrganization,
|
||||
getProjectsByOrganizationSlug,
|
||||
} from '@openpanel/db';
|
||||
|
||||
import LayoutMenu from './layout-menu';
|
||||
@@ -40,19 +40,20 @@ export function LayoutSidebar({
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActive(false)}
|
||||
className={cn(
|
||||
'fixed bottom-0 left-0 right-0 top-0 z-50 backdrop-blur-sm transition-opacity',
|
||||
active
|
||||
? 'pointer-events-auto opacity-100'
|
||||
: 'pointer-events-none opacity-0'
|
||||
: 'pointer-events-none opacity-0',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'fixed left-0 top-0 z-50 flex h-screen w-72 flex-col border-r border-border bg-card transition-transform',
|
||||
'-translate-x-72 lg:-translate-x-0', // responsive
|
||||
active && 'translate-x-0' // force active on mobile
|
||||
active && 'translate-x-0', // force active on mobile
|
||||
)}
|
||||
>
|
||||
<div className="absolute -right-12 flex h-16 items-center lg:hidden">
|
||||
@@ -77,7 +78,7 @@ export function LayoutSidebar({
|
||||
<LayoutMenu dashboards={dashboards} />
|
||||
</div>
|
||||
<div className="fixed bottom-0 left-0 right-0">
|
||||
<div className="h-8 w-full bg-gradient-to-t from-card to-card/0"></div>
|
||||
<div className="h-8 w-full bg-gradient-to-t from-card to-card/0" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function StickyBelowHeader({
|
||||
<div
|
||||
className={cn(
|
||||
'top-0 z-20 border-b border-border bg-card md:sticky',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { ReportChart } from '@/components/report-chart';
|
||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||
import { cn } from '@/utils/cn';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { ExternalLinkIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import type { IServicePage } from '@openpanel/db';
|
||||
|
||||
@@ -38,7 +38,7 @@ export const PagesTable = memo(
|
||||
className={cn(
|
||||
cell,
|
||||
'center-center font-mono text-lg font-semibold',
|
||||
index === data.length - 1 && 'rounded-bl-md'
|
||||
index === data.length - 1 && 'rounded-bl-md',
|
||||
)}
|
||||
>
|
||||
{number.short(item.count)}
|
||||
@@ -46,7 +46,7 @@ export const PagesTable = memo(
|
||||
<div
|
||||
className={cn(
|
||||
cell,
|
||||
'flex w-80 flex-col justify-center gap-2 text-left'
|
||||
'flex w-80 flex-col justify-center gap-2 text-left',
|
||||
)}
|
||||
>
|
||||
<span className="truncate font-medium">{item.title}</span>
|
||||
@@ -68,7 +68,7 @@ export const PagesTable = memo(
|
||||
className={cn(
|
||||
cell,
|
||||
'p-1',
|
||||
index === data.length - 1 && 'rounded-br-md'
|
||||
index === data.length - 1 && 'rounded-br-md',
|
||||
)}
|
||||
>
|
||||
<ReportChart
|
||||
@@ -122,7 +122,7 @@ export const PagesTable = memo(
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return isEqual(prevProps.data, nextProps.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
PagesTable.displayName = 'PagesTable';
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Pages({ projectId }: { projectId: string }) {
|
||||
const take = 20;
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsInteger.withDefault(0)
|
||||
parseAsInteger.withDefault(0),
|
||||
);
|
||||
const [search, setSearch] = useQueryState('search', {
|
||||
defaultValue: '',
|
||||
@@ -31,7 +31,7 @@ export function Pages({ projectId }: { projectId: string }) {
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
const data = query.data ?? [];
|
||||
|
||||
@@ -56,7 +56,7 @@ export function Pages({ projectId }: { projectId: string }) {
|
||||
className="mt-2"
|
||||
setCursor={setCursor}
|
||||
cursor={cursor}
|
||||
count={Infinity}
|
||||
count={Number.POSITIVE_INFINITY}
|
||||
take={take}
|
||||
loading={query.isFetching}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import withLoadingWidget from '@/hocs/with-loading-widget';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { chQuery, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, chQuery } from '@openpanel/db';
|
||||
|
||||
import MostEvents from './most-events';
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
const MostEventsServer = async ({ projectId, profileId }: Props) => {
|
||||
const data = await chQuery<{ count: number; name: string }>(
|
||||
`SELECT count(*) as count, name FROM ${TABLE_NAMES.events} WHERE name NOT IN ('screen_view', 'session_start', 'session_end') AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY name ORDER BY count DESC`
|
||||
`SELECT count(*) as count, name FROM ${TABLE_NAMES.events} WHERE name NOT IN ('screen_view', 'session_start', 'session_end') AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY name ORDER BY count DESC`,
|
||||
);
|
||||
return <MostEvents data={data} />;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const MostEvents = ({ data }: Props) => {
|
||||
style={{
|
||||
width: `${(item.count / max) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
<div className="relative flex justify-between ">
|
||||
<div>{item.name}</div>
|
||||
<div>{item.count}</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import withLoadingWidget from '@/hocs/with-loading-widget';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { chQuery, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, chQuery } from '@openpanel/db';
|
||||
|
||||
import PopularRoutes from './popular-routes';
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
const PopularRoutesServer = async ({ projectId, profileId }: Props) => {
|
||||
const data = await chQuery<{ count: number; path: string }>(
|
||||
`SELECT count(*) as count, path FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY path ORDER BY count DESC`
|
||||
`SELECT count(*) as count, path FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY path ORDER BY count DESC`,
|
||||
);
|
||||
return <PopularRoutes data={data} />;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const PopularRoutes = ({ data }: Props) => {
|
||||
style={{
|
||||
width: `${(item.count / max) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
<div className="relative flex justify-between ">
|
||||
<div>{item.path}</div>
|
||||
<div>{item.count}</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import withLoadingWidget from '@/hocs/with-loading-widget';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { chQuery, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, chQuery } from '@openpanel/db';
|
||||
|
||||
import ProfileActivity from './profile-activity';
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
|
||||
const ProfileActivityServer = async ({ projectId, profileId }: Props) => {
|
||||
const data = await chQuery<{ count: number; date: string }>(
|
||||
`SELECT count(*) as count, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY date ORDER BY date DESC`
|
||||
`SELECT count(*) as count, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY date ORDER BY date DESC`,
|
||||
);
|
||||
return <ProfileActivity data={data} />;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Widget,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
subMonths,
|
||||
} from 'date-fns';
|
||||
import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
data: { count: number; date: string }[];
|
||||
@@ -64,17 +64,17 @@ const ProfileActivity = ({ data }: Props) => {
|
||||
}).map((date) => {
|
||||
const hit = data.find((item) =>
|
||||
item.date.includes(
|
||||
formatISO(date, { representation: 'date' })
|
||||
)
|
||||
formatISO(date, { representation: 'date' }),
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={date.toISOString()}
|
||||
className={cn(
|
||||
'aspect-square w-full rounded',
|
||||
hit ? 'bg-highlight' : 'bg-def-200'
|
||||
hit ? 'bg-highlight' : 'bg-def-200',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -90,17 +90,17 @@ const ProfileActivity = ({ data }: Props) => {
|
||||
}).map((date) => {
|
||||
const hit = data.find((item) =>
|
||||
item.date.includes(
|
||||
formatISO(date, { representation: 'date' })
|
||||
)
|
||||
formatISO(date, { representation: 'date' }),
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={date.toISOString()}
|
||||
className={cn(
|
||||
'aspect-square w-full rounded',
|
||||
hit ? 'bg-highlight' : 'bg-def-200'
|
||||
hit ? 'bg-highlight' : 'bg-def-200',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -116,17 +116,17 @@ const ProfileActivity = ({ data }: Props) => {
|
||||
}).map((date) => {
|
||||
const hit = data.find((item) =>
|
||||
item.date.includes(
|
||||
formatISO(date, { representation: 'date' })
|
||||
)
|
||||
formatISO(date, { representation: 'date' }),
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={date.toISOString()}
|
||||
className={cn(
|
||||
'aspect-square w-full rounded',
|
||||
hit ? 'bg-highlight' : 'bg-def-200'
|
||||
hit ? 'bg-highlight' : 'bg-def-200',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -140,17 +140,17 @@ const ProfileActivity = ({ data }: Props) => {
|
||||
}).map((date) => {
|
||||
const hit = data.find((item) =>
|
||||
item.date.includes(
|
||||
formatISO(date, { representation: 'date' })
|
||||
)
|
||||
formatISO(date, { representation: 'date' }),
|
||||
),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={date.toISOString()}
|
||||
className={cn(
|
||||
'aspect-square w-full rounded',
|
||||
hit ? 'bg-highlight' : 'bg-def-200'
|
||||
hit ? 'bg-highlight' : 'bg-def-200',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { ReportChart } from '@/components/report-chart';
|
||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
import { memo } from 'react';
|
||||
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const Events = ({ projectId, profileId }: Props) => {
|
||||
const [eventNames] = useEventQueryNamesFilter();
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsInteger.withDefault(0)
|
||||
parseAsInteger.withDefault(0),
|
||||
);
|
||||
const query = api.event.events.useQuery(
|
||||
{
|
||||
@@ -37,7 +37,7 @@ const Events = ({ projectId, profileId }: Props) => {
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,7 +35,7 @@ function Info({ title, value }: { title: string; value: string }) {
|
||||
const ProfileMetrics = ({ data, profile }: Props) => {
|
||||
const [tab, setTab] = useQueryState(
|
||||
'tab',
|
||||
parseAsStringEnum(['profile', 'properties']).withDefault('profile')
|
||||
parseAsStringEnum(['profile', 'properties']).withDefault('profile'),
|
||||
);
|
||||
const number = useNumber();
|
||||
return (
|
||||
@@ -44,24 +44,26 @@ const ProfileMetrics = ({ data, profile }: Props) => {
|
||||
<div className="col-span-2 @xl:col-span-3 @4xl:col-span-6">
|
||||
<div className="row border-b">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab('profile')}
|
||||
className={cn(
|
||||
'p-4',
|
||||
'opacity-50',
|
||||
tab === 'profile' &&
|
||||
'border-b border-foreground text-foreground opacity-100'
|
||||
'border-b border-foreground text-foreground opacity-100',
|
||||
)}
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<div className="h-full w-px bg-border" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTab('properties')}
|
||||
className={cn(
|
||||
'p-4',
|
||||
'opacity-50',
|
||||
tab === 'properties' &&
|
||||
'border-b border-foreground text-foreground opacity-100'
|
||||
'border-b border-foreground text-foreground opacity-100',
|
||||
)}
|
||||
>
|
||||
Properties
|
||||
@@ -81,15 +83,12 @@ const ProfileMetrics = ({ data, profile }: Props) => {
|
||||
<ListPropertiesIcon {...profile.properties} />
|
||||
</>
|
||||
)}
|
||||
{tab === 'properties' && (
|
||||
<>
|
||||
{Object.entries(profile.properties)
|
||||
.filter(([key, value]) => value !== undefined)
|
||||
.map(([key, value]) => (
|
||||
<Info key={key} title={key} value={value as string} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{tab === 'properties' &&
|
||||
Object.entries(profile.properties)
|
||||
.filter(([key, value]) => value !== undefined)
|
||||
.map(([key, value]) => (
|
||||
<Info key={key} title={key} value={value as string} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Card
|
||||
|
||||
@@ -26,11 +26,11 @@ export default function Page({
|
||||
<Padding>
|
||||
<div className="mb-4">
|
||||
<PageTabs>
|
||||
<PageTabsLink href={`?tab=profiles`} isActive={tab === 'profiles'}>
|
||||
<PageTabsLink href={'?tab=profiles'} isActive={tab === 'profiles'}>
|
||||
Profiles
|
||||
</PageTabsLink>
|
||||
<PageTabsLink
|
||||
href={`?tab=power-users`}
|
||||
href={'?tab=power-users'}
|
||||
isActive={tab === 'power-users'}
|
||||
>
|
||||
Power users
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
const Events = ({ projectId }: Props) => {
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsInteger.withDefault(0)
|
||||
parseAsInteger.withDefault(0),
|
||||
);
|
||||
const query = api.profile.powerUsers.useQuery(
|
||||
{
|
||||
@@ -23,7 +23,7 @@ const Events = ({ projectId }: Props) => {
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { chQuery, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, chQuery } from '@openpanel/db';
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
@@ -21,7 +21,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
|
||||
// Days since last event from users
|
||||
// group by days
|
||||
const res = await chQuery<Row>(
|
||||
`SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM ${TABLE_NAMES.events} where project_id = ${escape(projectId)} group by days order by days ASC LIMIT 51`
|
||||
`SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM ${TABLE_NAMES.events} where project_id = ${escape(projectId)} group by days order by days ASC LIMIT 51`,
|
||||
);
|
||||
|
||||
const maxValue = Math.max(...res.map((x) => x.count));
|
||||
@@ -29,7 +29,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
|
||||
const calculateRatio = (currentValue: number) =>
|
||||
Math.max(
|
||||
0.1,
|
||||
Math.min(1, (currentValue - minValue) / (maxValue - minValue))
|
||||
Math.min(1, (currentValue - minValue) / (maxValue - minValue)),
|
||||
);
|
||||
|
||||
const renderItem = (item: Row) => (
|
||||
@@ -41,7 +41,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
|
||||
style={{
|
||||
opacity: calculateRatio(item.count),
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{item.count} profiles last seen{' '}
|
||||
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
const Events = ({ projectId }: Props) => {
|
||||
const [cursor, setCursor] = useQueryState(
|
||||
'cursor',
|
||||
parseAsInteger.withDefault(0)
|
||||
parseAsInteger.withDefault(0),
|
||||
);
|
||||
const [search, setSearch] = useQueryState('search', {
|
||||
defaultValue: '',
|
||||
@@ -31,7 +31,7 @@ const Events = ({ projectId }: Props) => {
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ export type Coordinate = {
|
||||
|
||||
export function haversineDistance(
|
||||
coord1: Coordinate,
|
||||
coord2: Coordinate
|
||||
coord2: Coordinate,
|
||||
): number {
|
||||
const R = 6371; // Earth's radius in kilometers
|
||||
const lat1Rad = coord1.lat * (Math.PI / 180);
|
||||
@@ -25,7 +25,7 @@ export function haversineDistance(
|
||||
}
|
||||
|
||||
export function findFarthestPoints(
|
||||
coordinates: Coordinate[]
|
||||
coordinates: Coordinate[],
|
||||
): [Coordinate, Coordinate] {
|
||||
if (coordinates.length < 2) {
|
||||
throw new Error('At least two coordinates are required');
|
||||
@@ -80,12 +80,12 @@ function cross(o: Coordinate, a: Coordinate, b: Coordinate): number {
|
||||
|
||||
// convex hull
|
||||
export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] {
|
||||
coordinates = coordinates.sort(sortCoordinates);
|
||||
const sorted = coordinates.sort(sortCoordinates);
|
||||
|
||||
if (coordinates.length <= 3) return coordinates;
|
||||
if (sorted.length <= 3) return sorted;
|
||||
|
||||
const lower: Coordinate[] = [];
|
||||
for (const coord of coordinates) {
|
||||
for (const coord of sorted) {
|
||||
while (
|
||||
lower.length >= 2 &&
|
||||
cross(lower[lower.length - 2]!, lower[lower.length - 1]!, coord) <= 0
|
||||
@@ -99,15 +99,11 @@ export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] {
|
||||
for (let i = coordinates.length - 1; i >= 0; i--) {
|
||||
while (
|
||||
upper.length >= 2 &&
|
||||
cross(
|
||||
upper[upper.length - 2]!,
|
||||
upper[upper.length - 1]!,
|
||||
coordinates[i]!
|
||||
) <= 0
|
||||
cross(upper[upper.length - 2]!, upper[upper.length - 1]!, sorted[i]!) <= 0
|
||||
) {
|
||||
upper.pop();
|
||||
}
|
||||
upper.push(coordinates[i]!);
|
||||
upper.push(sorted[i]!);
|
||||
}
|
||||
|
||||
upper.pop();
|
||||
@@ -148,22 +144,22 @@ export function calculateCentroid(polygon: Coordinate[]): Coordinate {
|
||||
}
|
||||
|
||||
export function calculateGeographicMidpoint(
|
||||
coordinate: Coordinate[]
|
||||
coordinate: Coordinate[],
|
||||
): Coordinate {
|
||||
let minLat = Infinity,
|
||||
maxLat = -Infinity,
|
||||
minLong = Infinity,
|
||||
maxLong = -Infinity;
|
||||
let minLat = Number.POSITIVE_INFINITY;
|
||||
let maxLat = Number.NEGATIVE_INFINITY;
|
||||
let minLong = Number.POSITIVE_INFINITY;
|
||||
let maxLong = Number.NEGATIVE_INFINITY;
|
||||
|
||||
coordinate.forEach(({ lat, long }) => {
|
||||
for (const { lat, long } of coordinate) {
|
||||
if (lat < minLat) minLat = lat;
|
||||
if (lat > maxLat) maxLat = lat;
|
||||
if (long < minLong) minLong = long;
|
||||
if (long > maxLong) maxLong = long;
|
||||
});
|
||||
}
|
||||
|
||||
// Handling the wrap around the international date line
|
||||
let midLong;
|
||||
let midLong: number;
|
||||
if (maxLong > minLong) {
|
||||
midLong = (maxLong + minLong) / 2;
|
||||
} else {
|
||||
@@ -211,7 +207,7 @@ export function clusterCoordinates(coordinates: Coordinate[], radius = 25) {
|
||||
long: center.long + cur.long / cluster.members.length,
|
||||
};
|
||||
},
|
||||
{ lat: 0, long: 0 }
|
||||
{ lat: 0, long: 0 },
|
||||
);
|
||||
|
||||
clusters.push(cluster);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { subMinutes } from 'date-fns';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { chQuery, formatClickhouseDate, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, chQuery, formatClickhouseDate } from '@openpanel/db';
|
||||
|
||||
import type { Coordinate } from './coordinates';
|
||||
import Map from './map';
|
||||
@@ -11,7 +11,7 @@ type Props = {
|
||||
};
|
||||
const RealtimeMap = async ({ projectId }: Props) => {
|
||||
const res = await chQuery<Coordinate>(
|
||||
`SELECT DISTINCT city, longitude as long, latitude as lat FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(subMinutes(new Date(), 30))}' ORDER BY created_at DESC`
|
||||
`SELECT DISTINCT city, longitude as long, latitude as lat FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(subMinutes(new Date(), 30))}' ORDER BY created_at DESC`,
|
||||
);
|
||||
|
||||
return <Map markers={res} />;
|
||||
|
||||
@@ -49,7 +49,7 @@ export const getBoundingBox = (coordinates: Coordinate[]) => {
|
||||
|
||||
export const determineZoom = (
|
||||
bbox: ReturnType<typeof getBoundingBox>,
|
||||
aspectRatio = 1.0
|
||||
aspectRatio = 1.0,
|
||||
): number => {
|
||||
const latDiff = bbox.maxLat - bbox.minLat;
|
||||
const longDiff = bbox.maxLong - bbox.minLong;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useFullscreen } from '@/components/fullscreen-toggle';
|
||||
import { Tooltiper } from '@/components/ui/tooltip';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { bind } from 'bind-event-listener';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
} from './coordinates';
|
||||
import {
|
||||
CustomZoomableGroup,
|
||||
determineZoom,
|
||||
GEO_MAP_URL,
|
||||
determineZoom,
|
||||
getBoundingBox,
|
||||
useAnimatedState,
|
||||
} from './map.helpers';
|
||||
@@ -37,7 +37,7 @@ const Map = ({ markers }: Props) => {
|
||||
const showCenterMarker = false;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [size, setSize] = useState<{ width: number; height: number } | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
// const { markers, toggle } = useActiveMarkers(_m);
|
||||
@@ -50,7 +50,7 @@ const Map = ({ markers }: Props) => {
|
||||
const [zoom] = useAnimatedState(
|
||||
markers.length === 1
|
||||
? 20
|
||||
: determineZoom(boundingBox, size ? size?.height / size?.width : 1)
|
||||
: determineZoom(boundingBox, size ? size?.height / size?.width : 1),
|
||||
);
|
||||
|
||||
const [long] = useAnimatedState(center.long);
|
||||
@@ -99,7 +99,7 @@ const Map = ({ markers }: Props) => {
|
||||
<div
|
||||
className={cn(
|
||||
'fixed bottom-0 left-0 right-0 top-0',
|
||||
!isFullscreen && 'lg:left-72'
|
||||
!isFullscreen && 'lg:left-72',
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
@@ -142,7 +142,7 @@ const Map = ({ markers }: Props) => {
|
||||
)}
|
||||
{clusterCoordinates(markers).map((marker) => {
|
||||
const size = adjustSizeBasedOnZoom(
|
||||
calculateMarkerSize(marker.count)
|
||||
calculateMarkerSize(marker.count),
|
||||
);
|
||||
const coordinates: [number, number] = [
|
||||
marker.center.long,
|
||||
|
||||
@@ -11,7 +11,7 @@ const useActiveMarkers = (initialMarkers: Coordinate[]) => {
|
||||
// Cut the array in half randomly to simulate changes in active markers
|
||||
const selected = shuffled.slice(
|
||||
0,
|
||||
Math.floor(Math.random() * shuffled.length) + 1
|
||||
Math.floor(Math.random() * shuffled.length) + 1,
|
||||
);
|
||||
setActiveMarkers(selected);
|
||||
}, [activeMarkers]);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Suspense } from 'react';
|
||||
import {
|
||||
Fullscreen,
|
||||
FullscreenClose,
|
||||
FullscreenOpen,
|
||||
} from '@/components/fullscreen-toggle';
|
||||
import { ReportChart } from '@/components/report-chart';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import RealtimeMap from './map';
|
||||
import RealtimeLiveEventsServer from './realtime-live-events';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import { getEvents, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, getEvents } from '@openpanel/db';
|
||||
|
||||
import LiveEvents from './live-events';
|
||||
|
||||
@@ -13,7 +13,7 @@ const RealtimeLiveEventsServer = async ({ projectId, limit = 30 }: Props) => {
|
||||
`SELECT * FROM ${TABLE_NAMES.events} WHERE created_at > now() - INTERVAL 2 HOUR AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit}`,
|
||||
{
|
||||
profile: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
return <LiveEvents events={events} projectId={projectId} limit={limit} />;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { EventListItem } from '@/components/events/event-list-item';
|
||||
import useWS from '@/hooks/useWS';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db';
|
||||
|
||||
@@ -19,7 +19,7 @@ const RealtimeLiveEvents = ({ events, projectId, limit }: Props) => {
|
||||
`/live/events/${projectId}`,
|
||||
(event) => {
|
||||
setState((p) => [event, ...p].slice(0, limit));
|
||||
}
|
||||
},
|
||||
);
|
||||
return (
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
|
||||
@@ -71,18 +71,16 @@ export function RealtimeLiveHistogram({
|
||||
const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0;
|
||||
|
||||
if (res.isInitialLoading || countRes.isInitialLoading || liveCount === 0) {
|
||||
// prettier-ignore
|
||||
const staticArray = [
|
||||
10, 25, 30, 45, 20, 5, 55, 18, 40, 12,
|
||||
50, 35, 8, 22, 38, 42, 15, 28, 52, 5,
|
||||
48, 14, 32, 58, 7, 19, 33, 56, 24, 5
|
||||
10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 50, 35, 8, 22, 38, 42, 15, 28, 52,
|
||||
5, 48, 14, 32, 58, 7, 19, 33, 56, 24, 5,
|
||||
];
|
||||
|
||||
return (
|
||||
<Wrapper count={0}>
|
||||
{staticArray.map((percent, i) => (
|
||||
<div
|
||||
key={i}
|
||||
key={i as number}
|
||||
className="flex-1 animate-pulse rounded bg-def-200"
|
||||
style={{ height: `${percent}%` }}
|
||||
/>
|
||||
@@ -104,7 +102,7 @@ export function RealtimeLiveHistogram({
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 rounded transition-all ease-in-out hover:scale-110',
|
||||
minute.count === 0 ? 'bg-def-200' : 'bg-highlight'
|
||||
minute.count === 0 ? 'bg-def-200' : 'bg-highlight',
|
||||
)}
|
||||
style={{
|
||||
height:
|
||||
|
||||
@@ -27,7 +27,7 @@ const RealtimeReloader = ({ projectId }: Props) => {
|
||||
maxWait: 15000,
|
||||
delay: 15000,
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header';
|
||||
import { ReportChart } from '@/components/report-chart';
|
||||
import { ReportChartType } from '@/components/report/ReportChartType';
|
||||
@@ -25,6 +24,7 @@ import { useDispatch, useSelector } from '@/redux';
|
||||
import { bind } from 'bind-event-listener';
|
||||
import { endOfDay, startOfDay } from 'date-fns';
|
||||
import { GanttChartSquareIcon } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import type { IServiceReport } from '@openpanel/db';
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ const Chart = ({ data }: Props) => {
|
||||
offset="0%"
|
||||
stopColor={getChartColor(0)}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={getChartColor(0)}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -70,7 +70,7 @@ const Chart = ({ data }: Props) => {
|
||||
dataKey="users"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={2}
|
||||
fill={`url(#bg)`}
|
||||
fill={'url(#bg)'}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<XAxis
|
||||
|
||||
@@ -72,36 +72,36 @@ const Chart = ({ data }: Props) => {
|
||||
offset="0%"
|
||||
stopColor={getChartColor(0)}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={getChartColor(0)}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="wau" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={getChartColor(1)}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={getChartColor(1)}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient id="mau" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={getChartColor(2)}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={getChartColor(2)}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -111,21 +111,21 @@ const Chart = ({ data }: Props) => {
|
||||
dataKey="dau"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={2}
|
||||
fill={`url(#dau)`}
|
||||
fill={'url(#dau)'}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Area
|
||||
dataKey="wau"
|
||||
stroke={getChartColor(1)}
|
||||
strokeWidth={2}
|
||||
fill={`url(#wau)`}
|
||||
fill={'url(#wau)'}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mau"
|
||||
stroke={getChartColor(2)}
|
||||
strokeWidth={2}
|
||||
fill={`url(#mau)`}
|
||||
fill={'url(#mau)'}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<XAxis {...xAxisProps} dataKey="date" />
|
||||
|
||||
@@ -70,12 +70,12 @@ const Chart = ({ data }: Props) => {
|
||||
offset="0%"
|
||||
stopColor={getChartColor(0)}
|
||||
stopOpacity={0.8}
|
||||
></stop>
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={getChartColor(0)}
|
||||
stopOpacity={0.1}
|
||||
></stop>
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
@@ -85,7 +85,7 @@ const Chart = ({ data }: Props) => {
|
||||
dataKey="retention"
|
||||
stroke={getChartColor(0)}
|
||||
strokeWidth={2}
|
||||
fill={`url(#bg)`}
|
||||
fill={'url(#bg)'}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<XAxis
|
||||
|
||||
@@ -17,7 +17,7 @@ const Cell = ({ value, ratio }: { value: number; ratio: number }) => {
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-highlight"
|
||||
style={{ opacity: ratio }}
|
||||
></div>
|
||||
/>
|
||||
<div className="relative z-10">{value}</div>
|
||||
</td>
|
||||
);
|
||||
@@ -39,7 +39,7 @@ const WeeklyCohortsServer = async ({ projectId }: Props) => {
|
||||
row.period_7,
|
||||
row.period_8,
|
||||
row.period_9,
|
||||
])
|
||||
]),
|
||||
);
|
||||
|
||||
const calculateRatio = (currentValue: number) =>
|
||||
@@ -47,7 +47,7 @@ const WeeklyCohortsServer = async ({ projectId }: Props) => {
|
||||
? 0
|
||||
: Math.max(
|
||||
0.1,
|
||||
Math.min(1, (currentValue - minValue) / (maxValue - minValue))
|
||||
Math.min(1, (currentValue - minValue) / (maxValue - minValue)),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import {
|
||||
closeSheet,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
closeSheet,
|
||||
} from '@/components/ui/sheet';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { api } from '@/trpc/client';
|
||||
|
||||
@@ -52,7 +52,7 @@ export default async function Page({
|
||||
}
|
||||
|
||||
const member = organization.members.find(
|
||||
(member) => member.userId === session.userId
|
||||
(member) => member.userId === session.userId,
|
||||
);
|
||||
|
||||
const hasAccess = member?.role === 'org:admin';
|
||||
@@ -69,13 +69,13 @@ export default async function Page({
|
||||
return (
|
||||
<Padding>
|
||||
<PageTabs className="mb-4">
|
||||
<PageTabsLink href={`?tab=org`} isActive={tab === 'org'}>
|
||||
<PageTabsLink href={'?tab=org'} isActive={tab === 'org'}>
|
||||
Organization
|
||||
</PageTabsLink>
|
||||
<PageTabsLink href={`?tab=members`} isActive={tab === 'members'}>
|
||||
<PageTabsLink href={'?tab=members'} isActive={tab === 'members'}>
|
||||
Members
|
||||
</PageTabsLink>
|
||||
<PageTabsLink href={`?tab=invites`} isActive={tab === 'invites'}>
|
||||
<PageTabsLink href={'?tab=invites'} isActive={tab === 'invites'}>
|
||||
Invites
|
||||
</PageTabsLink>
|
||||
</PageTabs>
|
||||
|
||||
@@ -10,9 +10,7 @@ export function Logout() {
|
||||
<span className="title">Sad part</span>
|
||||
</WidgetHead>
|
||||
<WidgetBody>
|
||||
<p className="mb-4">
|
||||
Sometimes you need to go. See you next time
|
||||
</p>
|
||||
<p className="mb-4">Sometimes you need to go. See you next time</p>
|
||||
<SignOutButton />
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
|
||||
<Accordion type="single" collapsible className="-mx-4">
|
||||
{projects.map((project) => {
|
||||
const pClients = clients.filter(
|
||||
(client) => client.projectId === project.id
|
||||
(client) => client.projectId === project.id,
|
||||
);
|
||||
return (
|
||||
<AccordionItem
|
||||
@@ -61,7 +61,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
|
||||
: 'No clients created yet'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mx-4"></div>
|
||||
<div className="mx-4" />
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4">
|
||||
<ProjectActions {...project} />
|
||||
@@ -91,6 +91,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
pushModal('AddClient', {
|
||||
projectId: project.id,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { pushModal, useOnPushModal } from '@/modals';
|
||||
import { useUser } from '@clerk/nextjs';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useOpenPanel } from '@openpanel/nextjs';
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function SideEffects() {
|
||||
const { user } = useUser();
|
||||
const accountAgeInDays = differenceInDays(
|
||||
new Date(),
|
||||
user?.createdAt || new Date()
|
||||
user?.createdAt || new Date(),
|
||||
);
|
||||
useOnPushModal('Testimonial', (open) => {
|
||||
if (!open) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { EventListItem } from '@/components/events/event-list-item';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { IServiceEventMinimal } from '@openpanel/db';
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ const Page = ({ children }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 hidden md:grid md:grid-cols-[30vw_1fr] lg:grid-cols-[30vw_1fr]">
|
||||
<div className=""></div>
|
||||
<div className="border-l border-border bg-card"></div>
|
||||
<div className="" />
|
||||
<div className="border-l border-border bg-card" />
|
||||
</div>
|
||||
<div className="relative min-h-screen bg-card md:bg-transparent">
|
||||
<FullWidthNavbar>
|
||||
|
||||
@@ -21,6 +21,7 @@ const ConnectApp = ({ client }: Props) => {
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
{frameworks.app.map((framework) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
pushModal('Instructions', {
|
||||
framework,
|
||||
|
||||
@@ -21,6 +21,7 @@ const ConnectBackend = ({ client }: Props) => {
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
{frameworks.backend.map((framework) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
pushModal('Instructions', {
|
||||
framework,
|
||||
|
||||
@@ -21,6 +21,7 @@ const ConnectWeb = ({ client }: Props) => {
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
{frameworks.website.map((framework) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
pushModal('Instructions', {
|
||||
framework,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import useWS from '@/hooks/useWS';
|
||||
import { pushModal } from '@/modals';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { timeAgo } from '@/utils/date';
|
||||
import { CheckCircle2Icon, CheckIcon, Loader2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type {
|
||||
IServiceClient,
|
||||
@@ -28,7 +28,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
|
||||
(data) => {
|
||||
setEvents((prev) => [...prev, data]);
|
||||
onVerified(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const isConnected = events.length > 0;
|
||||
@@ -70,7 +70,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
|
||||
'my-6 flex gap-6 rounded-xl p-4 md:p-6',
|
||||
isConnected
|
||||
? 'bg-emerald-100 dark:bg-emerald-700'
|
||||
: 'bg-blue-100 dark:bg-blue-700'
|
||||
: 'bg-blue-100 dark:bg-blue-700',
|
||||
)}
|
||||
>
|
||||
{renderIcon()}
|
||||
@@ -87,7 +87,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
{events.slice(-5).map((event, index) => (
|
||||
<div key={index} className="flex items-center gap-2 ">
|
||||
<div key={event.id} className="flex items-center gap-2 ">
|
||||
<CheckIcon size={14} />{' '}
|
||||
<span className="font-medium">{event.name}</span>{' '}
|
||||
<span className="ml-auto text-emerald-800">
|
||||
@@ -108,6 +108,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
You can{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
pushModal('OnboardingTroubleshoot', {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { LinkButton } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { IServiceEvent, IServiceProjectWithClients } from '@openpanel/db';
|
||||
|
||||
@@ -84,7 +84,7 @@ const Verify = ({ project, events }: Props) => {
|
||||
size="lg"
|
||||
className={cn(
|
||||
'min-w-28 self-start',
|
||||
!verified && 'pointer-events-none select-none opacity-20'
|
||||
!verified && 'pointer-events-none select-none opacity-20',
|
||||
)}
|
||||
>
|
||||
Your dashboard
|
||||
|
||||
@@ -2,10 +2,10 @@ import { cookies } from 'next/headers';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import {
|
||||
TABLE_NAMES,
|
||||
getCurrentOrganizations,
|
||||
getEvents,
|
||||
getProjectWithClients,
|
||||
TABLE_NAMES,
|
||||
} from '@openpanel/db';
|
||||
|
||||
import OnboardingVerify from './onboarding-verify';
|
||||
@@ -25,7 +25,7 @@ const Verify = async ({ params: { projectId } }: Props) => {
|
||||
const [project, events] = await Promise.all([
|
||||
await getProjectWithClients(projectId),
|
||||
getEvents(
|
||||
`SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} LIMIT 100`
|
||||
`SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} LIMIT 100`,
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import AnimateHeight from '@/components/animate-height';
|
||||
import { ButtonContainer } from '@/components/button-container';
|
||||
import { CheckboxItem } from '@/components/forms/checkbox-item';
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
SmartphoneIcon,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
@@ -185,7 +185,7 @@ const Tracking = ({
|
||||
}
|
||||
return `https://${trimmed}`;
|
||||
})
|
||||
.join(',')
|
||||
.join(','),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { showConfirm } from '@/modals';
|
||||
import { api } from '@/trpc/client';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { ChevronLastIcon } from 'lucide-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const SkipOnboarding = () => {
|
||||
const router = useRouter();
|
||||
@@ -20,6 +20,7 @@ const SkipOnboarding = () => {
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (res.data?.canSkip && res.data?.url) {
|
||||
router.push(res.data.url);
|
||||
|
||||
@@ -39,7 +39,7 @@ function useSteps(path: string) {
|
||||
];
|
||||
|
||||
const matchIndex = steps.findLastIndex((step) =>
|
||||
path.match(new RegExp(step.match))
|
||||
path.match(new RegExp(step.match)),
|
||||
);
|
||||
|
||||
return steps.map((step, index) => {
|
||||
@@ -59,17 +59,17 @@ const Steps = ({ className }: Props) => {
|
||||
const currentIndex = steps.findIndex((i) => i.status === 'current');
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute bottom-4 left-4 top-4 w-px bg-def-200"></div>
|
||||
<div className="absolute bottom-4 left-4 top-4 w-px bg-def-200" />
|
||||
<div
|
||||
className="absolute left-4 top-4 w-px bg-highlight"
|
||||
style={{
|
||||
height: `calc(${((currentIndex + 1) / steps.length) * 100}% - 3.5rem)`,
|
||||
}}
|
||||
></div>
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex gap-4 overflow-hidden md:-ml-3 md:flex-col md:gap-8',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{steps.map((step, index) => (
|
||||
@@ -80,32 +80,31 @@ const Steps = ({ className }: Props) => {
|
||||
'rounded-xl border border-border bg-card',
|
||||
step.status === 'completed' &&
|
||||
index !== currentIndex - 1 &&
|
||||
'max-md:hidden'
|
||||
'max-md:hidden',
|
||||
)}
|
||||
key={step.name}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white'
|
||||
'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-0 rounded-full bg-highlight',
|
||||
step.status === 'pending' && 'bg-def-400'
|
||||
step.status === 'pending' && 'bg-def-400',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
{step.status === 'current' && (
|
||||
<div className="absolute inset-1 z-0 animate-ping-slow rounded-full bg-highlight"></div>
|
||||
<div className="absolute inset-1 z-0 animate-ping-slow rounded-full bg-highlight" />
|
||||
)}
|
||||
<div className="relative">
|
||||
{step.status === 'completed' && <CheckCheckIcon size={14} />}
|
||||
{/* {step.status === 'current' && (
|
||||
<ArrowRightCircleIcon size={14} />
|
||||
)} */}
|
||||
{(step.status === 'pending' || step.status === 'current') && (
|
||||
<>{index + 1}</>
|
||||
)}
|
||||
{(step.status === 'pending' || step.status === 'current') &&
|
||||
index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function POST(request: Request) {
|
||||
if (!email) {
|
||||
return Response.json(
|
||||
{ message: 'No email address found' },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import NextTopLoader from 'nextjs-toploader';
|
||||
import Providers from './providers';
|
||||
|
||||
import '@/styles/globals.css';
|
||||
import '/node_modules/flag-icons/css/flag-icons.min.css';
|
||||
import 'flag-icons/css/flag-icons.min.css';
|
||||
|
||||
import { GeistMono } from 'geist/font/mono';
|
||||
import { GeistSans } from 'geist/font/sans';
|
||||
@@ -31,7 +31,7 @@ export default function RootLayout({
|
||||
className={cn(
|
||||
'grainy min-h-screen bg-def-100 font-sans text-base antialiased',
|
||||
GeistSans.variable,
|
||||
GeistMono.variable
|
||||
GeistMono.variable,
|
||||
)}
|
||||
>
|
||||
<NextTopLoader
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { ModalProvider } from '@/modals';
|
||||
import type { AppStore } from '@/redux';
|
||||
@@ -10,6 +9,8 @@ import { ClerkProvider, useAuth } from '@clerk/nextjs';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { httpLink } from '@trpc/client';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import type React from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { Toaster } from 'sonner';
|
||||
import superjson from 'superjson';
|
||||
@@ -28,7 +29,7 @@ function AllProviders({ children }: { children: React.ReactNode }) {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
const [trpcClient] = useState(() =>
|
||||
api.createClient({
|
||||
@@ -47,7 +48,7 @@ function AllProviders({ children }: { children: React.ReactNode }) {
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const storeRef = useRef<AppStore>();
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Card({ children, hover, className }: CardProps) {
|
||||
className={cn(
|
||||
'card relative',
|
||||
hover && 'transition-all hover:-translate-y-0.5',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -51,23 +51,23 @@ export function ChartSSR({
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={`gradient`}
|
||||
id={'gradient'}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0%" stopColor={'blue'} stopOpacity={0.2}></stop>
|
||||
<stop offset="50%" stopColor={'blue'} stopOpacity={0.05}></stop>
|
||||
<stop offset="100%" stopColor={'blue'} stopOpacity={0}></stop>
|
||||
<stop offset="0%" stopColor={'blue'} stopOpacity={0.2} />
|
||||
<stop offset="50%" stopColor={'blue'} stopOpacity={0.05} />
|
||||
<stop offset="100%" stopColor={'blue'} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* Gradient area */}
|
||||
{pathArea && (
|
||||
<path
|
||||
d={pathArea}
|
||||
fill={`url(#gradient)`}
|
||||
fill={'url(#gradient)'}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -39,6 +39,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
||||
target="_blank"
|
||||
href="https://docs.openpanel.dev"
|
||||
className="underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
|
||||
@@ -8,7 +8,7 @@ export function ColorSquare({ children, className }: ColorSquareProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-sm font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { MoonIcon, SunIcon } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import * as React from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
||||
@@ -47,7 +47,7 @@ export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
header.getContext(),
|
||||
)}
|
||||
</GridCell>
|
||||
))}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Dot({ className, size = 8, animated }: DotProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
filterCn(['bg-', 'animate-', 'group-hover/row'], className)
|
||||
filterCn(['bg-', 'animate-', 'group-hover/row'], className),
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
@@ -30,14 +30,14 @@ export function Dot({ className, size = 8, animated }: DotProps) {
|
||||
className={cn(
|
||||
'absolute !m-0 rounded-full',
|
||||
animated !== false && 'animate-ping',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute !m-0 rounded-full',
|
||||
filterCn(['animate-', 'group-hover/row'], className)
|
||||
filterCn(['animate-', 'group-hover/row'], className),
|
||||
)}
|
||||
style={style}
|
||||
/>
|
||||
|
||||
@@ -51,6 +51,7 @@ export function EventListItem(props: EventListItemProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isMinimal) {
|
||||
pushModal('EventDetails', {
|
||||
@@ -61,7 +62,7 @@ export function EventListItem(props: EventListItemProps) {
|
||||
className={cn(
|
||||
'card hover:bg-light-background flex w-full items-center justify-between rounded-lg p-4 transition-colors',
|
||||
meta?.conversion &&
|
||||
`bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700`
|
||||
`bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700`,
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -38,6 +38,7 @@ export default function EventListener({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
counter.set(0);
|
||||
onRefresh();
|
||||
@@ -47,14 +48,14 @@ export default function EventListener({
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all'
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all'
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
|
||||
)}
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
{counter.debounced === 0 ? (
|
||||
'Listening'
|
||||
|
||||
@@ -52,6 +52,7 @@ export function useColumns() {
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipComplete content="Click to edit" side="left">
|
||||
<button
|
||||
type="button"
|
||||
className="transition-transform hover:scale-105"
|
||||
onClick={() => {
|
||||
pushModal('EditEvent', {
|
||||
@@ -68,6 +69,7 @@ export function useColumns() {
|
||||
</TooltipComplete>
|
||||
<span className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
pushModal('EventDetails', {
|
||||
id: row.original.id,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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';
|
||||
@@ -7,6 +6,7 @@ import { TableSkeleton } from '@/components/ui/table';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
import { column } from 'mathjs';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import type { IServiceEvent } from '@openpanel/db';
|
||||
|
||||
@@ -55,7 +55,7 @@ export const EventsTable = ({ query, ...props }: Props) => {
|
||||
className="mt-2"
|
||||
setCursor={props.setCursor}
|
||||
cursor={props.cursor}
|
||||
count={Infinity}
|
||||
count={Number.POSITIVE_INFINITY}
|
||||
take={50}
|
||||
loading={isFetching}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { slug } from '@/utils/slug';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { ControllerRenderProps } from 'react-hook-form';
|
||||
|
||||
import { Switch } from '../ui/switch';
|
||||
@@ -17,7 +17,7 @@ type Props = {
|
||||
export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
|
||||
(
|
||||
{ label, description, Icon, children, onChange, value, disabled, error },
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const id = slug(label);
|
||||
return (
|
||||
@@ -25,7 +25,7 @@ export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
|
||||
<label
|
||||
className={cn(
|
||||
'flex items-center gap-4 px-4 py-6 transition-colors hover:bg-def-200',
|
||||
disabled && 'cursor-not-allowed opacity-50'
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
htmlFor={id}
|
||||
>
|
||||
@@ -48,7 +48,7 @@ export const CheckboxItem = forwardRef<HTMLButtonElement, Props>(
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
CheckboxItem.displayName = 'CheckboxItem';
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
const CopyInput = ({ label, value, className }: Props) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn('w-full text-left', className)}
|
||||
onClick={() => clipboard(value)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { BanIcon, InfoIcon } from 'lucide-react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { Input } from '../ui/input';
|
||||
import type { InputProps } from '../ui/input';
|
||||
@@ -57,7 +57,7 @@ export const InputWithLabel = forwardRef<HTMLInputElement, InputWithLabelProps>(
|
||||
<Input ref={ref} id={props.label} {...props} />
|
||||
</WithLabel>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
InputWithLabel.displayName = 'InputWithLabel';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Based on Christin Alares tag input component (https://github.com/christianalares/seventy-seven)
|
||||
|
||||
import type { ElementRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useAnimate } from 'framer-motion';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import type { ElementRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
placeholder: string;
|
||||
@@ -47,7 +47,7 @@ const TagInput = ({
|
||||
e.preventDefault();
|
||||
|
||||
const tagAlreadyExists = value.some(
|
||||
(tag) => tag.toLowerCase() === inputValue.toLowerCase()
|
||||
(tag) => tag.toLowerCase() === inputValue.toLowerCase(),
|
||||
);
|
||||
|
||||
if (inputValue) {
|
||||
@@ -59,7 +59,7 @@ const TagInput = ({
|
||||
},
|
||||
{
|
||||
duration: 0.3,
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ const TagInput = ({
|
||||
ref={scope}
|
||||
className={cn(
|
||||
'inline-flex w-full flex-wrap items-center gap-2 rounded-md border border-input p-1 px-3 ring-offset-background has-[input:focus]:ring-2 has-[input:focus]:ring-ring has-[input:focus]:ring-offset-1',
|
||||
!!error && 'border-destructive'
|
||||
!!error && 'border-destructive',
|
||||
)}
|
||||
>
|
||||
{value.map((tag, i) => {
|
||||
@@ -116,7 +116,7 @@ const TagInput = ({
|
||||
isMarkedForDeletion &&
|
||||
i === value.length - 1 &&
|
||||
'bg-destructive-foreground ring-2 ring-destructive/50 ring-offset-1',
|
||||
isCreating && 'opacity-60'
|
||||
isCreating && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
{renderTag ? renderTag(tag) : tag}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function FullPageEmptyState({
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center p-4 text-center',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full max-w-xl flex-col items-center justify-center p-8">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { bind } from 'bind-event-listener';
|
||||
import { ChevronLeftIcon, FullscreenIcon } from 'lucide-react';
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useDebounce } from 'usehooks-ts';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
@@ -21,7 +21,7 @@ export const useFullscreen = () =>
|
||||
parseAsBoolean.withDefault(false).withOptions({
|
||||
history: 'push',
|
||||
clearOnDefault: true,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
export const Fullscreen = (props: Props) => {
|
||||
@@ -29,7 +29,7 @@ export const Fullscreen = (props: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isFullscreen && 'fixed inset-0 z-50 overflow-auto bg-def-200'
|
||||
isFullscreen && 'fixed inset-0 z-50 overflow-auto bg-def-200',
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
@@ -92,10 +92,11 @@ export const FullscreenClose = () => {
|
||||
<div className="fixed bottom-0 top-0 z-50 flex items-center">
|
||||
<Tooltiper content="Exit full screen" asChild>
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex h-20 w-20 -translate-x-20 items-center justify-center rounded-full bg-foreground transition-transform',
|
||||
visible && isFullscreenDebounced && '-translate-x-10'
|
||||
visible && isFullscreenDebounced && '-translate-x-10',
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsFullscreen(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type React from 'react';
|
||||
|
||||
export const Grid: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement> & { columns: number }
|
||||
@@ -63,7 +63,7 @@ export const GridCell: React.FC<
|
||||
'flex min-h-12 items-center whitespace-nowrap px-4 align-middle shadow-[0_0_0_0.5px] shadow-border',
|
||||
isHeader && 'h-10 bg-def-100 font-semibold text-muted-foreground',
|
||||
colSpan && `col-span-${colSpan}`,
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -79,7 +79,7 @@ export const GridRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
<div
|
||||
className={cn(
|
||||
'contents transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function OriginFilter() {
|
||||
},
|
||||
{
|
||||
staleTime: 1000 * 60 * 60,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
@@ -32,7 +32,7 @@ export function OriginFilter() {
|
||||
variant="outline"
|
||||
icon={GlobeIcon}
|
||||
className={cn(
|
||||
originFilter?.value.includes(item.origin) && 'border-foreground'
|
||||
originFilter?.value.includes(item.origin) && 'border-foreground',
|
||||
)}
|
||||
onClick={() => setFilter('origin', [item.origin], 'is')}
|
||||
>
|
||||
|
||||
@@ -125,7 +125,7 @@ export function FilterOptionEvent({
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator
|
||||
operator: IChartEventFilterOperator,
|
||||
) => void;
|
||||
}) {
|
||||
const { interval, range } = useOverviewOptions();
|
||||
@@ -172,7 +172,7 @@ export function FilterOptionProfile({
|
||||
setFilter: (
|
||||
name: string,
|
||||
value: IChartEventFilterValue,
|
||||
operator: IChartEventFilterOperator
|
||||
operator: IChartEventFilterOperator,
|
||||
) => void;
|
||||
}) {
|
||||
const values = useProfileValues(projectId, filter.name);
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { OverviewFiltersDrawerContentProps } from './overview-filters-drawe
|
||||
import { OverviewFiltersDrawerContent } from './overview-filters-drawer-content';
|
||||
|
||||
export function OverviewFiltersDrawer(
|
||||
props: OverviewFiltersDrawerContentProps
|
||||
props: OverviewFiltersDrawerContentProps,
|
||||
) {
|
||||
return (
|
||||
<Sheet>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { TooltipComplete } from '@/components/tooltip-complete';
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -12,6 +11,7 @@ import useWS from '@/hooks/useWS';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface LiveCounterProps {
|
||||
@@ -34,7 +34,7 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
|
||||
const lastRefresh = useRef(Date.now());
|
||||
|
||||
useWS<number>(`/live/visitors/${projectId}`, (value) => {
|
||||
if (!isNaN(value)) {
|
||||
if (!Number.isNaN(value)) {
|
||||
counter.set(value);
|
||||
if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) {
|
||||
lastRefresh.current = Date.now();
|
||||
@@ -57,13 +57,13 @@ export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) {
|
||||
<div
|
||||
className={cn(
|
||||
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
|
||||
counter.debounced === 0 && 'bg-destructive opacity-0'
|
||||
counter.debounced === 0 && 'bg-destructive opacity-0',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
|
||||
counter.debounced === 0 && 'bg-destructive'
|
||||
counter.debounced === 0 && 'bg-destructive',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { BarChartIcon, LineChartIcon } from 'lucide-react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
|
||||
@@ -69,18 +69,16 @@ export function OverviewLiveHistogram({
|
||||
const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0;
|
||||
|
||||
if (res.isInitialLoading || countRes.isInitialLoading) {
|
||||
// prettier-ignore
|
||||
const staticArray = [
|
||||
10, 25, 30, 45, 20, 5, 55, 18, 40, 12,
|
||||
50, 35, 8, 22, 38, 42, 15, 28, 52, 5,
|
||||
48, 14, 32, 58, 7, 19, 33, 56, 24, 5
|
||||
10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 50, 35, 8, 22, 38, 42, 15, 28, 52,
|
||||
5, 48, 14, 32, 58, 7, 19, 33, 56, 24, 5,
|
||||
];
|
||||
|
||||
return (
|
||||
<Wrapper count={0}>
|
||||
{staticArray.map((percent, i) => (
|
||||
<div
|
||||
key={i}
|
||||
key={i as number}
|
||||
className="flex-1 animate-pulse rounded-t bg-def-200"
|
||||
style={{ height: `${percent}%` }}
|
||||
/>
|
||||
@@ -102,7 +100,7 @@ export function OverviewLiveHistogram({
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 rounded-t transition-all ease-in-out hover:scale-110',
|
||||
minute.count === 0 ? 'bg-def-200' : 'bg-highlight'
|
||||
minute.count === 0 ? 'bg-def-200' : 'bg-highlight',
|
||||
)}
|
||||
style={{
|
||||
height:
|
||||
|
||||
@@ -197,10 +197,11 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
<div className="card mb-2 grid grid-cols-4 overflow-hidden rounded-md">
|
||||
{reports.map((report, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
key={report.id}
|
||||
className={cn(
|
||||
'col-span-2 flex-1 shadow-[0_0_0_0.5px] shadow-border md:col-span-1',
|
||||
index === metric && 'bg-def-100'
|
||||
index === metric && 'bg-def-100',
|
||||
)}
|
||||
onClick={() => {
|
||||
setMetric(index);
|
||||
@@ -211,7 +212,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
'col-span-4 min-h-16 flex-1 p-4 pb-0 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-2'
|
||||
'col-span-4 min-h-16 flex-1 p-4 pb-0 shadow-[0_0_0_0.5px] shadow-border max-md:row-start-1 md:col-span-2',
|
||||
)}
|
||||
>
|
||||
<OverviewLiveHistogram projectId={projectId} />
|
||||
|
||||
@@ -31,8 +31,8 @@ export function OverviewShare({ data }: OverviewShareProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button icon={data && data.public ? Globe2Icon : LockIcon} responsive>
|
||||
{data && data.public ? 'Public' : 'Private'}
|
||||
<Button icon={data?.public ? Globe2Icon : LockIcon} responsive>
|
||||
{data?.public ? 'Public' : 'Private'}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { api } from '@/trpc/client';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Pagination } from '../pagination';
|
||||
import { Tooltiper } from '../ui/tooltip';
|
||||
@@ -23,7 +23,7 @@ const OverviewTopBots = ({ projectId }: Props) => {
|
||||
{ projectId, cursor },
|
||||
{
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
const data = res.data?.data ?? [];
|
||||
const count = res.data?.count ?? 0;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
@@ -295,6 +295,7 @@ export default function OverviewTopDevices({
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ReportChart } from '@/components/report-chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
@@ -155,6 +155,7 @@ export default function OverviewTopEvents({
|
||||
.filter((item) => item.hide !== true)
|
||||
.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { getCountry } from '@/translations/countries';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
@@ -158,6 +158,7 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ExternalLinkIcon, FilterIcon, Globe2Icon } from 'lucide-react';
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
@@ -180,6 +180,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
@@ -225,7 +226,7 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
||||
<WidgetFooter>
|
||||
<OverviewDetailsButton chart={widget.chart.report} />
|
||||
<OverviewChartToggle {...{ chartType, setChartType }} />
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex-1" />
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { IChartType } from '@openpanel/validation';
|
||||
|
||||
@@ -301,6 +301,7 @@ export default function OverviewTopSources({
|
||||
<WidgetButtons>
|
||||
{widgets.map((w) => (
|
||||
<button
|
||||
type="button"
|
||||
key={w.key}
|
||||
onClick={() => setWidget(w.key)}
|
||||
className={cn(w.key === widget.key && 'active')}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { Children, useEffect, useRef, useState } from 'react';
|
||||
import { useThrottle } from '@/hooks/useThrottle';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||
import { last } from 'ramda';
|
||||
import { Children, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -21,7 +21,7 @@ export function WidgetHead({ className, ...props }: WidgetHeadProps) {
|
||||
<WidgetHeadBase
|
||||
className={cn(
|
||||
'flex flex-col rounded-t-xl p-0 [&_.title]:flex [&_.title]:items-center [&_.title]:justify-between [&_.title]:p-4 [&_.title]:font-semibold',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -43,11 +43,11 @@ export function WidgetButtons({
|
||||
if (sizes.current.length === 0) {
|
||||
// Get buttons
|
||||
const buttons: HTMLButtonElement[] = Array.from(
|
||||
container.current.querySelectorAll(`button`)
|
||||
container.current.querySelectorAll('button'),
|
||||
);
|
||||
// Get sizes and cache them
|
||||
sizes.current = buttons.map(
|
||||
(button) => Math.ceil(button.offsetWidth) + gap
|
||||
(button) => Math.ceil(button.offsetWidth) + gap,
|
||||
);
|
||||
}
|
||||
const containerWidth = container.current.offsetWidth;
|
||||
@@ -62,7 +62,7 @@ export function WidgetButtons({
|
||||
}
|
||||
return { index, size: acc.size + size };
|
||||
},
|
||||
{ index: 0, size: 0 }
|
||||
{ index: 0, size: 0 },
|
||||
);
|
||||
|
||||
setSlice(res.index);
|
||||
@@ -86,7 +86,7 @@ export function WidgetButtons({
|
||||
ref={container}
|
||||
className={cn(
|
||||
'-mb-px -mt-2 flex flex-wrap justify-start self-stretch px-4 transition-opacity [&_button.active]:border-b-2 [&_button.active]:border-black [&_button.active]:opacity-100 dark:[&_button.active]:border-white [&_button]:whitespace-nowrap [&_button]:py-1 [&_button]:text-sm [&_button]:opacity-50',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
style={{ gap }}
|
||||
{...props}
|
||||
@@ -96,7 +96,7 @@ export function WidgetButtons({
|
||||
<div
|
||||
className={cn(
|
||||
'flex [&_button]:leading-normal',
|
||||
slice < index ? hidden : 'opacity-100'
|
||||
slice < index ? hidden : 'opacity-100',
|
||||
)}
|
||||
>
|
||||
{child}
|
||||
@@ -106,9 +106,10 @@ export function WidgetButtons({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex select-none items-center gap-1',
|
||||
sizes.current.length - 1 === slice ? hidden : 'opacity-50'
|
||||
sizes.current.length - 1 === slice ? hidden : 'opacity-50',
|
||||
)}
|
||||
>
|
||||
More <ChevronsUpDownIcon size={12} />
|
||||
@@ -138,7 +139,7 @@ export function WidgetFooter({
|
||||
<div
|
||||
className={cn(
|
||||
'flex rounded-b-md border-t bg-def-100 p-2 py-1',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -19,17 +19,17 @@ const nuqsOptions = { history: 'push' } as const;
|
||||
export function useOverviewOptions() {
|
||||
const [startDate, setStartDate] = useQueryState(
|
||||
'start',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
parseAsString.withOptions(nuqsOptions),
|
||||
);
|
||||
const [endDate, setEndDate] = useQueryState(
|
||||
'end',
|
||||
parseAsString.withOptions(nuqsOptions)
|
||||
parseAsString.withOptions(nuqsOptions),
|
||||
);
|
||||
const [range, setRange] = useQueryState(
|
||||
'range',
|
||||
parseAsStringEnum(mapKeys(timeWindows))
|
||||
.withDefault('7d')
|
||||
.withOptions(nuqsOptions)
|
||||
.withOptions(nuqsOptions),
|
||||
);
|
||||
|
||||
const interval =
|
||||
@@ -38,7 +38,7 @@ export function useOverviewOptions() {
|
||||
|
||||
const [metric, setMetric] = useQueryState(
|
||||
'metric',
|
||||
parseAsInteger.withDefault(0).withOptions(nuqsOptions)
|
||||
parseAsInteger.withDefault(0).withOptions(nuqsOptions),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,14 +9,14 @@ export function useOverviewWidget<T extends string>(
|
||||
widgets: Record<
|
||||
T,
|
||||
{ title: string; btn: string; chart: ReportChartProps; hide?: boolean }
|
||||
>
|
||||
>,
|
||||
) {
|
||||
const keys = Object.keys(widgets) as T[];
|
||||
const [widget, setWidget] = useQueryState<T>(
|
||||
key,
|
||||
parseAsStringEnum(keys)
|
||||
.withDefault(keys[0]!)
|
||||
.withOptions({ history: 'push' })
|
||||
.withOptions({ history: 'push' }),
|
||||
);
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ export function PageTabsLink({
|
||||
<Link
|
||||
className={cn(
|
||||
'inline-block opacity-100 transition-transform hover:translate-y-[-1px]',
|
||||
isActive ? 'opacity-100' : 'opacity-50'
|
||||
isActive ? 'opacity-100' : 'opacity-50',
|
||||
)}
|
||||
href={href}
|
||||
>
|
||||
@@ -50,9 +50,10 @@ export function PageTabsItem({
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-block opacity-100 transition-transform hover:translate-y-[-1px]',
|
||||
isActive ? 'opacity-100' : 'opacity-50'
|
||||
isActive ? 'opacity-100' : 'opacity-50',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
@@ -7,6 +5,8 @@ import {
|
||||
ChevronsLeftIcon,
|
||||
ChevronsRightIcon,
|
||||
} from 'lucide-react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
|
||||
@@ -45,7 +45,7 @@ export function Pagination({
|
||||
<div
|
||||
className={cn(
|
||||
'flex select-none items-center justify-end gap-1',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{size === 'base' && (
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ProfileAvatar({
|
||||
: size === 'xs'
|
||||
? 'text-[8px]'
|
||||
: 'text-base',
|
||||
'bg-def-200 text-muted-foreground'
|
||||
'bg-def-200 text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{firstName?.at(0) ?? '🫣'}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { memo } from 'react';
|
||||
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';
|
||||
@@ -8,6 +6,8 @@ import { TableSkeleton } from '@/components/ui/table';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { GanttChartIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ProfilesTable = memo(
|
||||
className="mt-2"
|
||||
setCursor={props.setCursor}
|
||||
cursor={props.cursor}
|
||||
count={Infinity}
|
||||
count={Number.POSITIVE_INFINITY}
|
||||
take={50}
|
||||
loading={isFetching}
|
||||
/>
|
||||
@@ -68,7 +68,7 @@ export const ProfilesTable = memo(
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return isEqual(prevProps.query.data, nextProps.query.data);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ProfilesTable.displayName = 'ProfilesTable';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Suspense } from 'react';
|
||||
import { shortNumber } from '@/hooks/useNumerFormatter';
|
||||
import { Suspense } from 'react';
|
||||
import { escape } from 'sqlstring';
|
||||
|
||||
import type { IServiceProject } from '@openpanel/db';
|
||||
import { chQuery, TABLE_NAMES } from '@openpanel/db';
|
||||
import { TABLE_NAMES, chQuery } from '@openpanel/db';
|
||||
|
||||
import { ChartSSR } from '../chart-ssr';
|
||||
import { FadeIn } from '../fade-in';
|
||||
@@ -34,7 +34,7 @@ function ProjectCard({ id, name, organizationSlug }: IServiceProject) {
|
||||
|
||||
async function ProjectChart({ id }: { id: string }) {
|
||||
const chart = await chQuery<{ value: number; date: string }>(
|
||||
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`
|
||||
`SELECT countDistinct(profile_id) as value, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(id)} AND name = 'session_start' AND created_at >= now() - interval '1 month' GROUP BY date ORDER BY date ASC`,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -70,7 +70,7 @@ async function ProjectMetrics({ id }: { id: string }) {
|
||||
(
|
||||
SELECT count(DISTINCT profile_id) as count FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(id)} AND created_at >= now() - interval '1 day'
|
||||
) as day
|
||||
`
|
||||
`,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user