dashboard: add bots to overview
This commit is contained in:
81
apps/dashboard/src/components/overview/overview-top-bots.tsx
Normal file
81
apps/dashboard/src/components/overview/overview-top-bots.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { api } from '@/trpc/client';
|
||||||
|
|
||||||
|
import { Pagination } from '../pagination';
|
||||||
|
import { Tooltiper } from '../ui/tooltip';
|
||||||
|
import { WidgetTable } from '../widget-table';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPath(path: string) {
|
||||||
|
try {
|
||||||
|
return new URL(path).pathname;
|
||||||
|
} catch {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const OverviewTopBots = ({ projectId }: Props) => {
|
||||||
|
const [cursor, setCursor] = useState<number>(0);
|
||||||
|
const res = api.event.bots.useQuery(
|
||||||
|
{ projectId, cursor },
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = res.data?.data ?? [];
|
||||||
|
const count = res.data?.count ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="-m-4">
|
||||||
|
<WidgetTable
|
||||||
|
className="max-w-full [&_td:first-child]:w-full [&_th]:text-xs [&_tr]:text-xs"
|
||||||
|
data={data}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
name: 'Path',
|
||||||
|
render(item) {
|
||||||
|
return (
|
||||||
|
<Tooltiper asChild content={item.path}>
|
||||||
|
<span className="w-full">{getPath(item.path)}</span>
|
||||||
|
</Tooltiper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date',
|
||||||
|
render(item) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 whitespace-nowrap">
|
||||||
|
<Tooltiper asChild content={`${item.type}`}>
|
||||||
|
<div>{item.name}</div>
|
||||||
|
</Tooltiper>
|
||||||
|
<Tooltiper
|
||||||
|
asChild
|
||||||
|
content={`${item.createdAt.toLocaleString()}`}
|
||||||
|
>
|
||||||
|
<div>{item.createdAt.toLocaleDateString()}</div>
|
||||||
|
</Tooltiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
className="mt-4"
|
||||||
|
cursor={cursor}
|
||||||
|
setCursor={setCursor}
|
||||||
|
count={count}
|
||||||
|
take={8}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverviewTopBots;
|
||||||
@@ -9,6 +9,7 @@ import type { IChartType } from '@openpanel/validation';
|
|||||||
import { LazyChart } from '../report/chart/LazyChart';
|
import { LazyChart } from '../report/chart/LazyChart';
|
||||||
import { Widget, WidgetBody } from '../widget';
|
import { Widget, WidgetBody } from '../widget';
|
||||||
import { OverviewChartToggle } from './overview-chart-toggle';
|
import { OverviewChartToggle } from './overview-chart-toggle';
|
||||||
|
import OverviewTopBots from './overview-top-bots';
|
||||||
import { WidgetButtons, WidgetHead } from './overview-widget';
|
import { WidgetButtons, WidgetHead } from './overview-widget';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
import { useOverviewWidget } from './useOverviewWidget';
|
import { useOverviewWidget } from './useOverviewWidget';
|
||||||
@@ -112,6 +113,10 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
bot: {
|
||||||
|
title: 'Bots',
|
||||||
|
btn: 'Bots',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -135,14 +140,18 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
|
|||||||
</WidgetButtons>
|
</WidgetButtons>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<LazyChart
|
{widget.key === 'bot' ? (
|
||||||
hideID
|
<OverviewTopBots projectId={projectId} />
|
||||||
{...widget.chart}
|
) : (
|
||||||
previous={false}
|
<LazyChart
|
||||||
onClick={(item) => {
|
hideID
|
||||||
setFilter('path', item.name);
|
{...widget.chart}
|
||||||
}}
|
previous={false}
|
||||||
/>
|
onClick={(item) => {
|
||||||
|
setFilter('path', item.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
</Widget>
|
</Widget>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const data = {
|
|||||||
linkedin: 'https://linkedin.com',
|
linkedin: 'https://linkedin.com',
|
||||||
linux: 'https://upload.wikimedia.org/wikipedia/commons/3/35/Tux.svg',
|
linux: 'https://upload.wikimedia.org/wikipedia/commons/3/35/Tux.svg',
|
||||||
microlaunch: 'https://microlaunch.net',
|
microlaunch: 'https://microlaunch.net',
|
||||||
|
openalternative: 'https://openalternative.co',
|
||||||
opera: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Opera_2015_icon.svg/1920px-Opera_2015_icon.svg.png',
|
opera: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Opera_2015_icon.svg/1920px-Opera_2015_icon.svg.png',
|
||||||
pinterest: 'https://www.pinterest.se',
|
pinterest: 'https://www.pinterest.se',
|
||||||
producthunt: 'https://www.producthunt.com',
|
producthunt: 'https://www.producthunt.com',
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
columns: {
|
columns: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -5,11 +7,17 @@ interface Props<T> {
|
|||||||
}[];
|
}[];
|
||||||
keyExtractor: (item: T) => string;
|
keyExtractor: (item: T) => string;
|
||||||
data: T[];
|
data: T[];
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WidgetTable<T>({ columns, data, keyExtractor }: Props<T>) {
|
export function WidgetTable<T>({
|
||||||
|
className,
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
keyExtractor,
|
||||||
|
}: Props<T>) {
|
||||||
return (
|
return (
|
||||||
<table className="w-full">
|
<table className={cn('w-full', className)}>
|
||||||
<thead className="border-b border-border bg-slate-50 text-sm text-slate-500 [&_th:last-child]:text-right [&_th]:whitespace-nowrap [&_th]:p-4 [&_th]:py-2 [&_th]:text-left [&_th]:font-medium">
|
<thead className="border-b border-border bg-slate-50 text-sm text-slate-500 [&_th:last-child]:text-right [&_th]:whitespace-nowrap [&_th]:p-4 [&_th]:py-2 [&_th]:text-left [&_th]:font-medium">
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from '@/trpc/api/trpc';
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
|
} from '@/trpc/api/trpc';
|
||||||
|
import { escape } from 'sqlstring';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { db } from '@openpanel/db';
|
import { chQuery, convertClickhouseDateToJs, db } from '@openpanel/db';
|
||||||
|
|
||||||
export const eventRouter = createTRPCRouter({
|
export const eventRouter = createTRPCRouter({
|
||||||
updateEventMeta: protectedProcedure
|
updateEventMeta: protectedProcedure
|
||||||
@@ -26,4 +31,40 @@ export const eventRouter = createTRPCRouter({
|
|||||||
update: { icon, color, conversion },
|
update: { icon, color, conversion },
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
bots: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
cursor: z.number().optional(),
|
||||||
|
limit: z.number().default(8),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { projectId, cursor, limit } }) => {
|
||||||
|
const [events, counts] = await Promise.all([
|
||||||
|
chQuery<{
|
||||||
|
id: string;
|
||||||
|
project_id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
path: string;
|
||||||
|
created_at: string;
|
||||||
|
}>(
|
||||||
|
`SELECT * FROM events_bots WHERE project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit} OFFSET ${(cursor ?? 0) * limit}`
|
||||||
|
),
|
||||||
|
chQuery<{
|
||||||
|
count: number;
|
||||||
|
}>(
|
||||||
|
`SELECT count(*) as count FROM events_bots WHERE project_id = ${escape(projectId)}`
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: events.map((item) => ({
|
||||||
|
...item,
|
||||||
|
createdAt: convertClickhouseDateToJs(item.created_at),
|
||||||
|
})),
|
||||||
|
count: counts[0]?.count ?? 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ ORDER BY
|
|||||||
(project_id, created_at, profile_id) SETTINGS index_granularity = 8192;
|
(project_id, created_at, profile_id) SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
CREATE TABLE openpanel.events_bots (
|
CREATE TABLE openpanel.events_bots (
|
||||||
|
`id` UUID DEFAULT generateUUIDv4(),
|
||||||
`project_id` String,
|
`project_id` String,
|
||||||
`name` String,
|
`name` String,
|
||||||
`type` String,
|
`type` String,
|
||||||
@@ -71,3 +72,8 @@ CREATE TABLE ba (
|
|||||||
) ENGINE MergeTree
|
) ENGINE MergeTree
|
||||||
ORDER BY
|
ORDER BY
|
||||||
(a, b) SETTINGS index_granularity = 8192;
|
(a, b) SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
ALTER TABLE
|
||||||
|
test.events_bots
|
||||||
|
ADD
|
||||||
|
COLUMN id UUID DEFAULT generateUUIDv4() FIRST;
|
||||||
Reference in New Issue
Block a user