From 40468c081f8567d8fb3734ed32e42370d9de1a54 Mon Sep 17 00:00:00 2001 From: taylor Date: Fri, 20 Mar 2026 12:41:57 -0600 Subject: [PATCH 1/5] initial commit --- .../(rest)/connectors/assets/page.tsx | 26 +-- .../connectors/deviceArtifacts/page.tsx | 26 +-- .../(rest)/connectors/remediations/page.tsx | 26 +-- .../connectors/vulnerabilities/page.tsx | 26 +-- .../api-key-connectors/components/columns.tsx | 70 ++++++++ .../components/connectors.tsx | 157 ++++++++++++++++++ .../hooks/use-connectors.ts | 23 +++ src/features/api-key-connectors/params.ts | 3 + .../server/params-loader.ts | 4 + .../api-key-connectors/server/prefetch.ts | 37 +++++ .../api-key-connectors/server/routers.ts | 36 ++-- src/features/api-key-connectors/types.ts | 50 ++++++ src/features/assets/components/asset.tsx | 4 +- 13 files changed, 421 insertions(+), 67 deletions(-) create mode 100644 src/features/api-key-connectors/components/columns.tsx create mode 100644 src/features/api-key-connectors/components/connectors.tsx create mode 100644 src/features/api-key-connectors/params.ts create mode 100644 src/features/api-key-connectors/server/params-loader.ts create mode 100644 src/features/api-key-connectors/server/prefetch.ts create mode 100644 src/features/api-key-connectors/types.ts diff --git a/src/app/(dashboard)/(rest)/connectors/assets/page.tsx b/src/app/(dashboard)/(rest)/connectors/assets/page.tsx index 5f18e108..011bc502 100644 --- a/src/app/(dashboard)/(rest)/connectors/assets/page.tsx +++ b/src/app/(dashboard)/(rest)/connectors/assets/page.tsx @@ -1,18 +1,18 @@ import { - AssetsContainer, - AssetsError, - AssetsList, - AssetsLoading, -} from "@/features/assets/components/assets"; -import { assetsParamsLoader } from "@/features/assets/server/params-loader"; -import { prefetchAssets } from "@/features/assets/server/prefetch"; + AssetConnectorList, + AssetConnectorsContainer, + ConnectorsError, + ConnectorsLoading, +} from "@/features/api-key-connectors/components/connectors"; +import { connectorsParamsLoader } from "@/features/api-key-connectors/server/params-loader"; +import { prefetchAssetConnectors } from "@/features/api-key-connectors/server/prefetch"; import { createListPage } from "@/lib/page-factory"; export default createListPage({ - paramsLoader: assetsParamsLoader, - prefetch: prefetchAssets, - Container: AssetsContainer, - List: AssetsList, - Loading: AssetsLoading, - Error: AssetsError, + paramsLoader: connectorsParamsLoader, + prefetch: prefetchAssetConnectors, + Container: AssetConnectorsContainer, + List: AssetConnectorList, + Loading: ConnectorsLoading, + Error: ConnectorsError, }); diff --git a/src/app/(dashboard)/(rest)/connectors/deviceArtifacts/page.tsx b/src/app/(dashboard)/(rest)/connectors/deviceArtifacts/page.tsx index ad511eaa..4bd16e48 100644 --- a/src/app/(dashboard)/(rest)/connectors/deviceArtifacts/page.tsx +++ b/src/app/(dashboard)/(rest)/connectors/deviceArtifacts/page.tsx @@ -1,18 +1,18 @@ import { - DeviceArtifactsContainer, - DeviceArtifactsError, - DeviceArtifactsList, - DeviceArtifactsLoading, -} from "@/features/device-artifacts/components/device-artifacts"; -import { deviceArtifactsParamsLoader } from "@/features/device-artifacts/server/params-loader"; -import { prefetchDeviceArtifacts } from "@/features/device-artifacts/server/prefetch"; + ConnectorsError, + ConnectorsLoading, + DeviceArtifactConnectorList, + DeviceArtifactConnectorsContainer, +} from "@/features/api-key-connectors/components/connectors"; +import { connectorsParamsLoader } from "@/features/api-key-connectors/server/params-loader"; +import { prefetchDeviceArtifactConnectors } from "@/features/api-key-connectors/server/prefetch"; import { createListPage } from "@/lib/page-factory"; export default createListPage({ - paramsLoader: deviceArtifactsParamsLoader, - prefetch: prefetchDeviceArtifacts, - Container: DeviceArtifactsContainer, - List: DeviceArtifactsList, - Loading: DeviceArtifactsLoading, - Error: DeviceArtifactsError, + paramsLoader: connectorsParamsLoader, + prefetch: prefetchDeviceArtifactConnectors, + Container: DeviceArtifactConnectorsContainer, + List: DeviceArtifactConnectorList, + Loading: ConnectorsLoading, + Error: ConnectorsError, }); diff --git a/src/app/(dashboard)/(rest)/connectors/remediations/page.tsx b/src/app/(dashboard)/(rest)/connectors/remediations/page.tsx index 8a2bb4fa..6e1b6bbf 100644 --- a/src/app/(dashboard)/(rest)/connectors/remediations/page.tsx +++ b/src/app/(dashboard)/(rest)/connectors/remediations/page.tsx @@ -1,18 +1,18 @@ import { - RemediationsContainer, - RemediationsError, - RemediationsList, - RemediationsLoading, -} from "@/features/remediations/components/remediations"; -import { remediationsParamsLoader } from "@/features/remediations/server/params-loader"; -import { prefetchRemediations } from "@/features/remediations/server/prefetch"; + ConnectorsError, + ConnectorsLoading, + RemediationConnectorList, + RemediationConnectorsContainer, +} from "@/features/api-key-connectors/components/connectors"; +import { connectorsParamsLoader } from "@/features/api-key-connectors/server/params-loader"; +import { prefetchRemediationConnectors } from "@/features/api-key-connectors/server/prefetch"; import { createListPage } from "@/lib/page-factory"; export default createListPage({ - paramsLoader: remediationsParamsLoader, - prefetch: prefetchRemediations, - Container: RemediationsContainer, - List: RemediationsList, - Loading: RemediationsLoading, - Error: RemediationsError, + paramsLoader: connectorsParamsLoader, + prefetch: prefetchRemediationConnectors, + Container: RemediationConnectorsContainer, + List: RemediationConnectorList, + Loading: ConnectorsLoading, + Error: ConnectorsError, }); diff --git a/src/app/(dashboard)/(rest)/connectors/vulnerabilities/page.tsx b/src/app/(dashboard)/(rest)/connectors/vulnerabilities/page.tsx index 4bf9c17a..fb054f44 100644 --- a/src/app/(dashboard)/(rest)/connectors/vulnerabilities/page.tsx +++ b/src/app/(dashboard)/(rest)/connectors/vulnerabilities/page.tsx @@ -1,18 +1,18 @@ import { - VulnerabilitiesContainer, - VulnerabilitiesError, - VulnerabilitiesList, - VulnerabilitiesLoading, -} from "@/features/vulnerabilities/components/vulnerabilities"; -import { vulnerabilitiesParamsLoader } from "@/features/vulnerabilities/server/params-loader"; -import { prefetchVulnerabilities } from "@/features/vulnerabilities/server/prefetch"; + ConnectorsError, + ConnectorsLoading, + VulnerabilityConnectorList, + VulnerabilityConnectorsContainer, +} from "@/features/api-key-connectors/components/connectors"; +import { connectorsParamsLoader } from "@/features/api-key-connectors/server/params-loader"; +import { prefetchVulnerabilityConnectors } from "@/features/api-key-connectors/server/prefetch"; import { createListPage } from "@/lib/page-factory"; export default createListPage({ - paramsLoader: vulnerabilitiesParamsLoader, - prefetch: prefetchVulnerabilities, - Container: VulnerabilitiesContainer, - List: VulnerabilitiesList, - Loading: VulnerabilitiesLoading, - Error: VulnerabilitiesError, + paramsLoader: connectorsParamsLoader, + prefetch: prefetchVulnerabilityConnectors, + Container: VulnerabilityConnectorsContainer, + List: VulnerabilityConnectorList, + Loading: ConnectorsLoading, + Error: ConnectorsError, }); diff --git a/src/features/api-key-connectors/components/columns.tsx b/src/features/api-key-connectors/components/columns.tsx new file mode 100644 index 00000000..c816c2c4 --- /dev/null +++ b/src/features/api-key-connectors/components/columns.tsx @@ -0,0 +1,70 @@ +"use client"; + +import type { ColumnDef } from "@tanstack/react-table"; +import { Badge } from "@/components/ui/badge"; +import { SortableHeader } from "@/components/ui/data-table"; +import type { ConnectorResponse } from "../types"; + +export const columns: ColumnDef[] = [ + { + id: "name", + accessorKey: "name", + header: ({ column }) => ( + + ), + }, + { + accessorKey: "Username", + meta: { title: "Username" }, + header: "Username", + accessorFn: (row) => row.user.name, + }, + { + id: "lastRequest", + accessorKey: "lastRequest", + header: ({ column }) => ( + + ), + cell: ({ getValue }) => { + return getValue() ?? "Never"; + }, + }, + { + id: "status", + accessorKey: "status", + header: "Status", + cell: ({ row }) => { + let text = "Active"; + let color = "bg-green-300"; + if (!row.original.apiKeyId) { + text = "Expired"; + color = "bg-red-400"; + } + + return ( + + {text} + + ); + }, + }, + { + id: "type", + accessorKey: "type", + header: "Type", + cell: ({ row }) => { + let text = "API Key"; + let color = "bg-blue-300"; + if (row.original.integrationId) { + text = "Integration"; + color = "bg-yellow-300"; + } + + return ( + + {text} + + ); + }, + }, +]; diff --git a/src/features/api-key-connectors/components/connectors.tsx b/src/features/api-key-connectors/components/connectors.tsx new file mode 100644 index 00000000..45d71a7d --- /dev/null +++ b/src/features/api-key-connectors/components/connectors.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { + EmptyView, + EntityContainer, + EntityHeader, + EntitySearch, + ErrorView, + LoadingView, +} from "@/components/entity-components"; +import { DataTable } from "@/components/ui/data-table"; +import { ResourceType } from "@/generated/prisma"; +import { useEntitySearch } from "@/hooks/use-entity-search"; +import { + useConnectorParams, + useSuspenseConnectorsByResourceType, +} from "../hooks/use-connectors"; +import { columns } from "./columns"; + +export const ConnectorsLoading = () => { + return ; +}; + +export const ConnectorsError = () => { + return ; +}; + +export const ConnectorsEmpty = () => { + return ( + + ); +}; + +const ConnectorsHeader = ({ + disabled, + resourceType, +}: { + disabled?: boolean; + resourceType: string; +}) => { + return ( + + ); +}; + +const ConnectorsContainer = ({ + children, + resourceType, +}: { + children: React.ReactNode; + resourceType: ResourceType | string; +}) => { + return ( + }> + {children} + + ); +}; + +const ConnectorsSearch = () => { + const [params, setParams] = useConnectorParams(); + const { searchValue, onSearchChange } = useEntitySearch({ + params, + setParams, + }); + + return ( + + ); +}; + +const ConnectorsList = ({ resourceType }: { resourceType: ResourceType }) => { + const { data: connectors, isFetching } = useSuspenseConnectorsByResourceType({ + resourceType, + }); + + return ( + } + /> + ); +}; + +export const AssetConnectorsContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; + +export const DeviceArtifactConnectorsContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; + +export const RemediationConnectorsContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; + +export const VulnerabilityConnectorsContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; + +export const AssetConnectorList = () => { + return ; +}; + +export const DeviceArtifactConnectorList = () => { + return ; +}; + +export const RemediationConnectorList = () => { + return ; +}; + +export const VulnerabilityConnectorList = () => { + return ; +}; diff --git a/src/features/api-key-connectors/hooks/use-connectors.ts b/src/features/api-key-connectors/hooks/use-connectors.ts index ffa25e5c..86c662d8 100644 --- a/src/features/api-key-connectors/hooks/use-connectors.ts +++ b/src/features/api-key-connectors/hooks/use-connectors.ts @@ -1,5 +1,8 @@ import { useSuspenseQuery } from "@tanstack/react-query"; +import { useQueryStates } from "nuqs"; +import type { ResourceType } from "@/generated/prisma"; import { useTRPC } from "@/trpc/client"; +import { connectorsParams } from "../params"; export const useSuspenseConnectors = () => { const trpc = useTRPC(); @@ -8,3 +11,23 @@ export const useSuspenseConnectors = () => { trpc.apiKeyConnectors.getManyTypeCountInternal.queryOptions(), ); }; + +export const useConnectorParams = () => { + return useQueryStates(connectorsParams); +}; + +export const useSuspenseConnectorsByResourceType = ({ + resourceType, +}: { + resourceType: ResourceType; +}) => { + const trpc = useTRPC(); + const [params] = useConnectorParams(); + + return useSuspenseQuery( + trpc.apiKeyConnectors.getManyByTypeInternal.queryOptions({ + ...params, + resourceType, + }), + ); +}; diff --git a/src/features/api-key-connectors/params.ts b/src/features/api-key-connectors/params.ts new file mode 100644 index 00000000..df4082fb --- /dev/null +++ b/src/features/api-key-connectors/params.ts @@ -0,0 +1,3 @@ +import { createPaginationParams } from "@/lib/url-state"; + +export const connectorsParams = createPaginationParams(); diff --git a/src/features/api-key-connectors/server/params-loader.ts b/src/features/api-key-connectors/server/params-loader.ts new file mode 100644 index 00000000..d64a0040 --- /dev/null +++ b/src/features/api-key-connectors/server/params-loader.ts @@ -0,0 +1,4 @@ +import { createLoader } from "nuqs/server"; +import { connectorsParams } from "../params"; + +export const connectorsParamsLoader = createLoader(connectorsParams); diff --git a/src/features/api-key-connectors/server/prefetch.ts b/src/features/api-key-connectors/server/prefetch.ts new file mode 100644 index 00000000..5a1db2e8 --- /dev/null +++ b/src/features/api-key-connectors/server/prefetch.ts @@ -0,0 +1,37 @@ +import type { inferInput } from "@trpc/tanstack-react-query"; +import { ResourceType } from "@/generated/prisma"; +import { prefetch, trpc } from "@/trpc/server"; + +// endpoint takes in a resourceType but we don't pass that value via params +type Input = Omit< + inferInput, + "resourceType" +>; + +const prefetchConnectorsByType = ( + params: Input, + resourceType: ResourceType, +) => { + return prefetch( + trpc.apiKeyConnectors.getManyByTypeInternal.queryOptions({ + ...params, + resourceType, + }), + ); +}; + +export const prefetchAssetConnectors = (params: Input) => { + return prefetchConnectorsByType(params, ResourceType.Asset); +}; + +export const prefetchDeviceArtifactConnectors = (params: Input) => { + return prefetchConnectorsByType(params, ResourceType.DeviceArtifact); +}; + +export const prefetchRemediationConnectors = (params: Input) => { + return prefetchConnectorsByType(params, ResourceType.Remediation); +}; + +export const prefetchVulnerabilityConnectors = (params: Input) => { + return prefetchConnectorsByType(params, ResourceType.Vulnerability); +}; diff --git a/src/features/api-key-connectors/server/routers.ts b/src/features/api-key-connectors/server/routers.ts index 97132aa9..44c502a1 100644 --- a/src/features/api-key-connectors/server/routers.ts +++ b/src/features/api-key-connectors/server/routers.ts @@ -1,21 +1,33 @@ -import z from "zod"; import { ResourceType } from "@/generated/prisma"; import prisma from "@/lib/db"; +import { fetchPaginated } from "@/lib/router-utils"; import { createTRPCRouter, protectedProcedure } from "@/trpc/init"; +import { + connectorCountResponseSchema, + connectorInclude, + paginatedConnectorInputSchema, + paginatedConnectorOutputSchema, +} from "../types"; -// { "Asset": 2, "Vulnerability": 5, "Remediation": 3, ... } -const resourceTypeCountSchema = z.object( - Object.fromEntries( - Object.values(ResourceType).map((type) => [type, z.number()]), - ), -); +export const apiKeyConnectorsRouter = createTRPCRouter({ + getManyByTypeInternal: protectedProcedure + .input(paginatedConnectorInputSchema) + .output(paginatedConnectorOutputSchema) + .query(async ({ input }) => { + const { resourceType } = input; -const connectorCountResponseSchema = z.object({ - activeCount: resourceTypeCountSchema, - totalCount: resourceTypeCountSchema, -}); + const where = { + resourceType, + }; + + const data = await fetchPaginated(prisma.apiKeyConnector, input, { + where: where, + include: connectorInclude, + }); + + return data; + }), -export const apiKeyConnectorsRouter = createTRPCRouter({ getManyTypeCountInternal: protectedProcedure .output(connectorCountResponseSchema) .query(async () => { diff --git a/src/features/api-key-connectors/types.ts b/src/features/api-key-connectors/types.ts new file mode 100644 index 00000000..bce0ab26 --- /dev/null +++ b/src/features/api-key-connectors/types.ts @@ -0,0 +1,50 @@ +import { userSchema } from "better-auth"; +import z from "zod"; +import { ResourceType } from "@/generated/prisma"; +import { + createPaginatedResponseSchema, + paginationInputSchema, +} from "@/lib/pagination"; +import { userIncludeSelect } from "@/lib/schemas"; + +export const connectorResponseSchema = z.object({ + id: z.string(), + name: z.string().nullable(), + resourceType: z.string().nullable(), + lastRequest: z.date().nullable(), + createdAt: z.date(), + updatedAt: z.date(), + apiKeyId: z.string().nullable(), + integrationId: z.string().nullable(), + userId: z.string(), + user: userSchema, +}); + +export const paginatedConnectorInputSchema = paginationInputSchema.extend({ + resourceType: z.enum(ResourceType), +}); + +export const paginatedConnectorOutputSchema = createPaginatedResponseSchema( + connectorResponseSchema, +); + +export type ConnectorResponse = z.infer; +export type paginatedConnectorsByTypeInput = z.infer< + typeof paginatedConnectorInputSchema +>; + +// { "Asset": 2, "Vulnerability": 5, "Remediation": 3, ... } +export const resourceTypeCountSchema = z.object( + Object.fromEntries( + Object.values(ResourceType).map((type) => [type, z.number()]), + ), +); + +export const connectorCountResponseSchema = z.object({ + activeCount: resourceTypeCountSchema, + totalCount: resourceTypeCountSchema, +}); + +export const connectorInclude = { + user: userIncludeSelect, +} as const; diff --git a/src/features/assets/components/asset.tsx b/src/features/assets/components/asset.tsx index 2c69c70f..36b0c9d3 100644 --- a/src/features/assets/components/asset.tsx +++ b/src/features/assets/components/asset.tsx @@ -305,9 +305,7 @@ export const AssetDetailPage = ({ assetId }: AssetDetailProps) => { - - All Assets - + All Assets From 3d6278b6fad6dc2994b033b2c35e8db333f87a4c Mon Sep 17 00:00:00 2001 From: taylor Date: Fri, 20 Mar 2026 13:14:29 -0600 Subject: [PATCH 2/5] better timestamp --- .../api-key-connectors/components/columns.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/features/api-key-connectors/components/columns.tsx b/src/features/api-key-connectors/components/columns.tsx index c816c2c4..1e0856e6 100644 --- a/src/features/api-key-connectors/components/columns.tsx +++ b/src/features/api-key-connectors/components/columns.tsx @@ -1,8 +1,14 @@ "use client"; import type { ColumnDef } from "@tanstack/react-table"; +import { formatDistanceToNow } from "date-fns"; import { Badge } from "@/components/ui/badge"; import { SortableHeader } from "@/components/ui/data-table"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import type { ConnectorResponse } from "../types"; export const columns: ColumnDef[] = [ @@ -25,8 +31,18 @@ export const columns: ColumnDef[] = [ header: ({ column }) => ( ), - cell: ({ getValue }) => { - return getValue() ?? "Never"; + cell: ({ row }) => { + const value = row.original.lastRequest; + return ( + + + {value ? `${formatDistanceToNow(value)} ago` : "Never"} + + + {value ? value.toLocaleString() : "Never"} + + + ); }, }, { From 9d58ad74e7c4a57a00dbf6d0309875b0215034f0 Mon Sep 17 00:00:00 2001 From: taylor Date: Fri, 20 Mar 2026 13:45:32 -0600 Subject: [PATCH 3/5] feedback --- src/features/api-key-connectors/components/columns.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/api-key-connectors/components/columns.tsx b/src/features/api-key-connectors/components/columns.tsx index 1e0856e6..61249f43 100644 --- a/src/features/api-key-connectors/components/columns.tsx +++ b/src/features/api-key-connectors/components/columns.tsx @@ -50,11 +50,11 @@ export const columns: ColumnDef[] = [ accessorKey: "status", header: "Status", cell: ({ row }) => { - let text = "Active"; - let color = "bg-green-300"; - if (!row.original.apiKeyId) { - text = "Expired"; - color = "bg-red-400"; + let text = "Expired"; + let color = "bg-red-400"; + if (row.original.apiKeyId || row.original.integrationId) { + text = "Active"; + color = "bg-green-300"; } return ( From f1e605cee2d1c684431399b481cd003638206de5 Mon Sep 17 00:00:00 2001 From: taylor Date: Mon, 23 Mar 2026 11:21:35 -0600 Subject: [PATCH 4/5] logging --- src/app/api/trpc/[trpc]/route.ts | 3 +++ src/features/api-key-connectors/components/connectors.tsx | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/app/api/trpc/[trpc]/route.ts b/src/app/api/trpc/[trpc]/route.ts index e9a59906..b59e04cc 100644 --- a/src/app/api/trpc/[trpc]/route.ts +++ b/src/app/api/trpc/[trpc]/route.ts @@ -9,6 +9,9 @@ const handler = (req: Request) => router: appRouter, // @ts-expect-error createContext: createTRPCContext, + onError: ({ path, error }) => { + console.error(path, error); + }, }); export { handler as GET, handler as POST }; diff --git a/src/features/api-key-connectors/components/connectors.tsx b/src/features/api-key-connectors/components/connectors.tsx index 45d71a7d..c45f5b87 100644 --- a/src/features/api-key-connectors/components/connectors.tsx +++ b/src/features/api-key-connectors/components/connectors.tsx @@ -82,6 +82,8 @@ const ConnectorsList = ({ resourceType }: { resourceType: ResourceType }) => { resourceType, }); + console.log("useSuspenseConnectorsByResourceType:", connectors); + return ( Date: Mon, 23 Mar 2026 14:32:37 -0400 Subject: [PATCH 5/5] change user schema import path --- src/features/api-key-connectors/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/api-key-connectors/types.ts b/src/features/api-key-connectors/types.ts index bce0ab26..8a8ec5f6 100644 --- a/src/features/api-key-connectors/types.ts +++ b/src/features/api-key-connectors/types.ts @@ -1,11 +1,10 @@ -import { userSchema } from "better-auth"; import z from "zod"; import { ResourceType } from "@/generated/prisma"; import { createPaginatedResponseSchema, paginationInputSchema, } from "@/lib/pagination"; -import { userIncludeSelect } from "@/lib/schemas"; +import { userIncludeSelect, userSchema } from "@/lib/schemas"; export const connectorResponseSchema = z.object({ id: z.string(),