Skip to content

Commit

Permalink
fix: Init job filters (#174)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Oct 27, 2024
1 parent d71d491 commit 5b7e69a
Show file tree
Hide file tree
Showing 52 changed files with 2,252 additions and 200 deletions.
135 changes: 135 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/(job)/jobs/JobTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"use client";

import React from "react";
import { IconFilter, IconLoader2 } from "@tabler/icons-react";
import _ from "lodash";

import { Badge } from "@ctrlplane/ui/badge";
import { Button } from "@ctrlplane/ui/button";
import { Skeleton } from "@ctrlplane/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@ctrlplane/ui/table";
import { JobStatusReadable } from "@ctrlplane/validators/jobs";

import { NoFilterMatch } from "~/app/[workspaceSlug]/_components/filter/NoFilterMatch";
import { JobConditionBadge } from "~/app/[workspaceSlug]/_components/job-condition/JobConditionBadge";
import { JobConditionDialog } from "~/app/[workspaceSlug]/_components/job-condition/JobConditionDialog";
import { useJobFilter } from "~/app/[workspaceSlug]/_components/job-condition/useJobFilter";
import { useJobDrawer } from "~/app/[workspaceSlug]/_components/job-drawer/useJobDrawer";
import { JobTableStatusIcon } from "~/app/[workspaceSlug]/_components/JobTableStatusIcon";
import { api } from "~/trpc/react";

type JobTableProps = {
workspaceId: string;
};

export const JobTable: React.FC<JobTableProps> = ({ workspaceId }) => {
const { filter, setFilter } = useJobFilter();
const { setJobId } = useJobDrawer();
const allReleaseJobTriggers = api.job.config.byWorkspaceId.list.useQuery(
{ workspaceId },
{ refetchInterval: 60_000, placeholderData: (prev) => prev },
);

const releaseJobTriggers = api.job.config.byWorkspaceId.list.useQuery(
{ workspaceId, filter },
{ refetchInterval: 10_000, placeholderData: (prev) => prev },
);

return (
<div className="h-full text-sm">
<div className="flex h-[41px] items-center justify-between border-b border-neutral-800 p-1 px-2">
<div className="flex items-center gap-2">
<JobConditionDialog condition={filter} onChange={setFilter}>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="flex h-7 w-7 flex-shrink-0 items-center gap-1 text-xs"
>
<IconFilter className="h-4 w-4" />
</Button>
{filter != null && <JobConditionBadge condition={filter} />}
</div>
</JobConditionDialog>
{!releaseJobTriggers.isLoading && releaseJobTriggers.isFetching && (
<IconLoader2 className="h-4 w-4 animate-spin" />
)}
</div>

{releaseJobTriggers.data?.total != null && (
<div className="flex items-center gap-2 rounded-lg border border-neutral-800/50 px-2 py-1 text-sm text-muted-foreground">
Total:
<Badge
variant="outline"
className="rounded-full border-neutral-800 text-inherit"
>
{releaseJobTriggers.data.total}
</Badge>
</div>
)}
</div>

{releaseJobTriggers.isLoading && (
<div className="space-y-2 p-4">
{_.range(10).map((i) => (
<Skeleton
key={i}
className="h-9 w-full"
style={{ opacity: 1 * (1 - i / 10) }}
/>
))}
</div>
)}
{releaseJobTriggers.isSuccess && releaseJobTriggers.data.total === 0 && (
<NoFilterMatch
numItems={allReleaseJobTriggers.data?.total ?? 0}
itemType="job"
onClear={() => setFilter(undefined)}
/>
)}

{releaseJobTriggers.isSuccess && releaseJobTriggers.data.total > 0 && (
<div className="h-[calc(100%-41px)] overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Target</TableHead>
<TableHead>Environment</TableHead>
<TableHead>Deployment</TableHead>
<TableHead>Status</TableHead>
<TableHead>Release Version</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{releaseJobTriggers.data.items.map((job) => (
<TableRow
key={job.id}
onClick={() => setJobId(job.job.id)}
className="cursor-pointer"
>
<TableCell>{job.target.name}</TableCell>
<TableCell>{job.environment.name}</TableCell>
<TableCell>{job.release.deployment.name}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<JobTableStatusIcon status={job.job.status} />
{JobStatusReadable[job.job.status]}
</div>
</TableCell>
<TableCell>{job.release.version}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
);
};
47 changes: 6 additions & 41 deletions apps/webservice/src/app/[workspaceSlug]/(job)/jobs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import { notFound } from "next/navigation";
import { format } from "date-fns";

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@ctrlplane/ui/table";

import { api } from "~/trpc/server";
import { JobsGettingStarted } from "./JobsGettingStarted";
import { JobTable } from "./JobTable";

export default async function JobsPage({
params,
Expand All @@ -21,37 +12,11 @@ export default async function JobsPage({
const workspace = await api.workspace.bySlug(params.workspaceSlug);
if (workspace == null) return notFound();

const releaseJobTriggers = await api.job.config.byWorkspaceId.list(
workspace.id,
);
const releaseJobTriggers = await api.job.config.byWorkspaceId.list({
workspaceId: workspace.id,
});

if (releaseJobTriggers.length === 0) return <JobsGettingStarted />;
if (releaseJobTriggers.total === 0) return <JobsGettingStarted />;

return (
<div className="scrollbar-thin scrollbar-thumb-neutral-800 scrollbar-track-neutral-900 container mx-auto h-[calc(100vh-40px)] overflow-auto p-6">
<h1 className="mb-4 text-2xl font-bold">Jobs</h1>
<Table>
<TableHeader>
<TableRow>
<TableHead>Environment</TableHead>
<TableHead>Target</TableHead>
<TableHead>Release Version</TableHead>
<TableHead>Type</TableHead>
<TableHead>Created At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{releaseJobTriggers.map((job) => (
<TableRow key={job.id}>
<TableCell>{job.environment.name}</TableCell>
<TableCell>{job.target.name}</TableCell>
<TableCell>{job.release.version}</TableCell>
<TableCell>{job.type}</TableCell>
<TableCell>{format(new Date(job.createdAt), "PPpp")}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
return <JobTable workspaceId={workspace.id} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export const JobTableStatusIcon: React.FC<{
<IconLoader2 className={cn("h-4 w-4", className)} />
</div>
);
if (status === JobStatus.Cancelled)
return (
<IconCircleX className={cn("h-4 w-4 text-neutral-400", className)} />
);

return <IconClock className={cn("h-4 w-4 text-neutral-400", className)} />;
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react";
import { IconSelector } from "@tabler/icons-react";
import { IconLoader2, IconSelector } from "@tabler/icons-react";
import { capitalCase } from "change-case";

import { cn } from "@ctrlplane/ui";
Expand All @@ -18,6 +18,7 @@ type ChoiceConditionRenderProps = {
type: string;
selected: string | null;
options: { key: string; value: string; display: string }[];
loading?: boolean;
className?: string;
};

Expand All @@ -26,6 +27,7 @@ export const ChoiceConditionRender: React.FC<ChoiceConditionRenderProps> = ({
type,
selected,
options,
loading = false,
className,
}) => {
const [open, setOpen] = useState(false);
Expand All @@ -34,7 +36,7 @@ export const ChoiceConditionRender: React.FC<ChoiceConditionRenderProps> = ({
<div className={cn("flex w-full items-center gap-2", className)}>
<div className="grid w-full grid-cols-12">
<div className="col-span-2 flex items-center rounded-l-md border bg-transparent px-3 text-sm text-muted-foreground">
{capitalCase(type)}
<span className="truncate">{capitalCase(type)} is</span>
</div>
<div className="col-span-10">
<Popover open={open} onOpenChange={setOpen}>
Expand All @@ -46,9 +48,7 @@ export const ChoiceConditionRender: React.FC<ChoiceConditionRenderProps> = ({
className="w-full items-center justify-start gap-2 rounded-l-none rounded-r-md bg-transparent px-2 hover:bg-neutral-800/50"
>
<IconSelector className="h-4 w-4 text-muted-foreground" />
<span
className={cn(selected != null && "text-muted-foreground")}
>
<span className="text-muted-foreground">
{selected ?? `Select ${type}...`}
</span>
</Button>
Expand All @@ -58,15 +58,24 @@ export const ChoiceConditionRender: React.FC<ChoiceConditionRenderProps> = ({
<CommandInput placeholder={`Search ${type}...`} />
<CommandGroup>
<CommandList>
{options.length === 0 && (
{loading && (
<CommandItem
disabled
className="flex items-center gap-2 text-muted-foreground"
>
<IconLoader2 className="h-3 w-3 animate-spin" />
Loading {type}s...
</CommandItem>
)}
{!loading && options.length === 0 && (
<CommandItem disabled>No options to add</CommandItem>
)}
{options.map((option) => (
<CommandItem
key={option.key}
value={option.key}
value={option.value}
onSelect={() => {
onSelect(option.key);
onSelect(option.value);
setOpen(false);
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const DateConditionRender: React.FC<DateConditionRenderProps> = ({
<div className={cn("flex w-full items-center gap-2", className)}>
<div className="grid w-full grid-cols-12">
<div className="col-span-2 flex items-center rounded-l-md border bg-transparent px-3 text-sm text-muted-foreground">
{type}
<span className="truncate">{type}</span>
</div>
<div className="col-span-3">
<Select value={operator} onValueChange={setOperator}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { VersionOperatorType } from "@ctrlplane/validators/conditions";

import { cn } from "@ctrlplane/ui";
import { Input } from "@ctrlplane/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ctrlplane/ui/select";
import { VersionOperator } from "@ctrlplane/validators/conditions";

type VersionConditionRenderProps = {
operator: VersionOperatorType;
value: string;
setOperator: (operator: VersionOperatorType) => void;
setValue: (value: string) => void;
className?: string;
title?: string;
};

export const VersionConditionRender: React.FC<VersionConditionRenderProps> = ({
operator,
value,
setOperator,
setValue,
className,
title = "Version",
}) => (
<div className={cn("flex w-full items-center gap-2", className)}>
<div className="grid w-full grid-cols-12">
<div className="col-span-2 flex items-center rounded-l-md border bg-transparent px-3 text-sm text-muted-foreground">
<span className="truncate">{title}</span>
</div>
<div className="col-span-3 text-muted-foreground">
<Select value={operator} onValueChange={setOperator}>
<SelectTrigger className="w-full rounded-none hover:bg-neutral-800/50">
<SelectValue placeholder="Operator" />
</SelectTrigger>
<SelectContent>
<SelectItem value={VersionOperator.Equals}>Equals</SelectItem>
<SelectItem value={VersionOperator.Like}>Like</SelectItem>
<SelectItem value={VersionOperator.Regex}>Regex</SelectItem>
</SelectContent>
</Select>
</div>
<div className="col-span-7">
<Input
placeholder={
operator === VersionOperator.Regex
? "^[a-zA-Z]+$"
: operator === VersionOperator.Like
? "%value%"
: "Value"
}
value={value}
onChange={(e) => setValue(e.target.value)}
className="w-full cursor-pointer rounded-l-none"
/>
</div>
</div>
</div>
);
Loading

0 comments on commit 5b7e69a

Please sign in to comment.