Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Deployment level resource filtering #273

Merged
merged 7 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Comment on lines +58 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for the query.

The query implementation should handle potential errors to provide feedback to users when resource fetching fails.

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

   const resources = data?.items ?? [];
   const count = data?.total ?? 0;
+  
+  const hasError = error != null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
const { data, isLoading, error } = api.resource.byWorkspaceId.list.useQuery(
{ workspaceId, filter, limit: 5 },
{ enabled: selectedEnvironment != null && isFilterValid },
);
const resources = data?.items ?? [];
const count = data?.total ?? 0;
const hasError = error != null;


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={() => {}} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement or remove the empty onChange handler.

The onChange prop is provided with an empty function. Either implement the handler or remove it if not needed.

{!isLoading && (
<ResourceList
resources={resources}
count={count}
filter={filter}
/>
)}
</>
)}
</DialogContent>
</Dialog>
);
};
Loading
Loading