diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/SystemsPageContent.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/SystemsPageContent.tsx new file mode 100644 index 000000000..fbf39d137 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/SystemsPageContent.tsx @@ -0,0 +1,89 @@ +"use client"; + +import type * as SCHEMA from "@ctrlplane/db/schema"; +import { useEffect, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +import { Button } from "@ctrlplane/ui/button"; +import { Input } from "@ctrlplane/ui/input"; + +import { api } from "~/trpc/react"; +import { CreateDeploymentDialog } from "../_components/deployments/CreateDeployment"; +import { CreateSystemDialog } from "../../(app)/_components/CreateSystem"; +import { SystemDeploymentSkeleton } from "./system-deployment-table/SystemDeploymentSkeleton"; +import { SystemDeploymentTable } from "./system-deployment-table/SystemDeploymentTable"; + +const useSystemFilter = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const filter = searchParams.get("filter"); + + const setFilter = (filter: string) => { + const url = new URL(window.location.href); + const params = new URLSearchParams(url.search); + + if (filter === "") { + params.delete("filter"); + router.replace(`${url.pathname}?${params.toString()}`); + return; + } + + params.set("filter", filter); + router.replace(`${url.pathname}?${params.toString()}`); + }; + + return { filter, setFilter }; +}; + +export const SystemsPageContent: React.FC<{ + workspace: SCHEMA.Workspace; +}> = ({ workspace }) => { + const { filter, setFilter } = useSystemFilter(); + const [search, setSearch] = useState(filter ?? ""); + + useEffect(() => { + if (search !== (filter ?? "")) setFilter(search); + }, [search, filter, setFilter]); + + const workspaceId = workspace.id; + const query = filter ?? undefined; + const { data, isLoading } = api.system.list.useQuery( + { workspaceId, query }, + { placeholderData: (prev) => prev }, + ); + + const systems = data?.items ?? []; + + return ( +
+
+

Systems

+
+ + + + + + +
+
+ + setSearch(e.target.value)} + placeholder="Search systems and deployments..." + className="w-80" + /> + {isLoading && + Array.from({ length: 2 }).map((_, i) => ( +
+ +
+ ))} + {!isLoading && + systems.map((s) => ( + + ))} +
+ ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/(sidebar)/deployments/TableDeployments.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/(sidebar)/deployments/TableDeployments.tsx index 8e00e9bb3..284ea0e12 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/(sidebar)/deployments/TableDeployments.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/(sidebar)/deployments/TableDeployments.tsx @@ -145,6 +145,7 @@ const DeploymentTable: React.FC<{ environment={env} deployment={r} workspace={workspace} + systemSlug={systemSlug} /> ); diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/_components/deployments/environment-cell/DeploymentEnvironmentCell.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/_components/deployments/environment-cell/DeploymentEnvironmentCell.tsx index 1b68493d7..fa305194b 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/_components/deployments/environment-cell/DeploymentEnvironmentCell.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/[systemSlug]/_components/deployments/environment-cell/DeploymentEnvironmentCell.tsx @@ -1,6 +1,6 @@ "use client"; -import type { RouterOutputs } from "@ctrlplane/api"; +import type * as SCHEMA from "@ctrlplane/db/schema"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { @@ -18,19 +18,18 @@ import { ApprovalDialog } from "../../release/ApprovalDialog"; import { ReleaseDropdownMenu } from "../../release/ReleaseDropdownMenu"; import { Release } from "./ReleaseInfo"; -type Environment = RouterOutputs["environment"]["bySystemId"][number]; -type Deployment = RouterOutputs["deployment"]["bySystemId"][number]; - type DeploymentEnvironmentCellProps = { - environment: Environment; - deployment: Deployment; - workspace: { id: string; slug: string }; + environment: SCHEMA.Environment; + deployment: SCHEMA.Deployment; + workspace: SCHEMA.Workspace; + systemSlug: string; }; const DeploymentEnvironmentCell: React.FC = ({ environment, deployment, workspace, + systemSlug, }) => { const { data: release, isLoading: isReleaseLoading } = api.release.latest.byDeploymentAndEnvironment.useQuery({ @@ -68,7 +67,7 @@ const DeploymentEnvironmentCell: React.FC = ({ if (release.resourceCount === 0) return ( = ({
; @@ -14,27 +10,5 @@ export default async function SystemsPage(props: { const workspace = await api.workspace.bySlug(params.workspaceSlug); if (workspace == null) notFound(); - const systems = await api.system.list({ - workspaceId: workspace.id, - }); - - return ( -
-
-

Systems

-
- - - - - - -
-
- - {systems.items.map((s) => ( - - ))} -
- ); + return ; } diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentSkeleton.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentSkeleton.tsx index fc79ee8b4..c8426e017 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentSkeleton.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentSkeleton.tsx @@ -9,7 +9,7 @@ import { } from "@ctrlplane/ui/table"; export const SystemDeploymentSkeleton: React.FC = () => ( - +
diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentTable.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentTable.tsx index b0f13db8f..e10518ff1 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentTable.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/SystemDeploymentTable.tsx @@ -1,9 +1,8 @@ "use client"; -import type { System, Workspace } from "@ctrlplane/db/schema"; +import type * as SCHEMA from "@ctrlplane/db/schema"; import Link from "next/link"; import { IconDots, IconShip, IconTrash } from "@tabler/icons-react"; -import { useInView } from "react-intersection-observer"; import { Button } from "@ctrlplane/ui/button"; import { @@ -15,24 +14,17 @@ import { import { CreateDeploymentDialog } from "~/app/[workspaceSlug]/(appv2)/_components/deployments/CreateDeployment"; import { DeleteSystemDialog } from "~/app/[workspaceSlug]/(appv2)/_components/system/DeleteSystemDialog"; -import { api } from "~/trpc/react"; -import { SystemDeploymentSkeleton } from "./SystemDeploymentSkeleton"; import DeploymentTable from "./TableDeployments"; +type System = SCHEMA.System & { + deployments: SCHEMA.Deployment[]; + environments: SCHEMA.Environment[]; +}; + export const SystemDeploymentTable: React.FC<{ - workspace: Workspace; + workspace: SCHEMA.Workspace; system: System; }> = ({ workspace, system }) => { - const { ref, inView } = useInView(); - const environments = api.environment.bySystemId.useQuery(system.id, { - enabled: inView, - }); - const deployments = api.deployment.bySystemId.useQuery(system.id, { - enabled: inView, - }); - - const isLoading = environments.isLoading || deployments.isLoading; - return (
@@ -73,21 +65,14 @@ export const SystemDeploymentTable: React.FC<{
- {isLoading && ( -
- -
- )} - {!isLoading && ( -
- -
- )} +
+ +
); }; diff --git a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/TableDeployments.tsx b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/TableDeployments.tsx index d6906e250..8fa3b6d71 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/TableDeployments.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(appv2)/systems/system-deployment-table/TableDeployments.tsx @@ -1,7 +1,6 @@ "use client"; -import type { RouterOutputs } from "@ctrlplane/api"; -import type { Workspace } from "@ctrlplane/db/schema"; +import type * as SCHEMA from "@ctrlplane/db/schema"; import Link from "next/link"; import { IconLoader2 } from "@tabler/icons-react"; @@ -20,11 +19,8 @@ import { LazyDeploymentEnvironmentCell } from "~/app/[workspaceSlug]/(appv2)/sys import { urls } from "~/app/urls"; import { api } from "~/trpc/react"; -type Environment = RouterOutputs["environment"]["bySystemId"][number]; -type Deployment = RouterOutputs["deployment"]["bySystemId"][number]; - const EnvHeader: React.FC<{ - environment: Environment; + environment: SCHEMA.Environment; workspaceSlug: string; }> = ({ environment: env, workspaceSlug }) => { const { data: workspace, isLoading: isWorkspaceLoading } = @@ -62,10 +58,10 @@ const EnvHeader: React.FC<{ }; const DeploymentTable: React.FC<{ - workspace: Workspace; + workspace: SCHEMA.Workspace; systemSlug: string; - environments: Environment[]; - deployments: Deployment[]; + environments: SCHEMA.Environment[]; + deployments: SCHEMA.Deployment[]; }> = ({ systemSlug, deployments, environments, workspace }) => { return (
@@ -118,6 +114,7 @@ const DeploymentTable: React.FC<{ environment={env} deployment={r} workspace={workspace} + systemSlug={systemSlug} />
diff --git a/packages/api/src/router/system.ts b/packages/api/src/router/system.ts index 9b8cfdae6..b3d02ba50 100644 --- a/packages/api/src/router/system.ts +++ b/packages/api/src/router/system.ts @@ -9,14 +9,16 @@ import { count, createEnv, eq, + ilike, isNotNull, isNull, - like, + or, takeFirst, takeFirstOrNull, } from "@ctrlplane/db"; import { createSystem, + deployment, environment, resource, resourceMatchesMetadata, @@ -51,15 +53,21 @@ export const systemRouter = createTRPCRouter({ .query(({ ctx, input }) => { const workspaceIdCheck = eq(system.workspaceId, input.workspaceId); - const checks = and( - workspaceIdCheck, - input.query ? like(system.name, `%${input.query}%`) : undefined, - ); + const query = input.query + ? or( + ilike(system.name, `%${input.query}%`), + ilike(system.slug, `%${input.query}%`), + ilike(deployment.name, `%${input.query}%`), + ilike(deployment.slug, `%${input.query}%`), + ) + : undefined; + const checks = and(workspaceIdCheck, query); const items = ctx.db .select() .from(system) .leftJoin(environment, eq(environment.systemId, system.id)) + .leftJoin(deployment, eq(deployment.systemId, system.id)) .where(checks) .limit(input.limit) .offset(input.offset) @@ -69,7 +77,16 @@ export const systemRouter = createTRPCRouter({ .groupBy((r) => r.system.id) .map((r) => ({ ...r[0]!.system, - environments: r.map((r) => r.environment).filter(isPresent), + environments: _.chain(r) + .map((r) => r.environment) + .filter(isPresent) + .uniqBy((e) => e.id) + .value(), + deployments: _.chain(r) + .map((r) => r.deployment) + .filter(isPresent) + .uniqBy((d) => d.id) + .value(), })) .value(), ); @@ -77,7 +94,7 @@ export const systemRouter = createTRPCRouter({ const total = ctx.db .select({ count: count().as("total") }) .from(system) - .where(checks) + .where(eq(system.workspaceId, input.workspaceId)) .then(takeFirst) .then((total) => total.count);