feat(dashboard): add quick filter for origin
This commit is contained in:
@@ -70,6 +70,7 @@
|
|||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"javascript-time-ago": "^2.5.9",
|
"javascript-time-ago": "^2.5.9",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"lucide-react": "^0.331.0",
|
"lucide-react": "^0.331.0",
|
||||||
@@ -117,6 +118,7 @@
|
|||||||
"@openpanel/tsconfig": "workspace:*",
|
"@openpanel/tsconfig": "workspace:*",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/lodash.debounce": "^4.0.9",
|
"@types/lodash.debounce": "^4.0.9",
|
||||||
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/lodash.throttle": "^4.1.9",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@types/node": "^18.19.15",
|
"@types/node": "^18.19.15",
|
||||||
"@types/ramda": "^0.29.10",
|
"@types/ramda": "^0.29.10",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{data?.map((item) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={item.origin}
|
||||||
|
variant="outline"
|
||||||
|
icon={GlobeIcon}
|
||||||
|
className={cn(
|
||||||
|
originFilter?.value.includes(item.origin) && 'border-foreground'
|
||||||
|
)}
|
||||||
|
onClick={() => setFilter('origin', [item.origin], 'is')}
|
||||||
|
>
|
||||||
|
{item.origin}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { useProfileProperties } from '@/hooks/useProfileProperties';
|
import { useProfileProperties } from '@/hooks/useProfileProperties';
|
||||||
import { useProfileValues } from '@/hooks/useProfileValues';
|
import { useProfileValues } from '@/hooks/useProfileValues';
|
||||||
import { usePropertyValues } from '@/hooks/usePropertyValues';
|
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 { Options as NuqsOptions } from 'nuqs';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
} from '@openpanel/validation';
|
} from '@openpanel/validation';
|
||||||
|
|
||||||
import { useOverviewOptions } from '../useOverviewOptions';
|
import { useOverviewOptions } from '../useOverviewOptions';
|
||||||
|
import { OriginFilter } from './origin-filter';
|
||||||
|
|
||||||
export interface OverviewFiltersDrawerContentProps {
|
export interface OverviewFiltersDrawerContentProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -52,6 +53,7 @@ export function OverviewFiltersDrawerContent({
|
|||||||
|
|
||||||
<div className="mt-8 flex flex-col rounded-md border bg-def-100">
|
<div className="mt-8 flex flex-col rounded-md border bg-def-100">
|
||||||
<div className="flex flex-col gap-4 p-4">
|
<div className="flex flex-col gap-4 p-4">
|
||||||
|
<OriginFilter />
|
||||||
{enableEventsFilter && (
|
{enableEventsFilter && (
|
||||||
<ComboboxAdvanced
|
<ComboboxAdvanced
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -106,10 +108,8 @@ export function OverviewFiltersDrawerContent({
|
|||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : /* TODO: Implement profile filters */
|
||||||
/* TODO: Implement profile filters */
|
null;
|
||||||
null
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { api } from '@/trpc/client';
|
import { api } from '@/trpc/client';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
import isEqual from 'lodash.isequal';
|
||||||
|
|
||||||
import type { IChartProps } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
@@ -18,94 +19,51 @@ import { ReportPieChart } from './ReportPieChart';
|
|||||||
|
|
||||||
export type ReportChartProps = IChartProps;
|
export type ReportChartProps = IChartProps;
|
||||||
|
|
||||||
function useChartData() {
|
const pluckChartContext = (context: IChartProps) => ({
|
||||||
const {
|
chartType: context.chartType,
|
||||||
interval,
|
interval: context.interval,
|
||||||
events,
|
breakdowns: context.breakdowns,
|
||||||
breakdowns,
|
range: context.range,
|
||||||
chartType,
|
previous: context.previous,
|
||||||
range,
|
formula: context.formula,
|
||||||
previous,
|
metric: context.metric,
|
||||||
formula,
|
projectId: context.projectId,
|
||||||
metric,
|
startDate: context.startDate,
|
||||||
projectId,
|
endDate: context.endDate,
|
||||||
startDate,
|
limit: context.limit,
|
||||||
endDate,
|
offset: context.offset,
|
||||||
limit,
|
events: context.events.map((event) => ({
|
||||||
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),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
debouncedSetParams({
|
|
||||||
interval,
|
|
||||||
events: events.map((event) => ({
|
|
||||||
...event,
|
...event,
|
||||||
filters: event.filters?.filter((filter) => filter.value.length > 0),
|
filters: event.filters?.filter((filter) => filter.value.length > 0),
|
||||||
})),
|
})),
|
||||||
breakdowns,
|
});
|
||||||
chartType,
|
|
||||||
range,
|
// TODO: Quick hack to avoid re-fetching
|
||||||
previous,
|
// Will refactor the entire chart component soon anyway...
|
||||||
formula,
|
function useChartData() {
|
||||||
metric,
|
const context = useChartContext();
|
||||||
projectId,
|
const [params, setParams] = useState(() => pluckChartContext(context));
|
||||||
startDate,
|
const debouncedSetParams = useMemo(() => debounce(setParams, 500), []);
|
||||||
endDate,
|
|
||||||
limit,
|
useEffect(() => {
|
||||||
offset,
|
const newParams = pluckChartContext(context);
|
||||||
});
|
if (!isEqual(newParams, params)) {
|
||||||
|
debouncedSetParams(newParams);
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
debouncedSetParams.cancel();
|
debouncedSetParams.cancel();
|
||||||
};
|
};
|
||||||
}, [
|
}, [context, params, debouncedSetParams]);
|
||||||
interval,
|
|
||||||
events,
|
|
||||||
breakdowns,
|
|
||||||
chartType,
|
|
||||||
range,
|
|
||||||
previous,
|
|
||||||
formula,
|
|
||||||
metric,
|
|
||||||
projectId,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
debouncedSetParams,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [data] = api.chart.chart.useSuspenseQuery(debouncedParams, {
|
return api.chart.chart.useSuspenseQuery(params, {
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chart() {
|
export function Chart() {
|
||||||
const { chartType } = useChartContext();
|
const { chartType } = useChartContext();
|
||||||
const data = useChartData();
|
const [data] = useChartData();
|
||||||
|
|
||||||
if (data.series.length === 0) {
|
if (data.series.length === 0) {
|
||||||
return <ChartEmpty />;
|
return <ChartEmpty />;
|
||||||
|
|||||||
@@ -169,7 +169,10 @@ const DropdownMenuShortcut = ({
|
|||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn('ml-auto text-sm tracking-widest opacity-60', className)}
|
className={cn(
|
||||||
|
'ml-auto font-mono text-sm tracking-widest opacity-60',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -181,4 +181,24 @@ export const eventRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return getTopPages(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?:\/\//, ''))
|
||||||
|
);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -341,6 +341,9 @@ importers:
|
|||||||
lodash.debounce:
|
lodash.debounce:
|
||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
|
lodash.isequal:
|
||||||
|
specifier: ^4.5.0
|
||||||
|
version: 4.5.0
|
||||||
lodash.throttle:
|
lodash.throttle:
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
@@ -477,6 +480,9 @@ importers:
|
|||||||
'@types/lodash.debounce':
|
'@types/lodash.debounce':
|
||||||
specifier: ^4.0.9
|
specifier: ^4.0.9
|
||||||
version: 4.0.9
|
version: 4.0.9
|
||||||
|
'@types/lodash.isequal':
|
||||||
|
specifier: ^4.5.8
|
||||||
|
version: 4.5.8
|
||||||
'@types/lodash.throttle':
|
'@types/lodash.throttle':
|
||||||
specifier: ^4.1.9
|
specifier: ^4.1.9
|
||||||
version: 4.1.9
|
version: 4.1.9
|
||||||
@@ -7701,6 +7707,12 @@ packages:
|
|||||||
'@types/lodash': 4.14.202
|
'@types/lodash': 4.14.202
|
||||||
dev: true
|
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:
|
/@types/lodash.throttle@4.1.9:
|
||||||
resolution: {integrity: sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==}
|
resolution: {integrity: sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13449,6 +13461,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isequal@4.5.0:
|
||||||
|
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.isinteger@4.0.4:
|
/lodash.isinteger@4.0.4:
|
||||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
Reference in New Issue
Block a user