Skip to content

Commit

Permalink
Run filtering now works on the job runs page too (#823)
Browse files Browse the repository at this point in the history
* Run filtering is now shared: runs page and job runs page

* Use optimistic location

* Improved the type in the run status filter dropdown

* Organize imports
  • Loading branch information
matt-aitken authored Jan 3, 2024
1 parent 61d33ed commit d1092fc
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 341 deletions.
194 changes: 194 additions & 0 deletions apps/webapp/app/components/runs/RunFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {
CheckCircleIcon,
ClockIcon,
ExclamationTriangleIcon,
NoSymbolIcon,
PauseCircleIcon,
XCircleIcon,
} from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { cn } from "~/utils/cn";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
import { Paragraph } from "../primitives/Paragraph";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../primitives/Select";
import { Spinner } from "../primitives/Spinner";
import {
FilterableEnvironment,
FilterableStatus,
RunListSearchSchema,
environmentKeys,
statusKeys,
} from "./RunStatuses";

export function RunsFilters() {
const navigate = useNavigate();
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const { environment, status } = RunListSearchSchema.parse(
Object.fromEntries(searchParams.entries())
);

const handleFilterChange = (filterType: string, value: string | undefined) => {
if (value) {
searchParams.set(filterType, value);
} else {
searchParams.delete(filterType);
}
searchParams.delete("cursor");
searchParams.delete("direction");
navigate(`${location.pathname}?${searchParams.toString()}`);
};

const handleStatusChange = (value: FilterableStatus | "ALL") => {
handleFilterChange("status", value === "ALL" ? undefined : value);
};

const handleEnvironmentChange = (value: FilterableEnvironment | "ALL") => {
handleFilterChange("environment", value === "ALL" ? undefined : value);
};

return (
<div className="flex flex-row justify-between gap-x-2">
<SelectGroup>
<Select
name="environment"
value={environment ?? "ALL"}
onValueChange={handleEnvironmentChange}
>
<SelectTrigger size="secondary/small" width="full">
<SelectValue placeholder={"Select environment"} className="ml-2 p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph variant="extra-small" className="pl-0.5">
All environments
</Paragraph>
</SelectItem>
{environmentKeys.map((env) => (
<SelectItem key={env} value={env}>
<div className="flex items-center gap-x-2">
<EnvironmentLabel environment={{ type: env }} />
<Paragraph variant="extra-small">environment</Paragraph>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>

<SelectGroup>
<Select name="status" value={status ?? "ALL"} onValueChange={handleStatusChange}>
<SelectTrigger size="secondary/small" width="full">
<SelectValue placeholder="Select status" className="ml-2 p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph variant="extra-small" className="pl-0.5">
All statuses
</Paragraph>
</SelectItem>
{statusKeys.map((status) => (
<SelectItem key={status} value={status}>
{
<span className="flex items-center gap-1 text-xs">
<FilterStatusIcon status={status} className="h-4 w-4" />
<FilterStatusLabel status={status} />
</span>
}
</SelectItem>
))}
</SelectContent>
</Select>
</SelectGroup>
</div>
);
}

export function FilterStatusLabel({ status }: { status: FilterableStatus }) {
return <span className={filterStatusClassNameColor(status)}>{filterStatusTitle(status)}</span>;
}

export function FilterStatusIcon({
status,
className,
}: {
status: FilterableStatus;
className: string;
}) {
switch (status) {
case "COMPLETED":
return <CheckCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "WAITING":
return <ClockIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "QUEUED":
return <PauseCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "IN_PROGRESS":
return <Spinner className={cn(filterStatusClassNameColor(status), className)} />;
case "TIMEDOUT":
return (
<ExclamationTriangleIcon className={cn(filterStatusClassNameColor(status), className)} />
);
case "CANCELED":
return <NoSymbolIcon className={cn(filterStatusClassNameColor(status), className)} />;
case "FAILED":
return <XCircleIcon className={cn(filterStatusClassNameColor(status), className)} />;
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}

export function filterStatusTitle(status: FilterableStatus): string {
switch (status) {
case "QUEUED":
return "Queued";
case "IN_PROGRESS":
return "In progress";
case "WAITING":
return "Waiting";
case "COMPLETED":
return "Completed";
case "FAILED":
return "Failed";
case "CANCELED":
return "Canceled";
case "TIMEDOUT":
return "Timed out";
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}

export function filterStatusClassNameColor(status: FilterableStatus): string {
switch (status) {
case "QUEUED":
return "text-slate-500";
case "IN_PROGRESS":
return "text-blue-500";
case "WAITING":
return "text-blue-500";
case "COMPLETED":
return "text-green-500";
case "FAILED":
return "text-rose-500";
case "CANCELED":
return "text-slate-500";
case "TIMEDOUT":
return "text-amber-300";
default: {
const _exhaustiveCheck: never = status;
throw new Error(`Non-exhaustive match for value: ${status}`);
}
}
}
42 changes: 42 additions & 0 deletions apps/webapp/app/components/runs/RunStatuses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import type { JobRunStatus } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
import { Spinner } from "../primitives/Spinner";
import { z } from "zod";

export function RunStatus({ status }: { status: JobRunStatus }) {
return (
Expand Down Expand Up @@ -127,3 +128,44 @@ export function runStatusClassNameColor(status: JobRunStatus): string {
}
}
}

export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]);
export type Direction = z.infer<typeof DirectionSchema>;

export const FilterableStatus = z.union([
z.literal("QUEUED"),
z.literal("IN_PROGRESS"),
z.literal("WAITING"),
z.literal("COMPLETED"),
z.literal("FAILED"),
z.literal("TIMEDOUT"),
z.literal("CANCELED"),
]);
export type FilterableStatus = z.infer<typeof FilterableStatus>;

export const FilterableEnvironment = z.union([
z.literal("DEVELOPMENT"),
z.literal("STAGING"),
z.literal("PRODUCTION"),
]);
export type FilterableEnvironment = z.infer<typeof FilterableEnvironment>;
export const environmentKeys: FilterableEnvironment[] = ["DEVELOPMENT", "STAGING", "PRODUCTION"];

export const RunListSearchSchema = z.object({
cursor: z.string().optional(),
direction: DirectionSchema.optional(),
status: FilterableStatus.optional(),
environment: FilterableEnvironment.optional(),
});

export const filterableStatuses: Record<FilterableStatus, JobRunStatus[]> = {
QUEUED: ["QUEUED", "WAITING_TO_EXECUTE", "PENDING", "WAITING_ON_CONNECTIONS"],
IN_PROGRESS: ["STARTED", "EXECUTING", "PREPROCESSING"],
WAITING: ["WAITING_TO_CONTINUE"],
COMPLETED: ["SUCCESS"],
FAILED: ["FAILURE", "UNRESOLVED_AUTH", "INVALID_PAYLOAD", "ABORTED"],
TIMEDOUT: ["TIMED_OUT"],
CANCELED: ["CANCELED"],
};

export const statusKeys: FilterableStatus[] = Object.keys(filterableStatuses) as FilterableStatus[];
12 changes: 12 additions & 0 deletions apps/webapp/app/hooks/useOptimisticLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useLocation, useNavigation } from "@remix-run/react";

export function useOptimisticLocation() {
const navigation = useNavigation();
const location = useLocation();

if (navigation.state === "idle" || !navigation.location) {
return location;
}

return navigation.location;
}
18 changes: 11 additions & 7 deletions apps/webapp/app/presenters/RunListPresenter.server.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
import { z } from "zod";
import {
Direction,
FilterableEnvironment,
FilterableStatus,
filterableStatuses,
} from "~/components/runs/RunStatuses";
import { PrismaClient, prisma } from "~/db.server";
import { DirectionSchema } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam._index/route";
import { getUsername } from "~/utils/username";

export type Direction = z.infer<typeof DirectionSchema>;

type RunListOptions = {
userId: string;
jobSlug?: string;
organizationSlug: string;
projectSlug: string;
direction?: Direction;
filterStatus?: JobRunStatus[];
filterEnvironment?: RuntimeEnvironmentType;
filterStatus?: FilterableStatus;
filterEnvironment?: FilterableEnvironment;
cursor?: string;
pageSize?: number;
};
Expand All @@ -40,6 +42,8 @@ export class RunListPresenter {
cursor,
pageSize = DEFAULT_PAGE_SIZE,
}: RunListOptions) {
const filterStatuses = filterStatus ? filterableStatuses[filterStatus] : undefined;

const directionMultiplier = direction === "forward" ? 1 : -1;

// Find the organization that the user is a member of
Expand Down Expand Up @@ -120,7 +124,7 @@ export class RunListPresenter {
environmentId: {
in: environments.map((environment) => environment.id),
},
status: filterStatus ? { in: filterStatus } : undefined,
status: filterStatuses ? { in: filterStatuses } : undefined,
environment: filterEnvironment ? { type: filterEnvironment } : undefined,
},
orderBy: [{ id: "desc" }],
Expand Down
3 changes: 2 additions & 1 deletion apps/webapp/app/presenters/TriggerSourcePresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { TriggerSource, User } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { Direction, RunList, RunListPresenter } from "./RunListPresenter.server";
import { RunList, RunListPresenter } from "./RunListPresenter.server";
import { Direction } from "~/components/runs/RunStatuses";

export class TriggerSourcePresenter {
#prismaClient: PrismaClient;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Direction } from "~/components/runs/RunStatuses";
import { PrismaClient, prisma } from "~/db.server";
import { Direction } from "./RunListPresenter.server";

type RunListOptions = {
userId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { User, Webhook } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { Direction } from "./RunListPresenter.server";
import { organizationPath, projectPath } from "~/utils/pathBuilder";
import { WebhookDeliveryListPresenter } from "./WebhookDeliveryListPresenter.server";
import { Direction } from "~/components/runs/RunStatuses";

export class WebhookDeliveryPresenter {
#prismaClient: PrismaClient;
Expand Down
3 changes: 2 additions & 1 deletion apps/webapp/app/presenters/WebhookSourcePresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { User, Webhook } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { Direction, RunListPresenter } from "./RunListPresenter.server";
import { RunListPresenter } from "./RunListPresenter.server";
import { organizationPath, projectPath } from "~/utils/pathBuilder";
import { Direction } from "~/components/runs/RunStatuses";

export class WebhookSourcePresenter {
#prismaClient: PrismaClient;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useLocation } from "@remix-run/react";
import { LinkButton } from "~/components/primitives/Buttons";
import { Direction, RunList } from "~/presenters/RunListPresenter.server";
import { Direction } from "~/components/runs/RunStatuses";
import { RunList } from "~/presenters/RunListPresenter.server";
import { WebhookDeliveryList } from "~/presenters/WebhookDeliveryListPresenter.server";
import { cn } from "~/utils/cn";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,8 @@ import {
organizationIntegrationsPath,
} from "~/utils/pathBuilder";
import { ListPagination } from "./ListPagination";

export const DirectionSchema = z.union([z.literal("forward"), z.literal("backward")]);

export const RunListSearchSchema = z.object({
cursor: z.string().optional(),
direction: DirectionSchema.optional(),
});
import { RunListSearchSchema } from "~/components/runs/RunStatuses";
import { RunsFilters } from "~/components/runs/RunFilters";

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
Expand All @@ -39,6 +34,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const presenter = new RunListPresenter();
const list = await presenter.call({
userId,
filterEnvironment: searchParams.environment,
filterStatus: searchParams.status,
jobSlug: jobParam,
projectSlug: projectParam,
organizationSlug,
Expand Down Expand Up @@ -73,10 +70,14 @@ export default function Page() {
{(open) => (
<div className={cn("grid h-fit gap-4", open ? "grid-cols-2" : "grid-cols-1")}>
<div>
<div className="mb-2 flex items-center justify-end gap-x-2">
<HelpTrigger title="How do I run my Job?" />
<ListPagination list={list} />
<div className="mb-2 flex items-center justify-between gap-x-2">
<RunsFilters />
<div className="flex items-center justify-end gap-x-2">
<HelpTrigger title="How do I run my Job?" />
<ListPagination list={list} />
</div>
</div>

<RunsTable
total={list.runs.length}
hasFilters={false}
Expand Down
Loading

0 comments on commit d1092fc

Please sign in to comment.