mirror of
https://github.com/umami-software/umami.git
synced 2026-02-06 05:37:20 +01:00
Refactor filter handling for queries.
This commit is contained in:
parent
5b300f1ff5
commit
ee6c68d27c
107 changed files with 731 additions and 835 deletions
|
|
@ -1,14 +1,13 @@
|
|||
import { ReactNode } from 'react';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { SearchField, Row, Column } from '@umami/react-zen';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Pager } from '@/components/common/Pager';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { PagedQueryResult } from '@/lib/types';
|
||||
|
||||
const DEFAULT_SEARCH_DELAY = 600;
|
||||
|
||||
export interface DataTableProps {
|
||||
queryResult: PagedQueryResult<any>;
|
||||
queryResult: any;
|
||||
searchDelay?: number;
|
||||
allowSearch?: boolean;
|
||||
allowPaging?: boolean;
|
||||
|
|
@ -20,31 +19,30 @@ export interface DataTableProps {
|
|||
export function DataGrid({
|
||||
queryResult,
|
||||
searchDelay = 600,
|
||||
allowSearch = true,
|
||||
allowPaging = true,
|
||||
allowSearch,
|
||||
allowPaging,
|
||||
autoFocus,
|
||||
children,
|
||||
}: DataTableProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { result, params, setParams, query } = queryResult || {};
|
||||
const { error, isLoading, isFetching } = query || {};
|
||||
const { page, pageSize, count, data } = result || {};
|
||||
const { search } = params || {};
|
||||
const hasData = Boolean(!isLoading && data?.length);
|
||||
const { data, error, isLoading, isFetching, setParams } = queryResult || {};
|
||||
const { router, updateParams } = useNavigation();
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const handleSearch = (search: string) => {
|
||||
setParams({ ...params, search });
|
||||
setSearch(search);
|
||||
setParams(params => ({ ...params, search }));
|
||||
router.push(updateParams({ search }));
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams({ ...params, page });
|
||||
setParams(params => ({ ...params, page }));
|
||||
router.push(updateParams({ page }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap="4" minHeight="300px">
|
||||
{allowSearch && (hasData || search) && (
|
||||
{allowSearch && (data || search) && (
|
||||
<Row width="280px" alignItems="center">
|
||||
<SearchField
|
||||
value={search}
|
||||
|
|
@ -57,11 +55,16 @@ export function DataGrid({
|
|||
)}
|
||||
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
|
||||
<Column>
|
||||
{hasData ? (typeof children === 'function' ? children(result) : children) : null}
|
||||
{data ? (typeof children === 'function' ? children(data) : children) : null}
|
||||
</Column>
|
||||
{allowPaging && hasData && (
|
||||
{allowPaging && data && (
|
||||
<Row marginTop="6">
|
||||
<Pager page={page} pageSize={pageSize} count={count} onPageChange={handlePageChange} />
|
||||
<Pager
|
||||
page={data.page}
|
||||
pageSize={data.pageSize}
|
||||
count={data.count}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function FilterEditForm({ websiteId, data = [], onChange, onClose }: Filt
|
|||
dateRange: { startDate, endDate },
|
||||
} = useDateRange(websiteId);
|
||||
|
||||
const updateFilter = (name: string, props: { [key: string]: any }) => {
|
||||
const updateFilter = (name: string, props: Record<string, any>) => {
|
||||
setFilters(filters =>
|
||||
filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export * from './queries/useTeamMembersQuery';
|
|||
export * from './queries/useUserQuery';
|
||||
export * from './queries/useUsersQuery';
|
||||
export * from './queries/useWebsiteQuery';
|
||||
export * from './queries/useWebsites';
|
||||
export * from './queries/useWebsitesQuery';
|
||||
export * from './queries/useWebsiteEventsQuery';
|
||||
export * from './queries/useWebsiteEventsSeriesQuery';
|
||||
export * from './queries/useWebsiteMetricsQuery';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useApi, useModified } from '@/components/hooks';
|
||||
|
||||
export function useDeleteQuery(path: string, params?: { [key: string]: any }) {
|
||||
export function useDeleteQuery(path: string, params?: Record<string, any>) {
|
||||
const { del, useMutation } = useApi();
|
||||
const { mutate, isPending, error } = useMutation({
|
||||
mutationFn: () => del(path, params),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,27 @@
|
|||
import { useTimezone } from '@/components/hooks/useTimezone';
|
||||
import { REALTIME_INTERVAL } from '@/lib/constants';
|
||||
import { RealtimeData } from '@/lib/types';
|
||||
import { useApi } from '../useApi';
|
||||
|
||||
export interface RealtimeData {
|
||||
countries: Record<string, number>;
|
||||
events: any[];
|
||||
pageviews: any[];
|
||||
referrers: Record<string, number>;
|
||||
timestamp: number;
|
||||
series: {
|
||||
views: any[];
|
||||
visitors: any[];
|
||||
};
|
||||
totals: {
|
||||
views: number;
|
||||
visitors: number;
|
||||
events: number;
|
||||
countries: number;
|
||||
};
|
||||
urls: Record<string, number>;
|
||||
visitors: any[];
|
||||
}
|
||||
|
||||
export function useRealtimeQuery(websiteId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { timezone } = useTimezone();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ReactQueryOptions } from '@/lib/types';
|
|||
|
||||
export function useResultQuery<T = any>(
|
||||
type: string,
|
||||
params?: { [key: string]: any },
|
||||
params?: Record<string, any>,
|
||||
options?: ReactQueryOptions<T>,
|
||||
) {
|
||||
const { websiteId } = params;
|
||||
|
|
@ -21,7 +21,7 @@ export function useResultQuery<T = any>(
|
|||
...params,
|
||||
},
|
||||
],
|
||||
queryFn: () => post(`/reports/${type}`, { type, ...filters, ...params }),
|
||||
queryFn: () => post(`/reports/${type}`, { type, filters, ...params }),
|
||||
enabled: !!type,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useApi } from '../useApi';
|
||||
|
||||
export function useUserQuery(userId: string, options?: { [key: string]: any }) {
|
||||
export function useUserQuery(userId: string, options?: Record<string, any>) {
|
||||
const { get, useQuery } = useApi();
|
||||
return useQuery({
|
||||
queryKey: ['users', userId],
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { ReactQueryOptions } from '@/lib/types';
|
|||
|
||||
export function useWebsiteEventsQuery(websiteId: string, options?: ReactQueryOptions<any>) {
|
||||
const { get } = useApi();
|
||||
const filterParams = useFilterParams(websiteId);
|
||||
const queryParams = useFilterParams(websiteId);
|
||||
|
||||
return usePagedQuery({
|
||||
queryKey: ['websites:events', { websiteId, ...filterParams }],
|
||||
queryFn: () => get(`/websites/${websiteId}/events`, { ...filterParams, pageSize: 20 }),
|
||||
queryKey: ['websites:events', { websiteId, ...queryParams }],
|
||||
queryFn: () => get(`/websites/${websiteId}/events`, { ...queryParams, pageSize: 20 }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
|
|||
options?: ReactQueryOptions<WebsiteMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const filterParams = useFilterParams(websiteId);
|
||||
const queryParams = useFilterParams(websiteId);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return useQuery<WebsiteMetricsData>({
|
||||
|
|
@ -23,13 +23,13 @@ export function useWebsiteMetricsQuery(
|
|||
'websites:metrics',
|
||||
{
|
||||
websiteId,
|
||||
...filterParams,
|
||||
...queryParams,
|
||||
...params,
|
||||
},
|
||||
],
|
||||
queryFn: async () =>
|
||||
get(`/websites/${websiteId}/metrics`, {
|
||||
...filterParams,
|
||||
...queryParams,
|
||||
[searchParams.get('view')]: undefined,
|
||||
...params,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ export function useWebsitePageviewsQuery(
|
|||
options?: ReactQueryOptions<WebsitePageviewsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const filterParams = useFilterParams(websiteId);
|
||||
const queryParams = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<WebsitePageviewsData>({
|
||||
queryKey: ['websites:pageviews', { websiteId, compare, ...filterParams }],
|
||||
queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...filterParams }),
|
||||
queryKey: ['websites:pageviews', { websiteId, compare, ...queryParams }],
|
||||
queryFn: () => get(`/websites/${websiteId}/pageviews`, { compare, ...queryParams }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useApi } from '../useApi';
|
||||
|
||||
export function useWebsiteQuery(websiteId: string, options?: { [key: string]: any }) {
|
||||
export function useWebsiteQuery(websiteId: string, options?: Record<string, any>) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
||||
return useQuery({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
|
||||
export function useWebsiteSessionStatsQuery(
|
||||
websiteId: string,
|
||||
options?: { [key: string]: string },
|
||||
) {
|
||||
export function useWebsiteSessionStatsQuery(websiteId: string, options?: Record<string, string>) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useFilterParams } from '@/components/hooks/useFilterParams';
|
|||
|
||||
export function useWebsiteSessionsQuery(
|
||||
websiteId: string,
|
||||
params?: { [key: string]: string | number },
|
||||
params?: Record<string, string | number>,
|
||||
) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`sessions`);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useFilterParams } from '@/components/hooks/useFilterParams';
|
|||
|
||||
export function useWebsiteSessionsWeeklyQuery(
|
||||
websiteId: string,
|
||||
params?: { [key: string]: string | number },
|
||||
params?: Record<string, string | number>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { modified } = useModified(`sessions`);
|
||||
|
|
|
|||
|
|
@ -19,15 +19,14 @@ export interface WebsiteStatsData {
|
|||
|
||||
export function useWebsiteStatsQuery(
|
||||
websiteId: string,
|
||||
compare?: string,
|
||||
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const params = useFilterParams(websiteId);
|
||||
const filterParams = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<WebsiteStatsData>({
|
||||
queryKey: ['websites:stats', { websiteId, ...params, compare }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, { ...params, compare }),
|
||||
queryKey: ['websites:stats', { websiteId, ...filterParams }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, { ...filterParams }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import { useApi } from '../useApi';
|
|||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import { useLoginQuery } from './useLoginQuery';
|
||||
import { useModified } from '../useModified';
|
||||
import { ReactQueryOptions } from '@/lib/types';
|
||||
|
||||
export function useWebsites(
|
||||
export function useWebsitesQuery(
|
||||
{ userId, teamId }: { userId?: string; teamId?: string },
|
||||
params?: { [key: string]: string | number },
|
||||
params?: Record<string, any>,
|
||||
options?: ReactQueryOptions,
|
||||
) {
|
||||
const { get } = useApi();
|
||||
const { user } = useLoginQuery();
|
||||
|
|
@ -13,10 +15,12 @@ export function useWebsites(
|
|||
|
||||
return usePagedQuery({
|
||||
queryKey: ['websites', { userId, teamId, modified, ...params }],
|
||||
queryFn: () => {
|
||||
queryFn: pageParams => {
|
||||
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
|
||||
...params,
|
||||
...pageParams,
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ export function useFormat() {
|
|||
return languageNames[value?.split('-')[0]] || value;
|
||||
};
|
||||
|
||||
const formatValue = (value: string, type: string, data?: { [key: string]: any }): string => {
|
||||
const formatValue = (value: string, type: string, data?: Record<string, any>): string => {
|
||||
switch (type) {
|
||||
case 'os':
|
||||
return formatOS(value);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function useMessages(): any {
|
|||
id: string;
|
||||
defaultMessage: string;
|
||||
},
|
||||
values?: { [key: string]: string },
|
||||
values?: Record<string, string>,
|
||||
opts?: any,
|
||||
) => {
|
||||
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ export function useNavigation() {
|
|||
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
|
||||
const query = Object.fromEntries(searchParams);
|
||||
|
||||
const updateParams = (params?: { [key: string]: string | number }) => {
|
||||
const updateParams = (params?: Record<string, string | number>) => {
|
||||
return !params ? pathname : buildUrl(pathname, { ...query, ...params });
|
||||
};
|
||||
|
||||
const renderUrl = (path: string, params?: { [key: string]: string | number } | false) => {
|
||||
const renderUrl = (path: string, params?: Record<string, string | number> | false) => {
|
||||
return buildUrl(
|
||||
teamId ? `/teams/${teamId}${path}` : path,
|
||||
params === false ? {} : { ...query, ...params },
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { PageResult, PageParams, PagedQueryResult } from '@/lib/types';
|
||||
import { useApi } from './useApi';
|
||||
import { useNavigation } from './useNavigation';
|
||||
|
||||
export function usePagedQuery<T = any>({
|
||||
export function usePagedQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
...options
|
||||
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }): PagedQueryResult<T> {
|
||||
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }) {
|
||||
const { query: queryParams } = useNavigation();
|
||||
const [params, setParams] = useState<PageParams>({
|
||||
search: '',
|
||||
page: queryParams?.page || '1',
|
||||
const [params, setParams] = useState({
|
||||
search: queryParams?.search ?? '',
|
||||
page: queryParams?.page ?? '1',
|
||||
});
|
||||
|
||||
const { useQuery } = useApi();
|
||||
const { data, ...query } = useQuery({
|
||||
queryKey: [{ ...queryKey, ...params }],
|
||||
queryFn: () => queryFn(params as any),
|
||||
...options,
|
||||
});
|
||||
|
||||
return {
|
||||
result: data as PageResult<T>,
|
||||
query,
|
||||
...useQuery({
|
||||
queryKey: [{ ...queryKey, ...params }],
|
||||
queryFn: () => queryFn(params),
|
||||
...options,
|
||||
}),
|
||||
params,
|
||||
setParams,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Select, ListItem } from '@umami/react-zen';
|
||||
import { useWebsites, useMessages } from '@/components/hooks';
|
||||
import type { SelectProps } from '@umami/react-zen/Select';
|
||||
import { Select, SelectProps, ListItem } from '@umami/react-zen';
|
||||
import { useWebsitesQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function WebsiteSelect({
|
||||
websiteId,
|
||||
|
|
@ -12,14 +11,14 @@ export function WebsiteSelect({
|
|||
}: {
|
||||
websiteId?: string;
|
||||
teamId?: string;
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'quiet' | 'danger' | 'zero';
|
||||
variant?: 'primary' | 'outline' | 'quiet' | 'danger' | 'zero';
|
||||
onSelect?: (key: any) => void;
|
||||
} & SelectProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedId, setSelectedId] = useState(websiteId);
|
||||
|
||||
const queryResult = useWebsites({ teamId }, { search, pageSize: 5 });
|
||||
const { data, isLoading } = useWebsitesQuery({ teamId }, { search, pageSize: 5 });
|
||||
|
||||
const handleSelect = (value: any) => {
|
||||
setSelectedId(value);
|
||||
|
|
@ -30,15 +29,17 @@ export function WebsiteSelect({
|
|||
setSearch(value);
|
||||
};
|
||||
|
||||
const items = queryResult?.result?.data as any[];
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...props}
|
||||
items={items}
|
||||
items={data?.['data'] || []}
|
||||
value={selectedId}
|
||||
placeholder={formatMessage(labels.selectWebsite)}
|
||||
isLoading={queryResult.query.isLoading}
|
||||
isLoading={isLoading}
|
||||
buttonProps={{ variant }}
|
||||
allowSearch={true}
|
||||
searchValue={search}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export interface MetricCardProps {
|
|||
formatValue?: (n: any) => string;
|
||||
showLabel?: boolean;
|
||||
showChange?: boolean;
|
||||
showPrevious?: boolean;
|
||||
}
|
||||
|
||||
export const MetricCard = ({
|
||||
|
|
@ -24,13 +23,11 @@ export const MetricCard = ({
|
|||
formatValue = formatNumber,
|
||||
showLabel = true,
|
||||
showChange = false,
|
||||
showPrevious = false,
|
||||
}: MetricCardProps) => {
|
||||
const diff = value - change;
|
||||
const pct = ((value - diff) / diff) * 100;
|
||||
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
|
||||
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
|
||||
const prevProps = useSpring({ x: Number(diff) || 0, from: { x: 0 } });
|
||||
|
||||
return (
|
||||
<Column
|
||||
|
|
@ -54,9 +51,6 @@ export const MetricCard = ({
|
|||
<AnimatedDiv>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</AnimatedDiv>
|
||||
</ChangeLabel>
|
||||
)}
|
||||
{showPrevious && (
|
||||
<AnimatedDiv title={diff.toString()}>{prevProps?.x?.to(x => formatValue(x))}</AnimatedDiv>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export interface MetricsTableProps extends ListTableProps {
|
|||
allowSearch?: boolean;
|
||||
searchFormattedValues?: boolean;
|
||||
showMore?: boolean;
|
||||
params?: { [key: string]: any };
|
||||
params?: Record<string, any>;
|
||||
onDataLoad?: (data: any) => any;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue