mirror of
https://github.com/umami-software/umami.git
synced 2026-02-07 06:07:17 +01:00
New admin section.
This commit is contained in:
parent
b78ff3b477
commit
ce1f6c3618
44 changed files with 515 additions and 157 deletions
|
|
@ -1,13 +1,15 @@
|
|||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode, useState, useCallback } from 'react';
|
||||
import { SearchField, Row, Column } from '@umami/react-zen';
|
||||
import { UseQueryResult } from '@tanstack/react-query';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Pager } from '@/components/common/Pager';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { PageResult } from '@/lib/types';
|
||||
|
||||
const DEFAULT_SEARCH_DELAY = 600;
|
||||
|
||||
export interface DataGridProps {
|
||||
queryResult: any;
|
||||
query: UseQueryResult<PageResult<any>, any>;
|
||||
searchDelay?: number;
|
||||
allowSearch?: boolean;
|
||||
allowPaging?: boolean;
|
||||
|
|
@ -17,7 +19,7 @@ export interface DataGridProps {
|
|||
}
|
||||
|
||||
export function DataGrid({
|
||||
queryResult,
|
||||
query,
|
||||
searchDelay = 600,
|
||||
allowSearch,
|
||||
allowPaging = true,
|
||||
|
|
@ -26,20 +28,23 @@ export function DataGrid({
|
|||
children,
|
||||
}: DataGridProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, error, isLoading, isFetching, setParams } = queryResult;
|
||||
const { router, updateParams } = useNavigation();
|
||||
const [search, setSearch] = useState('');
|
||||
const { data, error, isLoading, isFetching } = query;
|
||||
const { router, updateParams, query: queryParams } = useNavigation();
|
||||
const [search, setSearch] = useState(queryParams?.saerch || data?.search || '');
|
||||
|
||||
const handleSearch = (search: string) => {
|
||||
setSearch(search);
|
||||
setParams(params => ({ ...params, search }));
|
||||
router.push(updateParams({ search }));
|
||||
const handleSearch = (value: string) => {
|
||||
if (value !== search) {
|
||||
setSearch(value);
|
||||
router.push(updateParams({ search: value, page: 1 }));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setParams(params => ({ ...params, page }));
|
||||
router.push(updateParams({ page }));
|
||||
};
|
||||
const handlePageChange = useCallback(
|
||||
(page: number) => {
|
||||
router.push(updateParams({ search, page }));
|
||||
},
|
||||
[search],
|
||||
);
|
||||
|
||||
return (
|
||||
<Column gap="4" minHeight="300px">
|
||||
|
|
|
|||
|
|
@ -17,13 +17,20 @@ export function LinkButton({
|
|||
scroll = true,
|
||||
target,
|
||||
children,
|
||||
isDisabled,
|
||||
...props
|
||||
}: LinkButtonProps) {
|
||||
const { dir } = useLocale();
|
||||
|
||||
return (
|
||||
<Button {...props} variant={variant} asChild>
|
||||
<Link href={href} dir={dir} scroll={scroll} target={target}>
|
||||
<Button {...props} variant={variant} isDisabled={isDisabled} asChild>
|
||||
<Link
|
||||
href={href}
|
||||
dir={dir}
|
||||
scroll={scroll}
|
||||
target={target}
|
||||
style={{ pointerEvents: isDisabled ? 'none' : undefined }}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -34,9 +34,14 @@ export function Pager({ page, pageSize, count, onPageChange }: PagerProps) {
|
|||
|
||||
return (
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3" flexGrow={1}>
|
||||
<Text>{formatMessage(labels.numberOfRecords, { x: count })}</Text>
|
||||
<Text>{formatMessage(labels.numberOfRecords, { x: count.toLocaleString() })}</Text>
|
||||
<Row alignItems="center" justifyContent="flex-end" gap="3">
|
||||
<Text>{formatMessage(labels.pageOf, { current: page, total: maxPage })}</Text>
|
||||
<Text>
|
||||
{formatMessage(labels.pageOf, {
|
||||
current: page.toLocaleString(),
|
||||
total: maxPage.toLocaleString(),
|
||||
})}
|
||||
</Text>
|
||||
<Button onPress={() => handlePageChange(-1)} isDisabled={firstPage}>
|
||||
<Icon size="sm" rotate={180}>
|
||||
<Chevron />
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export * from './queries/useWebsiteSessionsWeeklyQuery';
|
|||
export * from './queries/useShareTokenQuery';
|
||||
export * from './queries/useTeamQuery';
|
||||
export * from './queries/useTeamsQuery';
|
||||
export * from './queries/useUserTeamsQuery';
|
||||
export * from './queries/useUserWebsitesQuery';
|
||||
export * from './queries/useTeamWebsitesQuery';
|
||||
export * from './queries/useTeamMembersQuery';
|
||||
export * from './queries/useUserQuery';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import { useApi } from '../useApi';
|
||||
import { useModified } from '../useModified';
|
||||
import { usePagedQuery } from '@/components/hooks';
|
||||
import { ReactQueryOptions } from '@/lib/types';
|
||||
|
||||
export function useTeamsQuery(userId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
export function useTeamsQuery(params?: Record<string, any>, options?: ReactQueryOptions) {
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified(`teams`);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['teams', { userId, modified }],
|
||||
queryFn: () => {
|
||||
return get(`/users/${userId}/teams`, { userId });
|
||||
return usePagedQuery({
|
||||
queryKey: ['websites', { modified, ...params }],
|
||||
queryFn: pageParams => {
|
||||
return get(`/admin/teams`, {
|
||||
...params,
|
||||
...pageParams,
|
||||
});
|
||||
},
|
||||
enabled: !!userId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
15
src/components/hooks/queries/useUserTeamsQuery.ts
Normal file
15
src/components/hooks/queries/useUserTeamsQuery.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { useApi } from '../useApi';
|
||||
import { useModified } from '../useModified';
|
||||
|
||||
export function useUserTeamsQuery(userId: string) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { modified } = useModified(`teams`);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['teams', { userId, modified }],
|
||||
queryFn: () => {
|
||||
return get(`/users/${userId}/teams`, { userId });
|
||||
},
|
||||
enabled: !!userId,
|
||||
});
|
||||
}
|
||||
26
src/components/hooks/queries/useUserWebsitesQuery.ts
Normal file
26
src/components/hooks/queries/useUserWebsitesQuery.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useApi } from '../useApi';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import { useLoginQuery } from './useLoginQuery';
|
||||
import { useModified } from '../useModified';
|
||||
import { ReactQueryOptions } from '@/lib/types';
|
||||
|
||||
export function useUserWebsitesQuery(
|
||||
{ userId, teamId }: { userId?: string; teamId?: string },
|
||||
params?: Record<string, any>,
|
||||
options?: ReactQueryOptions,
|
||||
) {
|
||||
const { get } = useApi();
|
||||
const { user } = useLoginQuery();
|
||||
const { modified } = useModified(`websites`);
|
||||
|
||||
return usePagedQuery({
|
||||
queryKey: ['websites', { userId, teamId, modified, ...params }],
|
||||
queryFn: pageParams => {
|
||||
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
|
||||
...params,
|
||||
...pageParams,
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
@ -8,9 +8,9 @@ export function useUsersQuery() {
|
|||
|
||||
return usePagedQuery({
|
||||
queryKey: ['users', { modified }],
|
||||
queryFn: (params: any) => {
|
||||
queryFn: (pageParams: any) => {
|
||||
return get('/admin/users', {
|
||||
...params,
|
||||
...pageParams,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
import { useApi } from '../useApi';
|
||||
import { usePagedQuery } from '../usePagedQuery';
|
||||
import { useLoginQuery } from './useLoginQuery';
|
||||
import { useModified } from '../useModified';
|
||||
import { ReactQueryOptions } from '@/lib/types';
|
||||
|
||||
export function useWebsitesQuery(
|
||||
{ userId, teamId }: { userId?: string; teamId?: string },
|
||||
params?: Record<string, any>,
|
||||
options?: ReactQueryOptions,
|
||||
) {
|
||||
export function useWebsitesQuery(params?: Record<string, any>, options?: ReactQueryOptions) {
|
||||
const { get } = useApi();
|
||||
const { user } = useLoginQuery();
|
||||
const { modified } = useModified(`websites`);
|
||||
|
||||
return usePagedQuery({
|
||||
queryKey: ['websites', { userId, teamId, modified, ...params }],
|
||||
queryKey: ['websites', { modified, ...params }],
|
||||
queryFn: pageParams => {
|
||||
return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, {
|
||||
...params,
|
||||
return get(`/admin/websites`, {
|
||||
...pageParams,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
...options,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { buildUrl } from '@/lib/url';
|
||||
|
||||
|
|
@ -7,18 +8,31 @@ export function useNavigation() {
|
|||
const searchParams = useSearchParams();
|
||||
const [, teamId] = pathname.match(/\/teams\/([a-f0-9-]+)/) || [];
|
||||
const [, websiteId] = pathname.match(/\/websites\/([a-f0-9-]+)/) || [];
|
||||
const query = Object.fromEntries(searchParams);
|
||||
const [queryParams, setQueryParams] = useState(Object.fromEntries(searchParams));
|
||||
|
||||
const updateParams = (params?: Record<string, string | number>) => {
|
||||
return !params ? pathname : buildUrl(pathname, { ...query, ...params });
|
||||
return !params ? pathname : buildUrl(pathname, { ...queryParams, ...params });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setQueryParams(Object.fromEntries(searchParams));
|
||||
}, [searchParams.toString()]);
|
||||
|
||||
const renderUrl = (path: string, params?: Record<string, string | number> | false) => {
|
||||
return buildUrl(
|
||||
teamId ? `/teams/${teamId}${path}` : path,
|
||||
params === false ? {} : { ...query, ...params },
|
||||
params === false ? {} : { ...queryParams, ...params },
|
||||
);
|
||||
};
|
||||
|
||||
return { router, pathname, searchParams, query, teamId, websiteId, updateParams, renderUrl };
|
||||
return {
|
||||
router,
|
||||
pathname,
|
||||
searchParams,
|
||||
query: queryParams,
|
||||
teamId,
|
||||
websiteId,
|
||||
updateParams,
|
||||
renderUrl,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,27 @@
|
|||
import { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
|
||||
import { useApi } from './useApi';
|
||||
import { useNavigation } from './useNavigation';
|
||||
import { PageResult } from '@/lib/types';
|
||||
|
||||
export function usePagedQuery({
|
||||
export function usePagedQuery<TData = any, TError = Error>({
|
||||
queryKey,
|
||||
queryFn,
|
||||
...options
|
||||
}: Omit<UseQueryOptions, 'queryFn'> & { queryFn: (params?: object) => any }) {
|
||||
const { query: queryParams } = useNavigation();
|
||||
const [params, setParams] = useState({
|
||||
search: queryParams?.search ?? '',
|
||||
page: queryParams?.page ?? '1',
|
||||
});
|
||||
|
||||
}: Omit<
|
||||
UseQueryOptions<PageResult<TData>, TError, PageResult<TData>, readonly unknown[]>,
|
||||
'queryFn' | 'queryKey'
|
||||
> & {
|
||||
queryKey: readonly unknown[];
|
||||
queryFn: (params?: object) => Promise<PageResult<TData>> | PageResult<TData>;
|
||||
}): UseQueryResult<PageResult<TData>, TError> {
|
||||
const {
|
||||
query: { page, search },
|
||||
} = useNavigation();
|
||||
const { useQuery } = useApi();
|
||||
|
||||
return {
|
||||
...useQuery({
|
||||
queryKey: [{ ...queryKey, ...params }],
|
||||
queryFn: () => queryFn(params),
|
||||
...options,
|
||||
}),
|
||||
params,
|
||||
setParams,
|
||||
};
|
||||
return useQuery<PageResult<TData>, TError>({
|
||||
queryKey: [...queryKey, page, search] as const,
|
||||
queryFn: () => queryFn({ page, search }),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
Row,
|
||||
Box,
|
||||
} from '@umami/react-zen';
|
||||
import { useLoginQuery, useMessages, useTeamsQuery, useNavigation } from '@/components/hooks';
|
||||
import { useLoginQuery, useMessages, useUserTeamsQuery, useNavigation } from '@/components/hooks';
|
||||
import { Chevron, User, Users } from '@/components/icons';
|
||||
|
||||
export function TeamsButton({
|
||||
|
|
@ -25,7 +25,7 @@ export function TeamsButton({
|
|||
}) {
|
||||
const { user } = useLoginQuery();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data } = useTeamsQuery(user.id);
|
||||
const { data } = useUserTeamsQuery(user.id);
|
||||
const { teamId } = useNavigation();
|
||||
const router = useRouter();
|
||||
const team = data?.data?.find(({ id }) => id === teamId);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Select, SelectProps, ListItem } from '@umami/react-zen';
|
||||
import { useWebsitesQuery, useMessages } from '@/components/hooks';
|
||||
import { useUserWebsitesQuery, useMessages } from '@/components/hooks';
|
||||
|
||||
export function WebsiteSelect({
|
||||
websiteId,
|
||||
|
|
@ -18,7 +18,7 @@ export function WebsiteSelect({
|
|||
const [search, setSearch] = useState('');
|
||||
const [selectedId, setSelectedId] = useState(websiteId);
|
||||
|
||||
const { data, isLoading } = useWebsitesQuery({ teamId }, { search, pageSize: 5 });
|
||||
const { data, isLoading } = useUserWebsitesQuery({ teamId }, { search, pageSize: 5 });
|
||||
|
||||
const handleSelect = (value: any) => {
|
||||
setSelectedId(value);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue