feat(dashboard): add basic search to profiles

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-09-05 22:08:35 +02:00
parent 9881f34e53
commit b19a874a0b
4 changed files with 74 additions and 38 deletions

View File

@@ -1,6 +1,9 @@
'use client';
import { TableButtons } from '@/components/data-table';
import { ProfilesTable } from '@/components/profiles/table';
import { Input } from '@/components/ui/input';
import { useDebounceValue } from '@/hooks/useDebounceValue';
import { api } from '@/trpc/client';
import { parseAsInteger, useQueryState } from 'nuqs';
@@ -14,12 +17,17 @@ const Events = ({ projectId }: Props) => {
'cursor',
parseAsInteger.withDefault(0)
);
const [search, setSearch] = useQueryState('search', {
defaultValue: '',
shallow: true,
});
const debouncedSearch = useDebounceValue(search, 500);
const query = api.profile.list.useQuery(
{
cursor,
projectId,
take: 50,
// filters,
search: debouncedSearch,
},
{
keepPreviousData: true,
@@ -28,6 +36,13 @@ const Events = ({ projectId }: Props) => {
return (
<div>
<TableButtons>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search profiles"
/>
</TableButtons>
<ProfilesTable query={query} cursor={cursor} setCursor={setCursor} />
</div>
);

View File

@@ -1,3 +1,4 @@
import { memo } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { DataTable } from '@/components/data-table';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
@@ -5,6 +6,7 @@ import { Pagination } from '@/components/pagination';
import { Button } from '@/components/ui/button';
import { TableSkeleton } from '@/components/ui/table';
import type { UseQueryResult } from '@tanstack/react-query';
import isEqual from 'lodash.isequal';
import { GanttChartIcon } from 'lucide-react';
import type { IServiceProfile } from '@openpanel/db';
@@ -22,44 +24,53 @@ type Props =
setCursor: Dispatch<SetStateAction<number>>;
});
export const ProfilesTable = ({ type, query, ...props }: Props) => {
const columns = useColumns(type);
const { data, isFetching, isLoading } = query;
export const ProfilesTable = memo(
({ type, query, ...props }: Props) => {
console.log('re-render');
if (isLoading) {
return <TableSkeleton cols={columns.length} />;
}
const columns = useColumns(type);
const { data, isFetching, isLoading } = query;
if (isLoading) {
return <TableSkeleton cols={columns.length} />;
}
if (data?.length === 0) {
return (
<FullPageEmptyState title="No profiles here" icon={GanttChartIcon}>
<p>Could not find any profiles</p>
{'cursor' in props && props.cursor !== 0 && (
<Button
className="mt-8"
variant="outline"
onClick={() => props.setCursor((p) => p - 1)}
>
Go to previous page
</Button>
)}
</FullPageEmptyState>
);
}
if (data?.length === 0) {
return (
<FullPageEmptyState title="No profiles here" icon={GanttChartIcon}>
<p>Could not find any profiles</p>
{'cursor' in props && props.cursor !== 0 && (
<Button
className="mt-8"
variant="outline"
onClick={() => props.setCursor((p) => p - 1)}
>
Go to previous page
</Button>
<>
<DataTable data={data ?? []} columns={columns} />
{'cursor' in props && (
<Pagination
className="mt-2"
setCursor={props.setCursor}
cursor={props.cursor}
count={Infinity}
take={50}
loading={isFetching}
/>
)}
</FullPageEmptyState>
</>
);
},
(prevProps, nextProps) => {
return isEqual(prevProps.query.data, nextProps.query.data);
}
);
return (
<>
<DataTable data={data ?? []} columns={columns} />
{'cursor' in props && (
<Pagination
className="mt-2"
setCursor={props.setCursor}
cursor={props.cursor}
count={Infinity}
take={50}
loading={isFetching}
/>
)}
</>
);
};
ProfilesTable.displayName = 'ProfilesTable';

View File

@@ -66,6 +66,7 @@ interface GetProfileListOptions {
take: number;
cursor?: number;
filters?: IChartEventFilter[];
search?: string;
}
export async function getProfiles(ids: string[]) {
@@ -90,6 +91,7 @@ export async function getProfileList({
cursor,
projectId,
filters,
search,
}: GetProfileListOptions) {
const { sb, getSql } = createSqlBuilder();
sb.from = 'profiles FINAL';
@@ -98,6 +100,13 @@ export async function getProfileList({
sb.limit = take;
sb.offset = Math.max(0, (cursor ?? 0) * take);
sb.orderBy.created_at = 'created_at DESC';
if (search) {
if (search.includes('@')) {
sb.where.email = `email ILIKE '%${search}%'`;
} else {
sb.where.first_name = `first_name ILIKE '%${search}%' OR last_name ILIKE '%${search}%'`;
}
}
const data = await chQuery<IClickhouseProfile>(getSql());
return data.map(transformProfile);
}
@@ -107,7 +116,7 @@ export async function getProfileListCount({
filters,
}: Omit<GetProfileListOptions, 'cursor' | 'take'>) {
const { sb, getSql } = createSqlBuilder();
sb.from = 'profiles FINAL';
sb.from = 'profiles';
sb.select.count = 'count(id) as count';
sb.where.project_id = `project_id = ${escape(projectId)}`;
sb.groupBy.project_id = 'project_id';

View File

@@ -40,11 +40,12 @@ export const profileRouter = createTRPCRouter({
projectId: z.string(),
cursor: z.number().optional(),
take: z.number().default(50),
search: z.string().optional(),
// filters: z.array(zChartEventFilter).default([]),
})
)
.query(async ({ input: { projectId, cursor, take } }) => {
return getProfileList({ projectId, cursor, take });
.query(async ({ input: { projectId, cursor, take, search } }) => {
return getProfileList({ projectId, cursor, take, search });
}),
powerUsers: protectedProcedure