diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/Filter.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/Filter.tsx deleted file mode 100644 index 2c0341c7f..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/Filter.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { useCallback, useMemo } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import LZString from "lz-string"; - -export type Filter = { - key: K; - value: V; -}; - -export const useFilters = >( - defaultFilters?: T[], -) => { - const urlParams = useSearchParams(); - const router = useRouter(); - - const filters = useMemo(() => { - const filtersJson = urlParams.get("filters"); - if (filtersJson == null) return defaultFilters ?? []; - return filtersJson - ? JSON.parse(LZString.decompressFromEncodedURIComponent(filtersJson)) - : []; - }, [defaultFilters, urlParams]); - - const setFilters = useCallback( - (v: any) => { - console.log(v); - const filtersJson = LZString.compressToEncodedURIComponent( - JSON.stringify(v), - ); - const query = new URLSearchParams(window.location.search); - query.set("filters", filtersJson); - router.replace(`?${query.toString()}`); - }, - [router], - ); - - // const [filters, setFilters] = useState(defaultFilters ?? []); - const addFilters = (newFilters: T[]) => - setFilters([...filters, ...newFilters]); - const removeFilter = (idx: number) => - setFilters(filters.filter((_, i) => i !== idx)); - const clearFilters = () => setFilters([]); - const updateFilter = (idx: number, filter: T) => - setFilters(filters.map((f, i) => (i === idx ? filter : f))); - return { filters, addFilters, removeFilter, clearFilters, updateFilter }; -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/FilterDropdown.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/FilterDropdown.tsx deleted file mode 100644 index 619d22c40..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/FilterDropdown.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import type { JSXElementConstructor } from "react"; -import React, { useState } from "react"; -import { IconFilter } from "@tabler/icons-react"; - -import { Button } from "@ctrlplane/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@ctrlplane/ui/dropdown-menu"; - -import type { Filter } from "./Filter"; -import { ContentDialog } from "./FilterDropdownItems"; - -const dialogs: Array> = [ContentDialog]; - -const isDialog = (type: string | JSXElementConstructor) => - dialogs.includes(type) || ((type as any).name as string).includes("Dialog"); - -interface FilterProps> { - property: string; - options?: string[]; - children?: React.ReactNode; - onChange?: (filter: T) => void; -} - -export const FilterDropdown = >({ - filters, - addFilters, - children, - className, -}: { - filters: T[]; - addFilters: (newFilters: T[]) => void; - children: React.ReactNode; - className: string; -}) => { - const [open, setOpen] = useState(false); - const [openContent, setOpenContent] = useState(""); - - return ( - { - setOpen(!open); - if (!open) setOpenContent(""); - }} - > - - {filters.length === 0 ? ( - - ) : ( - - )} - - - {React.Children.map(children, (child) => { - if (!React.isValidElement>(child)) return null; - if (isDialog(child.type)) return null; - - const { property } = child.props; - if (openContent === property) - return React.cloneElement(child, { - onChange: (filter: T) => { - addFilters([filter]); - setOpenContent(""); - setOpen(false); - }, - }); - })} - - {openContent === "" && - React.Children.map(children, (child) => { - if (!React.isValidElement>(child)) return null; - - const { property, children } = child.props; - - const c = children ? ( - {children} - ) : ( - property - ); - - if (isDialog(child.type)) - return React.cloneElement(child, { - onChange: (filter: T) => { - addFilters([filter]); - setOpenContent(""); - setOpen(false); - }, - children: ( - e.preventDefault()}> - {c} - - ), - }); - - return ( - { - e.preventDefault(); - setOpenContent(property); - }} - > - {c} - - ); - })} - - - ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/FilterDropdownItems.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/FilterDropdownItems.tsx deleted file mode 100644 index f35df7546..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/filter/FilterDropdownItems.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState } from "react"; - -import { Button } from "@ctrlplane/ui/button"; -import { - Command, - CommandEmpty, - CommandInput, - CommandItem, - CommandList, -} from "@ctrlplane/ui/command"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogTitle, - DialogTrigger, -} from "@ctrlplane/ui/dialog"; -import { Input } from "@ctrlplane/ui/input"; - -import type { Filter } from "./Filter"; - -export const ComboboxFilter = >({ - property, - options, - onChange, -}: { - property: string; - options: string[]; - onChange?: (filter: T) => void; - children?: React.ReactNode; -}) => { - return ( - - - - No {property} found - {options.map((option) => ( - onChange?.({ key: property, value: option } as T)} - > - {option} - - ))} - - - ); -}; - -export const ContentDialog = >({ - property, - onChange, - children, -}: { - property: string; - onChange?: (filter: T) => void; - children?: React.ReactNode; -}) => { - const [value, setValue] = useState(""); - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - onChange?.({ key: property, value } as T); - }; - return ( - - {children} - - -
- Filter {property} - setValue(e.target.value)} - /> - - - -
-
-
- ); -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/SystemFilter.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/SystemFilter.tsx deleted file mode 100644 index 526d4ca08..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/SystemFilter.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import type { Filter } from "../_components/filter/Filter"; - -export type SystemFilter = Filter<"name" | "slug", string>; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/SystemsList.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/SystemsList.tsx index 49b30d73b..aa2e5c40a 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/SystemsList.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/SystemsList.tsx @@ -1,110 +1,38 @@ "use client"; import type { Workspace } from "@ctrlplane/db/schema"; -import { IconTarget, IconX } from "@tabler/icons-react"; -import { capitalCase } from "change-case"; +import { useState } from "react"; import _ from "lodash"; +import { useDebounce } from "react-use"; -import { Badge } from "@ctrlplane/ui/badge"; -import { Button } from "@ctrlplane/ui/button"; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@ctrlplane/ui/hover-card"; import { Skeleton } from "@ctrlplane/ui/skeleton"; -import type { SystemFilter } from "./SystemFilter"; import { api } from "~/trpc/react"; -import { useFilters } from "../_components/filter/Filter"; -import { FilterDropdown } from "../_components/filter/FilterDropdown"; -import { ContentDialog } from "../_components/filter/FilterDropdownItems"; -import { NoFilterMatch } from "../_components/filter/NoFilterMatch"; +import { SearchInput } from "../(targets)/targets/TargetPageContent"; import { SystemsTable } from "./SystemsTable"; export const SystemsList: React.FC<{ workspace: Workspace; systemsCount: number; -}> = ({ workspace, systemsCount }) => { - const { filters, addFilters, removeFilter, clearFilters } = - useFilters(); - - const systems = api.system.list.useQuery({ - workspaceId: workspace.id, - filters, - }); - return ( -
-
-
- {filters.map((f, idx) => ( - - {capitalCase(f.key)} - - {f.key === "name" && "contains"} - {f.key === "slug" && "contains"} - - - {typeof f.value === "string" ? ( - f.value - ) : ( - - - {Object.entries(f.value).length} metadata - - - {Object.entries(f.value).map(([key, value]) => ( -
- {key}:{" "} - - {value as string} - -
- ))} -
-
- )} -
- - -
- ))} +}> = ({ workspace }) => { + const [query, setQuery] = useState(undefined); + const [debouncedQuery, setDebouncedQuery] = useState( + undefined, + ); - - filters={filters} - addFilters={addFilters} - className="min-w-[200px] bg-neutral-900 p-1" - > - property="name"> - Name - - -
+ useDebounce(() => setDebouncedQuery(query == "" ? undefined : query), 500, [ + query, + ]); - {systems.data?.total != null && ( -
- Total: - - {systems.data.total} - -
- )} + const systems = api.system.list.useQuery( + { workspaceId: workspace.id, query: debouncedQuery }, + { placeholderData: (prev) => prev }, + ); + return ( +
+
+
- {systems.isLoading && (
{_.range(10).map((i) => ( @@ -117,16 +45,8 @@ export const SystemsList: React.FC<{
)} - {systems.isSuccess && systems.data.total === 0 && ( - - )} - {systems.data != null && systems.data.total > 0 && ( -
+
[] = [ - { - id: "name", - header: "Name", - accessorKey: "name", - cell: (info) => info.getValue(), - }, -]; - export const SystemsTable: React.FC<{ systems: (System & { environments: Environment[] })[]; workspaceSlug: string; }> = ({ systems, workspaceSlug }) => { const router = useRouter(); - const table = useReactTable({ - data: systems, - columns, - getCoreRowModel: getCoreRowModel(), - }); - return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - ))} - +
- {table.getRowModel().rows.map((row) => ( + {systems.map((system) => ( - {row.getVisibleCells().map((cell) => ( - - - router.push( - `/${workspaceSlug}/systems/${row.original.slug}/deployments`, - ) - } - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - - - - - - - ))} + + router.push( + `/${workspaceSlug}/systems/${system.slug}/deployments`, + ) + } + > + {system.name} + + +
+ + + +
+
))}
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/page.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/page.tsx index dde74eea2..46de489c8 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/systems/page.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/systems/page.tsx @@ -36,12 +36,7 @@ export default async function SystemsPage({ <> -
- -
+ )} diff --git a/packages/api/src/router/system.ts b/packages/api/src/router/system.ts index 9035b68eb..e11901c62 100644 --- a/packages/api/src/router/system.ts +++ b/packages/api/src/router/system.ts @@ -2,7 +2,7 @@ import _ from "lodash"; import { isPresent } from "ts-is-present"; import { z } from "zod"; -import { and, asc, count, eq, like, or, takeFirst } from "@ctrlplane/db"; +import { and, asc, count, eq, like, takeFirst } from "@ctrlplane/db"; import { createSystem, environment, @@ -26,32 +26,17 @@ export const systemRouter = createTRPCRouter({ .input( z.object({ workspaceId: z.string().uuid(), - filters: z - .array( - z.object({ - key: z.enum(["name", "slug"]), - value: z.string(), - }), - ) - .optional(), + query: z.string().optional(), limit: z.number().default(500), offset: z.number().default(0), }), ) .query(({ ctx, input }) => { const workspaceIdCheck = eq(system.workspaceId, input.workspaceId); - const nameFilters = (input.filters ?? []) - .filter((f) => f.key === "name") - .map((f) => like(system.name, `%${f.value}%`)); - - const slugFilters = (input.filters ?? []) - .filter((f) => f.key === "slug") - .map((f) => like(system.slug, `%${f.value}%`)); const checks = and( workspaceIdCheck, - or(...nameFilters), - or(...slugFilters), + input.query ? like(system.name, `%${input.query}%`) : undefined, ); const items = ctx.db