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: Init job filters #174

Merged
merged 5 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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 },
);
adityachoudhari26 marked this conversation as resolved.
Show resolved Hide resolved

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>

adityachoudhari26 marked this conversation as resolved.
Show resolved Hide resolved
{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>
)}
adityachoudhari26 marked this conversation as resolved.
Show resolved Hide resolved
{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>
);
adityachoudhari26 marked this conversation as resolved.
Show resolved Hide resolved
};
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"
/>
adityachoudhari26 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</div>
);
Loading
Loading