From 7d37e24c647218b393a7588a6e7842265f1e8cf8 Mon Sep 17 00:00:00 2001 From: chronark Date: Fri, 20 Dec 2024 11:35:42 +0100 Subject: [PATCH] fix: audit log filters use search params --- .../components/filters/bucket-select.tsx | 58 ------------------- .../app/(app)/audit/[bucket]/page.tsx | 46 --------------- .../app/(app)/audit/{[bucket] => }/actions.ts | 15 ++--- .../components/filters/bucket-select.tsx | 48 +++++++++++++++ .../components/filters/clear-button.tsx | 0 .../components/filters/filter.tsx | 2 +- .../components/filters/index.tsx | 17 +++--- .../components/filters/root-key-filter.tsx | 8 +-- .../components/filters/user-filter.tsx | 1 + .../table/audit-log-table-client.tsx | 2 +- .../components/table/columns.tsx | 0 .../components/table/constants.ts | 0 .../components/table/log-footer.tsx | 0 .../components/table/log-header.tsx | 0 .../components/table/table-details.tsx | 2 +- .../{[bucket] => }/components/table/types.ts | 0 .../{[bucket] => }/components/table/utils.ts | 0 apps/dashboard/app/(app)/audit/page.tsx | 56 +++++++++++++++++- .../(app)/audit/{[bucket] => }/query-state.ts | 0 .../dashboard/lib/trpc/routers/audit/fetch.ts | 13 +++-- 20 files changed, 127 insertions(+), 141 deletions(-) delete mode 100644 apps/dashboard/app/(app)/audit/[bucket]/components/filters/bucket-select.tsx delete mode 100644 apps/dashboard/app/(app)/audit/[bucket]/page.tsx rename apps/dashboard/app/(app)/audit/{[bucket] => }/actions.ts (84%) create mode 100644 apps/dashboard/app/(app)/audit/components/filters/bucket-select.tsx rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/filters/clear-button.tsx (100%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/filters/filter.tsx (99%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/filters/index.tsx (82%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/filters/root-key-filter.tsx (68%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/filters/user-filter.tsx (99%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/audit-log-table-client.tsx (98%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/columns.tsx (100%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/constants.ts (100%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/log-footer.tsx (100%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/log-header.tsx (100%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/table-details.tsx (95%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/types.ts (100%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/components/table/utils.ts (100%) rename apps/dashboard/app/(app)/audit/{[bucket] => }/query-state.ts (100%) diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/bucket-select.tsx b/apps/dashboard/app/(app)/audit/[bucket]/components/filters/bucket-select.tsx deleted file mode 100644 index f42f4a2424..0000000000 --- a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/bucket-select.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; -import { Loading } from "@/components/dashboard/loading"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { useRouter } from "next/navigation"; -import type React from "react"; -import { useTransition } from "react"; - -type Props = { - selected: string; - ratelimitNamespaces: { id: string; name: string }[]; -}; - -export const BucketSelect: React.FC = ({ ratelimitNamespaces, selected }) => { - const [isPending, startTransition] = useTransition(); - const router = useRouter(); - - const options = [ - { value: "unkey_mutations", label: "System" }, - ...ratelimitNamespaces.map((ns) => ({ - value: ns.id, - label: `Ratelimit: ${ns.name}`, - })), - ]; - - return ( -
- -
- ); -}; diff --git a/apps/dashboard/app/(app)/audit/[bucket]/page.tsx b/apps/dashboard/app/(app)/audit/[bucket]/page.tsx deleted file mode 100644 index 3d07406c10..0000000000 --- a/apps/dashboard/app/(app)/audit/[bucket]/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Navbar } from "@/components/navbar"; -import { PageContent } from "@/components/page-content"; -import { getTenantId } from "@/lib/auth"; -import { InputSearch } from "@unkey/icons"; -import { type SearchParams, getWorkspace, parseFilterParams } from "./actions"; -import { Filters } from "./components/filters"; -import { AuditLogTableClient } from "./components/table/audit-log-table-client"; - -export const dynamic = "force-dynamic"; -export const runtime = "edge"; - -type Props = { - params: { - bucket: string; - }; - searchParams: SearchParams; -}; - -export default async function AuditPage(props: Props) { - const tenantId = getTenantId(); - const workspace = await getWorkspace(tenantId); - const parsedParams = parseFilterParams({ - ...props.searchParams, - bucket: props.params.bucket, - }); - - return ( -
- - }> - Audit - - {workspace.ratelimitNamespaces.find((ratelimit) => ratelimit.id === props.params.bucket) - ?.name ?? props.params.bucket} - - - - -
- - -
-
-
- ); -} diff --git a/apps/dashboard/app/(app)/audit/[bucket]/actions.ts b/apps/dashboard/app/(app)/audit/actions.ts similarity index 84% rename from apps/dashboard/app/(app)/audit/[bucket]/actions.ts rename to apps/dashboard/app/(app)/audit/actions.ts index 0854d45d97..a6a00b3493 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/actions.ts +++ b/apps/dashboard/app/(app)/audit/actions.ts @@ -8,12 +8,12 @@ export const getWorkspace = async (tenantId: string) => { where: (table, { eq, and, isNull }) => and(eq(table.tenantId, tenantId), isNull(table.deletedAt)), with: { - ratelimitNamespaces: { - where: (table, { isNull }) => isNull(table.deletedAt), + auditLogBuckets: { columns: { id: true, name: true, }, + orderBy: (table, { asc }) => asc(table.createdAt), }, }, }); @@ -39,10 +39,7 @@ export type SearchParams = { startTime?: string | string[]; endTime?: string | string[]; cursor?: string | string[]; -}; - -type ParseFilterInput = SearchParams & { - bucket: string; + bucketName?: string; }; export type ParsedParams = { @@ -51,11 +48,11 @@ export type ParsedParams = { selectedRootKeys: string[]; startTime: number | null; endTime: number | null; - bucket: string | null; + bucketName: string; cursor: string | null; }; -export const parseFilterParams = (params: ParseFilterInput): ParsedParams => { +export const parseFilterParams = (params: SearchParams): ParsedParams => { const filterParser = parseAsArrayOf(parseAsString).withDefault([]); const timeParser = parseAsInteger; const bucketParser = parseAsString; @@ -66,7 +63,7 @@ export const parseFilterParams = (params: ParseFilterInput): ParsedParams => { selectedRootKeys: filterParser.parseServerSide(params.rootKeys), startTime: timeParser.parseServerSide(params.startTime), endTime: timeParser.parseServerSide(params.endTime), - bucket: bucketParser.parseServerSide(params.bucket), + bucketName: bucketParser.withDefault("unkey_mutations").parseServerSide(params.bucketName), cursor: bucketParser.parseServerSide(params.cursor), }; }; diff --git a/apps/dashboard/app/(app)/audit/components/filters/bucket-select.tsx b/apps/dashboard/app/(app)/audit/components/filters/bucket-select.tsx new file mode 100644 index 0000000000..0e4538821a --- /dev/null +++ b/apps/dashboard/app/(app)/audit/components/filters/bucket-select.tsx @@ -0,0 +1,48 @@ +"use client"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import { parseAsString, useQueryState } from "nuqs"; +import type React from "react"; + +type Props = { + buckets: { id: string; name: string }[]; +}; + +export const BucketSelect: React.FC = ({ buckets }) => { + const [selected, setSelected] = useQueryState( + "bucket", + parseAsString.withDefault("unkey_mutations").withOptions({ + history: "push", + shallow: false, // otherwise server components won't notice the change + clearOnDefault: true, + }), + ); + + return ( +
+ +
+ ); +}; diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/clear-button.tsx b/apps/dashboard/app/(app)/audit/components/filters/clear-button.tsx similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/components/filters/clear-button.tsx rename to apps/dashboard/app/(app)/audit/components/filters/clear-button.tsx diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/filter.tsx b/apps/dashboard/app/(app)/audit/components/filters/filter.tsx similarity index 99% rename from apps/dashboard/app/(app)/audit/[bucket]/components/filters/filter.tsx rename to apps/dashboard/app/(app)/audit/components/filters/filter.tsx index b51ecc3bbe..58b1dd4713 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/filter.tsx +++ b/apps/dashboard/app/(app)/audit/components/filters/filter.tsx @@ -74,7 +74,7 @@ export const Filter: React.FC = ({ options, title, param }) => { - + No results found. diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/index.tsx b/apps/dashboard/app/(app)/audit/components/filters/index.tsx similarity index 82% rename from apps/dashboard/app/(app)/audit/[bucket]/components/filters/index.tsx rename to apps/dashboard/app/(app)/audit/components/filters/index.tsx index 8718b4e098..39ae9c9813 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/index.tsx +++ b/apps/dashboard/app/(app)/audit/components/filters/index.tsx @@ -1,6 +1,6 @@ import { DatePickerWithRange } from "@/app/(app)/logs/components/filters/components/custom-date-filter"; import { DEFAULT_BUCKET_NAME } from "@/lib/trpc/routers/audit/fetch"; -import type { ratelimitNamespaces, workspaces } from "@unkey/db/src/schema"; +import type { auditLogBucket, workspaces } from "@unkey/db/src/schema"; import { unkeyAuditLogEvents } from "@unkey/schema/src/auditlog"; import { Button } from "@unkey/ui"; import { Suspense } from "react"; @@ -12,29 +12,26 @@ import { RootKeyFilter } from "./root-key-filter"; import { UserFilter } from "./user-filter"; export type SelectWorkspace = typeof workspaces.$inferSelect & { - ratelimitNamespaces: Pick[]; + auditLogBuckets: Pick[]; }; export const Filters = ({ - bucket, + selectedBucketName, workspace, parsedParams, }: { - bucket: string | null; + selectedBucketName: string; workspace: SelectWorkspace; parsedParams: ParsedParams; }) => { return (
- + ({ value, label: value, @@ -48,7 +45,7 @@ export const Filters = ({ ] } /> - {bucket === DEFAULT_BUCKET_NAME ? ( + {selectedBucketName === DEFAULT_BUCKET_NAME ? ( }> diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/root-key-filter.tsx b/apps/dashboard/app/(app)/audit/components/filters/root-key-filter.tsx similarity index 68% rename from apps/dashboard/app/(app)/audit/[bucket]/components/filters/root-key-filter.tsx rename to apps/dashboard/app/(app)/audit/components/filters/root-key-filter.tsx index 9597b623f2..17845bc760 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/root-key-filter.tsx +++ b/apps/dashboard/app/(app)/audit/components/filters/root-key-filter.tsx @@ -5,12 +5,8 @@ import { Filter } from "./filter"; export const RootKeyFilter: React.FC<{ workspaceId: string }> = async ({ workspaceId }) => { const rootKeys = await db.query.keys.findMany({ - where: (table, { eq, and, or, isNull, gt }) => - and( - eq(table.forWorkspaceId, workspaceId), - isNull(table.deletedAt), - or(isNull(table.expires), gt(table.expires, new Date())), - ), + where: (table, { eq }) => eq(table.forWorkspaceId, workspaceId), + columns: { id: true, name: true, diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/user-filter.tsx b/apps/dashboard/app/(app)/audit/components/filters/user-filter.tsx similarity index 99% rename from apps/dashboard/app/(app)/audit/[bucket]/components/filters/user-filter.tsx rename to apps/dashboard/app/(app)/audit/components/filters/user-filter.tsx index 90a8e5cef2..a3b7ec7eaf 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/components/filters/user-filter.tsx +++ b/apps/dashboard/app/(app)/audit/components/filters/user-filter.tsx @@ -7,6 +7,7 @@ export const UserFilter: React.FC<{ tenantId: string }> = async ({ tenantId }) = if (tenantId.startsWith("user_")) { return null; } + const members = await clerkClient.organizations.getOrganizationMembershipList({ organizationId: tenantId, }); diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/audit-log-table-client.tsx b/apps/dashboard/app/(app)/audit/components/table/audit-log-table-client.tsx similarity index 98% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/audit-log-table-client.tsx rename to apps/dashboard/app/(app)/audit/components/table/audit-log-table-client.tsx index 07857da628..e0d9255fa1 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/components/table/audit-log-table-client.tsx +++ b/apps/dashboard/app/(app)/audit/components/table/audit-log-table-client.tsx @@ -19,7 +19,7 @@ export const AuditLogTableClient = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = trpc.audit.fetch.useInfiniteQuery( { - bucket: searchParams.bucket ?? undefined, + bucketName: searchParams.bucket ?? undefined, limit: DEFAULT_FETCH_COUNT, users: searchParams.users, events: searchParams.events, diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/columns.tsx b/apps/dashboard/app/(app)/audit/components/table/columns.tsx similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/columns.tsx rename to apps/dashboard/app/(app)/audit/components/table/columns.tsx diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/constants.ts b/apps/dashboard/app/(app)/audit/components/table/constants.ts similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/constants.ts rename to apps/dashboard/app/(app)/audit/components/table/constants.ts diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/log-footer.tsx b/apps/dashboard/app/(app)/audit/components/table/log-footer.tsx similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/log-footer.tsx rename to apps/dashboard/app/(app)/audit/components/table/log-footer.tsx diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/log-header.tsx b/apps/dashboard/app/(app)/audit/components/table/log-header.tsx similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/log-header.tsx rename to apps/dashboard/app/(app)/audit/components/table/log-header.tsx diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/table-details.tsx b/apps/dashboard/app/(app)/audit/components/table/table-details.tsx similarity index 95% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/table-details.tsx rename to apps/dashboard/app/(app)/audit/components/table/table-details.tsx index 503e445cda..6f3ad97f98 100644 --- a/apps/dashboard/app/(app)/audit/[bucket]/components/table/table-details.tsx +++ b/apps/dashboard/app/(app)/audit/components/table/table-details.tsx @@ -3,7 +3,7 @@ import { LogSection } from "@/app/(app)/logs/components/table/log-details/components/log-section"; import { memo, useMemo, useState } from "react"; import { useDebounceCallback } from "usehooks-ts"; -import ResizablePanel from "../../../../logs/components/table/log-details/resizable-panel"; +import ResizablePanel from "../../../logs/components/table/log-details/resizable-panel"; import { LogFooter } from "./log-footer"; import { LogHeader } from "./log-header"; import type { Data } from "./types"; diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/types.ts b/apps/dashboard/app/(app)/audit/components/table/types.ts similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/types.ts rename to apps/dashboard/app/(app)/audit/components/table/types.ts diff --git a/apps/dashboard/app/(app)/audit/[bucket]/components/table/utils.ts b/apps/dashboard/app/(app)/audit/components/table/utils.ts similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/components/table/utils.ts rename to apps/dashboard/app/(app)/audit/components/table/utils.ts diff --git a/apps/dashboard/app/(app)/audit/page.tsx b/apps/dashboard/app/(app)/audit/page.tsx index 3e615c7064..62aca04759 100644 --- a/apps/dashboard/app/(app)/audit/page.tsx +++ b/apps/dashboard/app/(app)/audit/page.tsx @@ -1,5 +1,55 @@ -import { redirect } from "next/navigation"; +import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder"; +import { Navbar } from "@/components/navbar"; +import { PageContent } from "@/components/page-content"; +import { getTenantId } from "@/lib/auth"; +import { InputSearch, Ufo } from "@unkey/icons"; +import { type SearchParams, getWorkspace, parseFilterParams } from "./actions"; +import { Filters } from "./components/filters"; +import { AuditLogTableClient } from "./components/table/audit-log-table-client"; -export default function Page() { - return redirect("/audit/unkey_mutations"); +export const dynamic = "force-dynamic"; +export const runtime = "edge"; + +type Props = { + searchParams: SearchParams; +}; + +export default async function AuditPage(props: Props) { + const tenantId = getTenantId(); + const workspace = await getWorkspace(tenantId); + const parsedParams = parseFilterParams(props.searchParams); + + return ( +
+ + }> + Audit + + + + {workspace.auditLogBuckets.length > 0 ? ( +
+ + + +
+ ) : ( + + + + + No logs + + There are no audit logs available yet. Create a key or another resource and come back + here. + + + )} +
+
+ ); } diff --git a/apps/dashboard/app/(app)/audit/[bucket]/query-state.ts b/apps/dashboard/app/(app)/audit/query-state.ts similarity index 100% rename from apps/dashboard/app/(app)/audit/[bucket]/query-state.ts rename to apps/dashboard/app/(app)/audit/query-state.ts diff --git a/apps/dashboard/lib/trpc/routers/audit/fetch.ts b/apps/dashboard/lib/trpc/routers/audit/fetch.ts index 4f3eefe245..765bb0093f 100644 --- a/apps/dashboard/lib/trpc/routers/audit/fetch.ts +++ b/apps/dashboard/lib/trpc/routers/audit/fetch.ts @@ -1,4 +1,4 @@ -import { DEFAULT_FETCH_COUNT } from "@/app/(app)/audit/[bucket]/components/table/constants"; +import { DEFAULT_FETCH_COUNT } from "@/app/(app)/audit/components/table/constants"; import { type Workspace, db } from "@/lib/db"; import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure"; import { type User, clerkClient } from "@clerk/nextjs/server"; @@ -11,7 +11,7 @@ export type AuditLogWithTargets = SelectAuditLog & { targets: Array; }; export const getAuditLogsInput = z.object({ - bucket: z.string().default(DEFAULT_BUCKET_NAME), + bucketName: z.string().default(DEFAULT_BUCKET_NAME), events: z.array(z.string()).default([]), users: z.array(z.string()).default([]), rootKeys: z.array(z.string()).default([]), @@ -24,7 +24,7 @@ export const getAuditLogsInput = z.object({ export const fetchAuditLog = rateLimitedProcedure(ratelimit.update) .input(getAuditLogsInput) .query(async ({ ctx, input }) => { - const { bucket, events, users, rootKeys, cursor, limit, endTime, startTime } = input; + const { bucketName, events, users, rootKeys, cursor, limit, endTime, startTime } = input; const workspace = await db.query.workspaces .findFirst({ @@ -52,7 +52,7 @@ export const fetchAuditLog = rateLimitedProcedure(ratelimit.update) { cursor, users: selectedActorIds, - bucket, + bucketName, endTime, startTime, events, @@ -115,7 +115,7 @@ export type QueryOptions = Omit, "rootKeys">; export const queryAuditLogs = async (options: QueryOptions, workspace: Workspace) => { const { - bucket, + bucketName, events = [], startTime, endTime, @@ -129,7 +129,8 @@ export const queryAuditLogs = async (options: QueryOptions, workspace: Workspace const retentionCutoffUnixMilli = Date.now() - retentionDays * 24 * 60 * 60 * 1000; return db.query.auditLogBucket.findFirst({ - where: (table, { eq, and }) => and(eq(table.workspaceId, workspace.id), eq(table.name, bucket)), + where: (table, { eq, and }) => + and(eq(table.workspaceId, workspace.id), eq(table.name, bucketName)), with: { logs: { where: (table, { and, inArray, between, lt }) =>