diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 01d7728f..19b3a876 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -70,6 +70,7 @@ "input-otp": "^1.2.4", "javascript-time-ago": "^2.5.9", "lodash.debounce": "^4.0.8", + "lodash.isequal": "^4.5.0", "lodash.throttle": "^4.1.1", "lottie-react": "^2.4.0", "lucide-react": "^0.331.0", @@ -117,6 +118,7 @@ "@openpanel/tsconfig": "workspace:*", "@types/bcrypt": "^5.0.2", "@types/lodash.debounce": "^4.0.9", + "@types/lodash.isequal": "^4.5.8", "@types/lodash.throttle": "^4.1.9", "@types/node": "^18.19.15", "@types/ramda": "^0.29.10", diff --git a/apps/dashboard/src/components/overview/filters/origin-filter.tsx b/apps/dashboard/src/components/overview/filters/origin-filter.tsx new file mode 100644 index 00000000..d957f632 --- /dev/null +++ b/apps/dashboard/src/components/overview/filters/origin-filter.tsx @@ -0,0 +1,45 @@ +import { Button } from '@/components/ui/button'; +import { useAppParams } from '@/hooks/useAppParams'; +import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; +import { api } from '@/trpc/client'; +import { cn } from '@/utils/cn'; +import { GlobeIcon } from 'lucide-react'; + +export function OriginFilter() { + const { projectId } = useAppParams(); + const [filters, setFilter] = useEventQueryFilters(); + const originFilter = filters.find((item) => item.name === 'origin'); + + const { data } = api.event.origin.useQuery( + { + projectId: projectId, + }, + { + staleTime: 1000 * 60 * 60, + } + ); + + if (!data || data.length === 0) { + return null; + } + + return ( +
+ {data?.map((item) => { + return ( + + ); + })} +
+ ); +} diff --git a/apps/dashboard/src/components/overview/filters/overview-filters-drawer-content.tsx b/apps/dashboard/src/components/overview/filters/overview-filters-drawer-content.tsx index bd6fb227..348c68ef 100644 --- a/apps/dashboard/src/components/overview/filters/overview-filters-drawer-content.tsx +++ b/apps/dashboard/src/components/overview/filters/overview-filters-drawer-content.tsx @@ -12,7 +12,7 @@ import { import { useProfileProperties } from '@/hooks/useProfileProperties'; import { useProfileValues } from '@/hooks/useProfileValues'; import { usePropertyValues } from '@/hooks/usePropertyValues'; -import { XIcon } from 'lucide-react'; +import { GlobeIcon, XIcon } from 'lucide-react'; import type { Options as NuqsOptions } from 'nuqs'; import type { @@ -22,6 +22,7 @@ import type { } from '@openpanel/validation'; import { useOverviewOptions } from '../useOverviewOptions'; +import { OriginFilter } from './origin-filter'; export interface OverviewFiltersDrawerContentProps { projectId: string; @@ -52,6 +53,7 @@ export function OverviewFiltersDrawerContent({
+ {enableEventsFilter && ( - ) : ( - /* TODO: Implement profile filters */ - null - ); + ) : /* TODO: Implement profile filters */ + null; })}
diff --git a/apps/dashboard/src/components/report/chart/Chart.tsx b/apps/dashboard/src/components/report/chart/Chart.tsx index 35fc2394..985c0932 100644 --- a/apps/dashboard/src/components/report/chart/Chart.tsx +++ b/apps/dashboard/src/components/report/chart/Chart.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react'; import { api } from '@/trpc/client'; import debounce from 'lodash.debounce'; +import isEqual from 'lodash.isequal'; import type { IChartProps } from '@openpanel/validation'; @@ -18,94 +19,51 @@ import { ReportPieChart } from './ReportPieChart'; export type ReportChartProps = IChartProps; +const pluckChartContext = (context: IChartProps) => ({ + chartType: context.chartType, + interval: context.interval, + breakdowns: context.breakdowns, + range: context.range, + previous: context.previous, + formula: context.formula, + metric: context.metric, + projectId: context.projectId, + startDate: context.startDate, + endDate: context.endDate, + limit: context.limit, + offset: context.offset, + events: context.events.map((event) => ({ + ...event, + filters: event.filters?.filter((filter) => filter.value.length > 0), + })), +}); + +// TODO: Quick hack to avoid re-fetching +// Will refactor the entire chart component soon anyway... function useChartData() { - const { - interval, - events, - breakdowns, - chartType, - range, - previous, - formula, - metric, - projectId, - startDate, - endDate, - limit, - offset, - } = useChartContext(); - - const [debouncedParams, setDebouncedParams] = useState({ - interval, - events, - breakdowns, - chartType, - range, - previous, - formula, - metric, - projectId, - startDate, - endDate, - limit, - offset, - }); - - const debouncedSetParams = useMemo( - () => debounce(setDebouncedParams, 500), - [] - ); + const context = useChartContext(); + const [params, setParams] = useState(() => pluckChartContext(context)); + const debouncedSetParams = useMemo(() => debounce(setParams, 500), []); useEffect(() => { - debouncedSetParams({ - interval, - events: events.map((event) => ({ - ...event, - filters: event.filters?.filter((filter) => filter.value.length > 0), - })), - breakdowns, - chartType, - range, - previous, - formula, - metric, - projectId, - startDate, - endDate, - limit, - offset, - }); + const newParams = pluckChartContext(context); + if (!isEqual(newParams, params)) { + debouncedSetParams(newParams); + } return () => { debouncedSetParams.cancel(); }; - }, [ - interval, - events, - breakdowns, - chartType, - range, - previous, - formula, - metric, - projectId, - startDate, - endDate, - limit, - offset, - debouncedSetParams, - ]); + }, [context, params, debouncedSetParams]); - const [data] = api.chart.chart.useSuspenseQuery(debouncedParams, { + return api.chart.chart.useSuspenseQuery(params, { keepPreviousData: true, staleTime: 1000 * 60 * 1, }); - - return data; } export function Chart() { const { chartType } = useChartContext(); - const data = useChartData(); + const [data] = useChartData(); if (data.series.length === 0) { return ; diff --git a/apps/dashboard/src/components/ui/dropdown-menu.tsx b/apps/dashboard/src/components/ui/dropdown-menu.tsx index 877ce2b8..996fcb68 100644 --- a/apps/dashboard/src/components/ui/dropdown-menu.tsx +++ b/apps/dashboard/src/components/ui/dropdown-menu.tsx @@ -169,7 +169,10 @@ const DropdownMenuShortcut = ({ }: React.HTMLAttributes) => { return ( ); diff --git a/packages/trpc/src/routers/event.ts b/packages/trpc/src/routers/event.ts index 6f39910c..05fb72ad 100644 --- a/packages/trpc/src/routers/event.ts +++ b/packages/trpc/src/routers/event.ts @@ -181,4 +181,24 @@ export const eventRouter = createTRPCRouter({ .query(async ({ input }) => { return getTopPages(input); }), + + origin: protectedProcedure + .input( + z.object({ + projectId: z.string(), + }) + ) + .query(async ({ input }) => { + const res = await chQuery<{ origin: string }>( + `SELECT DISTINCT origin FROM ${TABLE_NAMES.events} WHERE project_id = ${escape( + input.projectId + )} AND origin IS NOT NULL AND origin != '' AND toDate(created_at) > now() - INTERVAL 30 DAY ORDER BY origin ASC` + ); + + return res.sort((a, b) => + a.origin + .replace(/https?:\/\//, '') + .localeCompare(b.origin.replace(/https?:\/\//, '')) + ); + }), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72b2650f..e8c792aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -341,6 +341,9 @@ importers: lodash.debounce: specifier: ^4.0.8 version: 4.0.8 + lodash.isequal: + specifier: ^4.5.0 + version: 4.5.0 lodash.throttle: specifier: ^4.1.1 version: 4.1.1 @@ -477,6 +480,9 @@ importers: '@types/lodash.debounce': specifier: ^4.0.9 version: 4.0.9 + '@types/lodash.isequal': + specifier: ^4.5.8 + version: 4.5.8 '@types/lodash.throttle': specifier: ^4.1.9 version: 4.1.9 @@ -7701,6 +7707,12 @@ packages: '@types/lodash': 4.14.202 dev: true + /@types/lodash.isequal@4.5.8: + resolution: {integrity: sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==} + dependencies: + '@types/lodash': 4.14.202 + dev: true + /@types/lodash.throttle@4.1.9: resolution: {integrity: sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==} dependencies: @@ -13449,6 +13461,10 @@ packages: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} dev: false + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: false + /lodash.isinteger@4.0.4: resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} dev: false