Skip to content

Commit

Permalink
fix: Deployment level resource filtering (#273)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Dec 23, 2024
1 parent e0409a2 commit c243f95
Show file tree
Hide file tree
Showing 15 changed files with 4,898 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import type * as SCHEMA from "@ctrlplane/db/schema";
import type { ResourceCondition } from "@ctrlplane/validators/resources";
import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import {
IconExternalLink,
IconLoader2,
IconSelector,
} from "@tabler/icons-react";
import * as LZString from "lz-string";
import { IconLoader2, IconSelector } from "@tabler/icons-react";
import { z } from "zod";

import { Button } from "@ctrlplane/ui/button";
Expand All @@ -28,7 +21,6 @@ import {
FormMessage,
useForm,
} from "@ctrlplane/ui/form";
import { Label } from "@ctrlplane/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover";
import {
ComparisonOperator,
Expand All @@ -42,7 +34,7 @@ import {

import { api } from "~/trpc/react";
import { ResourceConditionRender } from "../resource-condition/ResourceConditionRender";
import { ResourceIcon } from "../ResourceIcon";
import { ResourceList } from "../resource-condition/ResourceList";

const ResourceViewsCombobox: React.FC<{
workspaceId: string;
Expand Down Expand Up @@ -120,7 +112,6 @@ export const EditFilterForm: React.FC<{
environment: SCHEMA.Environment;
workspaceId: string;
}> = ({ environment, workspaceId }) => {
const { workspaceSlug } = useParams<{ workspaceSlug: string }>();
const update = api.environment.update.useMutation();
const form = useForm({
schema: filterForm,
Expand Down Expand Up @@ -208,43 +199,11 @@ export const EditFilterForm: React.FC<{
{resourceFilter != null &&
resources.data != null &&
resources.data.total > 0 && (
<div className="space-y-4">
<Label>Resources ({resources.data.total})</Label>
<div className="space-y-2">
{resources.data.items.map((resource) => (
<div className="flex items-center gap-2" key={resource.id}>
<ResourceIcon
version={resource.version}
kind={resource.kind}
/>
<div className="flex flex-col">
<span className="overflow-hidden text-nowrap text-sm">
{resource.name}
</span>
<span className="text-xs text-muted-foreground">
{resource.version}
</span>
</div>
</div>
))}
</div>

<Button variant="outline" size="sm">
<Link
href={`/${workspaceSlug}/resources?${new URLSearchParams({
filter: LZString.compressToEncodedURIComponent(
JSON.stringify(form.getValues("resourceFilter")),
),
})}`}
className="flex items-center gap-1"
target="_blank"
rel="noopener noreferrer"
>
<IconExternalLink className="h-4 w-4" />
View Resources
</Link>
</Button>
</div>
<ResourceList
resources={resources.data.items}
count={resources.data.total}
filter={resourceFilter}
/>
)}
</form>
</Form>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { RouterOutputs } from "@ctrlplane/api";
import type { ResourceCondition } from "@ctrlplane/validators/resources";
import Link from "next/link";
import { useParams } from "next/navigation";
import { IconExternalLink } from "@tabler/icons-react";
import * as LZString from "lz-string";

import { Button } from "@ctrlplane/ui/button";
import { Label } from "@ctrlplane/ui/label";

import { ResourceIcon } from "../ResourceIcon";

type Resource =
RouterOutputs["resource"]["byWorkspaceId"]["list"]["items"][number];

type ResourceListProps = {
resources: Resource[];
count: number;
filter: ResourceCondition;
};

export const ResourceList: React.FC<ResourceListProps> = ({
resources,
count,
filter,
}) => {
const { workspaceSlug } = useParams<{ workspaceSlug: string }>();

return (
<div className="space-y-4">
<Label>Resources ({count})</Label>
<div className="space-y-2">
{resources.map((resource) => (
<div className="flex items-center gap-2" key={resource.id}>
<ResourceIcon version={resource.version} kind={resource.kind} />
<div className="flex flex-col">
<span className="overflow-hidden text-nowrap text-sm">
{resource.name}
</span>
<span className="text-xs text-muted-foreground">
{resource.version}
</span>
</div>
</div>
))}
</div>
<Button variant="outline" size="sm">
<Link
href={`/${workspaceSlug}/resources?${new URLSearchParams({
filter: LZString.compressToEncodedURIComponent(
JSON.stringify(filter),
),
})}`}
className="flex items-center gap-1"
target="_blank"
rel="noopener noreferrer"
>
<IconExternalLink className="h-4 w-4" />
View Resources
</Link>
</Button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
"use client";

import type { RouterOutputs } from "@ctrlplane/api";
import type * as SCHEMA from "@ctrlplane/db/schema";
import type { ResourceCondition } from "@ctrlplane/validators/resources";
import { useParams } from "next/navigation";
import { useInView } from "react-intersection-observer";
import { isPresent } from "ts-is-present";

import { Button } from "@ctrlplane/ui/button";
import {
ComparisonOperator,
FilterType,
} from "@ctrlplane/validators/conditions";

import { useReleaseChannelDrawer } from "~/app/[workspaceSlug]/(app)/_components/release-channel-drawer/useReleaseChannelDrawer";
import { api } from "~/trpc/react";
Expand All @@ -16,7 +23,7 @@ type BlockedEnv = RouterOutputs["release"]["blocked"][number];

type ReleaseEnvironmentCellProps = {
environment: Environment;
deployment: { slug: string; jobAgentId: string | null };
deployment: SCHEMA.Deployment;
release: { id: string; version: string; createdAt: Date };
blockedEnv?: BlockedEnv;
};
Expand All @@ -32,18 +39,41 @@ const ReleaseEnvironmentCell: React.FC<ReleaseEnvironmentCellProps> = ({
systemSlug: string;
}>();

const { data: statuses, isLoading } =
const { data: workspace, isLoading: isWorkspaceLoading } =
api.workspace.bySlug.useQuery(workspaceSlug);
const workspaceId = workspace?.id ?? "";

const { data: statuses, isLoading: isStatusesLoading } =
api.release.status.byEnvironmentId.useQuery(
{ releaseId: release.id, environmentId: environment.id },
{ refetchInterval: 2_000 },
);

const { resourceFilter: envResourceFilter } = environment;
const { resourceFilter: deploymentResourceFilter } = deployment;

const resourceFilter: ResourceCondition = {
type: FilterType.Comparison,
operator: ComparisonOperator.And,
conditions: [envResourceFilter, deploymentResourceFilter].filter(isPresent),
};

const { data: resourcesResult, isLoading: isResourcesLoading } =
api.resource.byWorkspaceId.list.useQuery(
{ workspaceId, filter: resourceFilter, limit: 0 },
{ enabled: workspaceId !== "" && envResourceFilter != null },
);

const total = resourcesResult?.total ?? 0;

const { setReleaseChannelId } = useReleaseChannelDrawer();

const isLoading =
isWorkspaceLoading || isStatusesLoading || isResourcesLoading;
if (isLoading)
return <p className="text-xs text-muted-foreground">Loading...</p>;

const hasResources = environment.resources.length > 0;
const hasResources = total > 0;
const isAlreadyDeployed = statuses != null && statuses.length > 0;

const hasJobAgent = deployment.jobAgentId != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import type { RouterOutputs } from "@ctrlplane/api";
import type { Deployment, Workspace } from "@ctrlplane/db/schema";
import Link from "next/link";
import { IconCircleFilled } from "@tabler/icons-react";
import { IconCircleFilled, IconLoader2 } from "@tabler/icons-react";

import { cn } from "@ctrlplane/ui";
import { Badge } from "@ctrlplane/ui/badge";

import { DeploymentOptionsDropdown } from "~/app/[workspaceSlug]/(app)/_components/DeploymentOptionsDropdown";
import { api } from "~/trpc/react";
import { LazyReleaseEnvironmentCell } from "./ReleaseEnvironmentCell";

type Environment = RouterOutputs["environment"]["bySystemId"][number];
Expand All @@ -34,6 +35,20 @@ const EnvIcon: React.FC<{
workspaceSlug: string;
systemSlug: string;
}> = ({ environment: env, isFirst, isLast, workspaceSlug, systemSlug }) => {
const { data: workspace, isLoading: isWorkspaceLoading } =
api.workspace.bySlug.useQuery(workspaceSlug);
const workspaceId = workspace?.id ?? "";

const filter = env.resourceFilter ?? undefined;
const { data: resourcesResult, isLoading: isResourcesLoading } =
api.resource.byWorkspaceId.list.useQuery(
{ workspaceId, filter, limit: 0 },
{ enabled: workspaceId !== "" && filter != null },
);
const total = resourcesResult?.total ?? 0;

const isLoading = isWorkspaceLoading || isResourcesLoading;

const envUrl = `/${workspaceSlug}/systems/${systemSlug}/deployments?environment_id=${env.id}`;
return (
<Icon
Expand All @@ -52,7 +67,10 @@ const EnvIcon: React.FC<{
variant="outline"
className="rounded-full text-muted-foreground"
>
{env.resources.length}
{isLoading && (
<IconLoader2 className="h-3 w-3 animate-spin text-muted-foreground" />
)}
{!isLoading && total}
</Badge>
</div>
</Link>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { ResourceCondition } from "@ctrlplane/validators/resources";
import { useState } from "react";
import { IconFilter } from "@tabler/icons-react";
import { isPresent } from "ts-is-present";

import { Button } from "@ctrlplane/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@ctrlplane/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ctrlplane/ui/select";
import {
ComparisonOperator,
FilterType,
} from "@ctrlplane/validators/conditions";
import { isValidResourceCondition } from "@ctrlplane/validators/resources";

import { ResourceConditionRender } from "~/app/[workspaceSlug]/(app)/_components/resource-condition/ResourceConditionRender";
import { ResourceList } from "~/app/[workspaceSlug]/(app)/_components/resource-condition/ResourceList";
import { api } from "~/trpc/react";

type Environment = {
id: string;
name: string;
resourceFilter: ResourceCondition;
};
type DeploymentResourcesDialogProps = {
environments: Environment[];
resourceFilter: ResourceCondition;
workspaceId: string;
};

export const DeploymentResourcesDialog: React.FC<
DeploymentResourcesDialogProps
> = ({ environments, resourceFilter, workspaceId }) => {
const [selectedEnvironment, setSelectedEnvironment] =
useState<Environment | null>(environments[0] ?? null);

const filter: ResourceCondition = {
type: FilterType.Comparison,
operator: ComparisonOperator.And,
conditions: [selectedEnvironment?.resourceFilter, resourceFilter].filter(
isPresent,
),
};
const isFilterValid = isValidResourceCondition(filter);

const { data, isLoading } = api.resource.byWorkspaceId.list.useQuery(
{ workspaceId, filter, limit: 5 },
{ enabled: selectedEnvironment != null && isFilterValid },
);

const resources = data?.items ?? [];
const count = data?.total ?? 0;

if (environments.length === 0) return null;
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
className="flex items-center gap-2"
type="button"
disabled={!isValidResourceCondition(resourceFilter)}
>
<IconFilter className="h-4 w-4" /> View Resources
</Button>
</DialogTrigger>
<DialogContent className="min-w-[1000px] space-y-6">
<DialogHeader>
<DialogTitle>View Resources</DialogTitle>
<DialogDescription>
Select an environment to view the resources based on the combined
environment and deployment filter.
</DialogDescription>
</DialogHeader>

<Select
value={selectedEnvironment?.id}
onValueChange={(value) => {
const environment = environments.find((e) => e.id === value);
setSelectedEnvironment(environment ?? null);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select an environment" />
</SelectTrigger>
<SelectContent>
{environments.map((environment) => (
<SelectItem key={environment.id} value={environment.id}>
{environment.name}
</SelectItem>
))}
</SelectContent>
</Select>

{selectedEnvironment != null && (
<>
<ResourceConditionRender condition={filter} onChange={() => {}} />
{!isLoading && (
<ResourceList
resources={resources}
count={count}
filter={filter}
/>
)}
</>
)}
</DialogContent>
</Dialog>
);
};
Loading

0 comments on commit c243f95

Please sign in to comment.