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/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/columns.tsx b/src/features/api-key-connectors/components/columns.tsx new file mode 100644 index 00000000..61249f43 --- /dev/null +++ b/src/features/api-key-connectors/components/columns.tsx @@ -0,0 +1,86 @@ +"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[] = [ + { + 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: ({ row }) => { + const value = row.original.lastRequest; + return ( + + + {value ? `${formatDistanceToNow(value)} ago` : "Never"} + + + {value ? value.toLocaleString() : "Never"} + + + ); + }, + }, + { + id: "status", + accessorKey: "status", + header: "Status", + cell: ({ row }) => { + let text = "Expired"; + let color = "bg-red-400"; + if (row.original.apiKeyId || row.original.integrationId) { + text = "Active"; + color = "bg-green-300"; + } + + 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..c45f5b87 --- /dev/null +++ b/src/features/api-key-connectors/components/connectors.tsx @@ -0,0 +1,159 @@ +"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, + }); + + console.log("useSuspenseConnectorsByResourceType:", connectors); + + 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..8a8ec5f6 --- /dev/null +++ b/src/features/api-key-connectors/types.ts @@ -0,0 +1,49 @@ +import z from "zod"; +import { ResourceType } from "@/generated/prisma"; +import { + createPaginatedResponseSchema, + paginationInputSchema, +} from "@/lib/pagination"; +import { userIncludeSelect, userSchema } 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