diff --git a/apps/dashboard/src/components/overview/overview-top-bots.tsx b/apps/dashboard/src/components/overview/overview-top-bots.tsx new file mode 100644 index 00000000..d6a56d7c --- /dev/null +++ b/apps/dashboard/src/components/overview/overview-top-bots.tsx @@ -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(0); + const res = api.event.bots.useQuery( + { projectId, cursor }, + { + keepPreviousData: true, + } + ); + const data = res.data?.data ?? []; + const count = res.data?.count ?? 0; + + return ( + <> +
+ item.id} + columns={[ + { + name: 'Path', + render(item) { + return ( + + {getPath(item.path)} + + ); + }, + }, + { + name: 'Date', + render(item) { + return ( +
+ +
{item.name}
+
+ +
{item.createdAt.toLocaleDateString()}
+
+
+ ); + }, + }, + ]} + /> +
+ + + ); +}; + +export default OverviewTopBots; diff --git a/apps/dashboard/src/components/overview/overview-top-pages.tsx b/apps/dashboard/src/components/overview/overview-top-pages.tsx index d5aa772c..447d5029 100644 --- a/apps/dashboard/src/components/overview/overview-top-pages.tsx +++ b/apps/dashboard/src/components/overview/overview-top-pages.tsx @@ -9,6 +9,7 @@ import type { IChartType } from '@openpanel/validation'; import { LazyChart } from '../report/chart/LazyChart'; import { Widget, WidgetBody } from '../widget'; import { OverviewChartToggle } from './overview-chart-toggle'; +import OverviewTopBots from './overview-top-bots'; import { WidgetButtons, WidgetHead } from './overview-widget'; import { useOverviewOptions } from './useOverviewOptions'; import { useOverviewWidget } from './useOverviewWidget'; @@ -112,6 +113,10 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) { metric: 'sum', }, }, + bot: { + title: 'Bots', + btn: 'Bots', + }, }); return ( @@ -135,14 +140,18 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) { - { - setFilter('path', item.name); - }} - /> + {widget.key === 'bot' ? ( + + ) : ( + { + setFilter('path', item.name); + }} + /> + )} diff --git a/apps/dashboard/src/components/report/chart/SerieIcon.urls.ts b/apps/dashboard/src/components/report/chart/SerieIcon.urls.ts index 295f042c..a9e731e8 100644 --- a/apps/dashboard/src/components/report/chart/SerieIcon.urls.ts +++ b/apps/dashboard/src/components/report/chart/SerieIcon.urls.ts @@ -24,6 +24,7 @@ const data = { linkedin: 'https://linkedin.com', linux: 'https://upload.wikimedia.org/wikipedia/commons/3/35/Tux.svg', 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', pinterest: 'https://www.pinterest.se', producthunt: 'https://www.producthunt.com', diff --git a/apps/dashboard/src/components/widget-table.tsx b/apps/dashboard/src/components/widget-table.tsx index 6357b802..eecaeaef 100644 --- a/apps/dashboard/src/components/widget-table.tsx +++ b/apps/dashboard/src/components/widget-table.tsx @@ -1,3 +1,5 @@ +import { cn } from '@/utils/cn'; + interface Props { columns: { name: string; @@ -5,11 +7,17 @@ interface Props { }[]; keyExtractor: (item: T) => string; data: T[]; + className?: string; } -export function WidgetTable({ columns, data, keyExtractor }: Props) { +export function WidgetTable({ + className, + columns, + data, + keyExtractor, +}: Props) { return ( - +
{columns.map((column) => ( diff --git a/apps/dashboard/src/trpc/api/routers/event.ts b/apps/dashboard/src/trpc/api/routers/event.ts index b0a665dc..be4213f5 100644 --- a/apps/dashboard/src/trpc/api/routers/event.ts +++ b/apps/dashboard/src/trpc/api/routers/event.ts @@ -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 { db } from '@openpanel/db'; +import { chQuery, convertClickhouseDateToJs, db } from '@openpanel/db'; export const eventRouter = createTRPCRouter({ updateEventMeta: protectedProcedure @@ -26,4 +31,40 @@ export const eventRouter = createTRPCRouter({ 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, + }; + }), }); diff --git a/packages/db/clickhouse_tables.sql b/packages/db/clickhouse_tables.sql index 61d5cf59..3ee0a8b3 100644 --- a/packages/db/clickhouse_tables.sql +++ b/packages/db/clickhouse_tables.sql @@ -31,6 +31,7 @@ ORDER BY (project_id, created_at, profile_id) SETTINGS index_granularity = 8192; CREATE TABLE openpanel.events_bots ( + `id` UUID DEFAULT generateUUIDv4(), `project_id` String, `name` String, `type` String, @@ -70,4 +71,9 @@ CREATE TABLE ba ( `b` String ) ENGINE MergeTree ORDER BY - (a, b) SETTINGS index_granularity = 8192; \ No newline at end of file + (a, b) SETTINGS index_granularity = 8192; + +ALTER TABLE + test.events_bots +ADD + COLUMN id UUID DEFAULT generateUUIDv4() FIRST; \ No newline at end of file