RunsTable displaying for registration attempts

This commit is contained in:
Matt Aitken
2023-07-10 14:40:10 +01:00
parent 4373538476
commit 75a07fe4ef
13 changed files with 348 additions and 279 deletions
@@ -21,7 +21,7 @@ import {
projectIntegrationsPath,
projectPath,
projectTriggersPath,
runDashboardPath,
jobRunDashboardPath,
} from "~/utils/pathBuilder";
import { BreadcrumbIcon } from "../primitives/BreadcrumbIcon";
import { JobsMenu } from "./JobsMenu";
@@ -29,7 +29,7 @@ import { BreadcrumbLink } from "./NavBar";
import { ProjectsMenu } from "./ProjectsMenu";
export type Breadcrumb = {
slug:
slug?:
| "projects"
| "jobs"
| "integrations"
@@ -45,22 +45,12 @@ export type Breadcrumb = {
| "runs"
| "run";
link?: {
to: string;
title: string;
};
};
function useBreadcrumbs(): Breadcrumb[] {
const matches = useMatches();
return matches
.filter((match) => match.handle)
.filter((match) => match.handle!.breadcrumb)
.map((match) => match.handle!.breadcrumb as Breadcrumb);
}
export function Breadcrumb() {
const breadcrumbs = useBreadcrumbs();
const matches = useMatches();
const organization = useOptionalOrganization();
const project = useOptionalProject();
const job = useOptionalJob();
@@ -69,19 +59,32 @@ export function Breadcrumb() {
return (
<div className="hidden items-center md:flex">
{breadcrumbs.map((breadcrumb, index) => (
<Fragment key={breadcrumb.slug}>
<BreadcrumbIcon />
<BreadcrumbItem
breadcrumb={breadcrumb}
organization={organization}
project={project}
job={job}
run={run}
client={client}
/>
</Fragment>
))}
{matches.map((match) => {
if (!match.handle || !match.handle.breadcrumb) return null;
const breadcrumb = match.handle.breadcrumb as Breadcrumb;
return (
<Fragment key={match.id}>
<BreadcrumbIcon />
{breadcrumb.link ? (
<BreadcrumbLink
to={match.pathname}
title={breadcrumb.link.title}
/>
) : (
<BreadcrumbItem
breadcrumb={breadcrumb}
organization={organization}
project={project}
job={job}
run={run}
client={client}
/>
)}
</Fragment>
);
})}
</div>
);
}
@@ -226,7 +229,7 @@ function BreadcrumbItem({
/>
<BreadcrumbIcon />
<BreadcrumbLink
to={runDashboardPath(organization, project, job, run)}
to={jobRunDashboardPath(organization, project, job, run)}
title={`Run #${run.number}`}
/>
</Fragment>
@@ -17,6 +17,7 @@ import {
BuildingOffice2Icon,
CalendarDaysIcon,
ChatBubbleLeftEllipsisIcon,
CheckCircleIcon,
CheckIcon,
ChevronDownIcon,
ChevronLeftIcon,
@@ -47,6 +48,7 @@ import {
UserIcon,
UserPlusIcon,
WrenchScrewdriverIcon,
XCircleIcon,
XMarkIcon,
} from "@heroicons/react/24/solid";
import { ScheduleIcon } from "~/assets/icons/ScheduleIcon";
@@ -61,6 +63,9 @@ const icons = {
account: (className: string) => (
<UserCircleIcon className={cn("text-slate-400", className)} />
),
active: (className: string) => (
<CheckCircleIcon className={cn("text-green-500", className)} />
),
"arrow-right": (className: string) => (
<ArrowRightIcon className={cn("text-white", className)} />
),
@@ -142,6 +147,9 @@ const icons = {
id: (className: string) => (
<FingerPrintIcon className={cn("text-rose-200", className)} />
),
inactive: (className: string) => (
<XCircleIcon className={cn("text-rose-500", className)} />
),
info: (className: string) => (
<InformationCircleIcon className={cn("text-blue-500", className)} />
),
+32 -12
View File
@@ -5,7 +5,12 @@ import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { RunList } from "~/presenters/RunListPresenter.server";
import { formatDuration } from "~/utils";
import { runDashboardPath } from "~/utils/pathBuilder";
import {
JobForPath,
OrgForPath,
ProjectForPath,
jobRunDashboardPath,
} from "~/utils/pathBuilder";
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
import { Callout } from "../primitives/Callout";
import { DateTime } from "../primitives/DateTime";
@@ -21,22 +26,37 @@ import {
TableRow,
} from "../primitives/Table";
import { RunStatus } from "./RunStatuses";
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
type RunTableItem = {
id: string;
number: number;
environment: {
type: RuntimeEnvironmentType;
};
status: JobRunStatus;
startedAt: Date | null;
completedAt: Date | null;
createdAt: Date | null;
version: string;
isTest: boolean;
};
type RunsTableProps = {
total: number;
hasFilters: boolean;
runs: RunTableItem[];
isLoading?: boolean;
runsParentPath: string;
};
export function RunsTable({
total,
hasFilters,
runs,
isLoading = false,
}: {
total: number;
hasFilters: boolean;
runs: RunList["runs"];
isLoading?: boolean;
}) {
const organization = useOrganization();
const project = useProject();
const job = useJob();
runsParentPath,
}: RunsTableProps) {
return (
<Table>
<TableHeader>
@@ -65,7 +85,7 @@ export function RunsTable({
</TableBlankRow>
) : (
runs.map((run) => {
const path = runDashboardPath(organization, project, job, run);
const path = `${runsParentPath}/${run.id}/trigger`;
return (
<TableRow key={run.id}>
<TableCell to={path}>#{run.number}</TableCell>
@@ -1,58 +0,0 @@
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";
export class DynamicTriggerSourcePresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({
userId,
projectSlug,
organizationSlug,
triggerSourceId,
}: {
userId: User["id"];
projectSlug: Project["slug"];
organizationSlug: Organization["slug"];
triggerSourceId: TriggerSource["id"];
}) {
const trigger = await this.#prismaClient.triggerSource.findUnique({
select: {
id: true,
active: true,
dynamicTrigger: true,
integration: {
select: {
id: true,
title: true,
slug: true,
definitionId: true,
setupStatus: true,
},
},
environment: {
select: {
type: true,
},
},
createdAt: true,
updatedAt: true,
params: true,
registrations: true,
sourceRegistrationJob: true,
},
where: {
id: triggerSourceId,
},
});
return {
trigger,
};
}
}
@@ -0,0 +1,119 @@
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";
export class TriggerSourcePresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({
userId,
projectSlug,
organizationSlug,
triggerSourceId,
}: {
userId: User["id"];
projectSlug: Project["slug"];
organizationSlug: Organization["slug"];
triggerSourceId: TriggerSource["id"];
}) {
const trigger = await this.#prismaClient.triggerSource.findUnique({
select: {
id: true,
active: true,
integration: {
select: {
id: true,
title: true,
slug: true,
definitionId: true,
setupStatus: true,
},
},
environment: {
select: {
type: true,
},
},
createdAt: true,
updatedAt: true,
params: true,
sourceRegistrationJob: {
select: {
job: {
select: {
id: true,
slug: true,
},
},
runs: {
select: {
id: true,
number: true,
environment: {
select: {
type: true,
},
},
status: true,
startedAt: true,
completedAt: true,
createdAt: true,
version: {
select: {
version: true,
},
},
isTest: true,
},
orderBy: {
id: "desc",
},
take: 20,
},
},
},
},
where: {
id: triggerSourceId,
},
});
if (!trigger) {
throw new Error("Trigger source not found");
}
return {
trigger: {
id: trigger.id,
active: trigger.active,
integration: trigger.integration,
environment: trigger.environment,
createdAt: trigger.createdAt,
updatedAt: trigger.updatedAt,
params: trigger.params,
registrationJob: trigger.sourceRegistrationJob
? {
id: trigger.sourceRegistrationJob.job.id,
slug: trigger.sourceRegistrationJob.job.slug,
runs: trigger.sourceRegistrationJob.runs.map((r) => ({
id: r.id,
number: r.number,
environment: r.environment,
status: r.status,
startedAt: r.startedAt,
completedAt: r.completedAt,
createdAt: r.createdAt,
version: r.version.version,
isTest: r.isTest,
})),
}
: undefined,
},
};
}
}
@@ -9,7 +9,11 @@ import { RunListPresenter } from "~/presenters/RunListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { JobParamsSchema, projectIntegrationsPath } from "~/utils/pathBuilder";
import {
JobParamsSchema,
projectIntegrationsPath,
jobRunsParentPath,
} from "~/utils/pathBuilder";
import { ListPagination } from "./ListPagination";
import { Callout } from "~/components/primitives/Callout";
import { useOrganization } from "~/hooks/useOrganizations";
@@ -97,6 +101,7 @@ export default function Page() {
hasFilters={false}
runs={list.runs}
isLoading={isLoading}
runsParentPath={jobRunsParentPath(organization, project, job)}
/>
<ListPagination list={list} className="mt-2 justify-end" />
</div>
@@ -58,7 +58,7 @@ import {
RunParamsSchema,
jobPath,
runCompletedPath,
runDashboardPath,
jobRunDashboardPath,
runStreamingPath,
runTaskPath,
runTriggerPath,
@@ -121,7 +121,7 @@ export const action: ActionFunction = async ({ request, params }) => {
}
return redirectWithSuccessMessage(
runDashboardPath(
jobRunDashboardPath(
{ slug: organizationSlug },
{ slug: projectParam },
{ slug: jobParam },
@@ -135,7 +135,7 @@ export const action: ActionFunction = async ({ request, params }) => {
await continueService.call({ runId: runParam });
return redirectWithSuccessMessage(
runDashboardPath(
jobRunDashboardPath(
{ slug: organizationSlug },
{ slug: projectParam },
{ slug: jobParam },
@@ -31,7 +31,7 @@ import { TestJobService } from "~/services/jobs/testJob.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { JobParamsSchema, runDashboardPath } from "~/utils/pathBuilder";
import { JobParamsSchema, jobRunDashboardPath } from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -102,7 +102,7 @@ export const action: ActionFunction = async ({ request, params }) => {
}
return redirectWithSuccessMessage(
runDashboardPath(
jobRunDashboardPath(
{ slug: organizationSlug },
{ slug: projectParam },
{ slug: jobParam },
@@ -17,6 +17,7 @@ import {
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
@@ -53,7 +54,9 @@ export const loader = async ({ request, params }: LoaderArgs) => {
export const handle: Handle = {
breadcrumb: {
slug: "triggers",
link: {
title: "External",
},
},
};
@@ -64,7 +67,6 @@ export default function Integrations() {
return (
<>
<Header2 spacing>External Triggers</Header2>
<Paragraph variant="small" spacing>
Webhooks are connected as external Triggers
</Paragraph>
@@ -79,75 +81,81 @@ export default function Integrations() {
</TableRow>
</TableHeader>
<TableBody>
{triggers.map((t) => {
const path = triggerSourcePath(organization, project, t);
return (
<TableRow
key={t.id}
className={cn(!t.active && "bg-rose-500/30")}
>
<TableCell to={path}>
<div className="flex items-center gap-1">
<NamedIcon
name={t.integration.definitionId}
className="h-8 w-8"
/>
<LabelValueStack
label={t.integration.title}
value={t.integration.slug}
variant="primary"
/>
</div>
</TableCell>
<TableCell to={path}>
{t.params && (
<SimpleTooltip
button={
<div className="flex max-w-[200px] items-start justify-start gap-5 truncate">
{Object.entries(t.params).map(
([label, value], index) => (
<LabelValueStack
key={index}
label={label}
value={value}
className="last:truncate"
/>
)
)}
</div>
}
content={
<div className="flex flex-col gap-2">
{Object.entries(t.params).map(
([label, value], index) => (
<LabelValueStack
key={index}
label={label}
value={value}
/>
)
)}
</div>
}
/>
)}
</TableCell>
<TableCell to={path}>
{t.active ? (
<CheckCircleIcon className="h-6 w-6 text-green-500" />
) : (
<XCircleIcon className="h-6 w-6 text-rose-500" />
)}
</TableCell>
<TableCell to={path}>
<span className="flex">
<EnvironmentLabel environment={t.environment} />
</span>
</TableCell>
<TableCellChevron to={path} />
</TableRow>
);
})}
{triggers.length > 0 ? (
triggers.map((t) => {
const path = triggerSourcePath(organization, project, t);
return (
<TableRow
key={t.id}
className={cn(!t.active && "bg-rose-500/30")}
>
<TableCell to={path}>
<div className="flex items-center gap-1">
<NamedIcon
name={t.integration.definitionId}
className="h-8 w-8"
/>
<LabelValueStack
label={t.integration.title}
value={t.integration.slug}
variant="primary"
/>
</div>
</TableCell>
<TableCell to={path}>
{t.params && (
<SimpleTooltip
button={
<div className="flex max-w-[200px] items-start justify-start gap-5 truncate">
{Object.entries(t.params).map(
([label, value], index) => (
<LabelValueStack
key={index}
label={label}
value={value}
className="last:truncate"
/>
)
)}
</div>
}
content={
<div className="flex flex-col gap-2">
{Object.entries(t.params).map(
([label, value], index) => (
<LabelValueStack
key={index}
label={label}
value={value}
/>
)
)}
</div>
}
/>
)}
</TableCell>
<TableCell to={path}>
{t.active ? (
<CheckCircleIcon className="h-6 w-6 text-green-500" />
) : (
<XCircleIcon className="h-6 w-6 text-rose-500" />
)}
</TableCell>
<TableCell to={path}>
<span className="flex">
<EnvironmentLabel environment={t.environment} />
</span>
</TableCell>
<TableCellChevron to={path} />
</TableRow>
);
})
) : (
<TableBlankRow colSpan={5}>
<Paragraph>No External triggers</Paragraph>
</TableBlankRow>
)}
</TableBody>
</Table>
</>
@@ -10,10 +10,7 @@ import {
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { Handle } from "~/utils/handle";
import {
projectScheduledTriggersPath,
projectTriggersPath,
} from "~/utils/pathBuilder";
import { projectTriggersPath } from "~/utils/pathBuilder";
export const handle: Handle = {
breadcrumb: {
@@ -27,7 +24,7 @@ export default function Integrations() {
return (
<PageContainer>
<PageHeader>
<PageHeader hideBorder>
<PageTitleRow>
<PageTitle title="Triggers" />
</PageTitleRow>
@@ -37,7 +34,7 @@ export default function Integrations() {
<PageTabs
tabs={[
{
label: "External",
label: "External Triggers",
to: projectTriggersPath(organization, project),
},
// {
@@ -1,7 +1,10 @@
import { Response } from "@remix-run/node";
import type { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Callout } from "~/components/primitives/Callout";
import { Header2 } from "~/components/primitives/Headers";
import {
PageHeader,
PageInfoGroup,
@@ -10,14 +13,17 @@ import {
PageTitle,
PageTitleRow,
} from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RunsTable } from "~/components/runs/RunsTable";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { DynamicTriggerSourcePresenter } from "~/presenters/DynamicTriggerSourcePresenter.server";
import { TriggerSourcePresenter } from "~/presenters/TriggerSourcePresenter.server";
import { requireUser } from "~/services/session.server";
import { Handle } from "~/utils/handle";
import {
TriggerSourceParamSchema,
projectTriggersPath,
triggerSourceRunsPath,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderArgs) => {
@@ -25,7 +31,7 @@ export const loader = async ({ request, params }: LoaderArgs) => {
const { organizationSlug, projectParam, triggerParam } =
TriggerSourceParamSchema.parse(params);
const presenter = new DynamicTriggerSourcePresenter();
const presenter = new TriggerSourcePresenter();
const { trigger } = await presenter.call({
userId: user.id,
organizationSlug,
@@ -49,8 +55,6 @@ export const handle: Handle = {
},
};
//todo outlet somewhere, look at how I did this with the integration client page.
export default function Integrations() {
const { trigger } = useTypedLoaderData<typeof loader>();
const organization = useOrganization();
@@ -61,7 +65,7 @@ export default function Integrations() {
<PageHeader>
<PageTitleRow>
<PageTitle
title="Trigger detail"
title={`${trigger.integration.title}: ${trigger.integration.slug}`}
backButton={{
to: projectTriggersPath(organization, project),
text: "Triggers",
@@ -72,97 +76,44 @@ export default function Integrations() {
<PageInfoGroup>
<PageInfoProperty
icon={trigger.integration.definitionId}
label="Integration"
value={trigger.integration.title}
label={trigger.integration.title ?? ""}
value={trigger.integration.slug}
/>
<PageInfoProperty
icon={trigger.active ? "active" : "inactive"}
label="Active"
value={trigger.active ? "Yes" : "No"}
/>
<PageInfoProperty
label="Environment"
value={<EnvironmentLabel environment={trigger.environment} />}
/>
</PageInfoGroup>
</PageInfoRow>
</PageHeader>
<PageBody scrollable={false}>
{/* <div className="h-full overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<div>
<Header2 spacing>External Triggers</Header2>
<Paragraph variant="small" spacing>
Webhooks are connected as external Triggers
</Paragraph>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Integration</TableHeaderCell>
<TableHeaderCell>Properties</TableHeaderCell>
<TableHeaderCell>Active</TableHeaderCell>
<TableHeaderCell>Environment</TableHeaderCell>
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{triggers.map((t) => {
const path = triggerSourcePath(organization, project, t);
return (
<TableRow key={t.id}>
<TableCell to={path}>
<span className="flex items-center gap-1">
<NamedIcon
name={t.integration.definitionId}
className="h-6 w-6"
/>
{t.integration.title}
</span>
</TableCell>
<TableCell to={path}>
{t.params && (
<SimpleTooltip
button={
<div className="flex max-w-[200px] items-start justify-start gap-5 truncate">
{Object.entries(t.params).map(
([label, value], index) => (
<LabelValueStack
key={index}
label={label}
value={value}
className="last:truncate"
/>
)
)}
</div>
}
content={
<div className="flex flex-col gap-2">
{Object.entries(t.params).map(
([label, value], index) => (
<LabelValueStack
key={index}
label={label}
value={value}
/>
)
)}
</div>
}
/>
)}
</TableCell>
<TableCell to={path}>
{t.active ? (
<CheckCircleIcon className="h-6 w-6 text-green-500" />
) : (
<XCircleIcon className="h-6 w-6 text-rose-500" />
)}
</TableCell>
<TableCell to={path}>
<span className="flex">
<EnvironmentLabel environment={t.environment} />
</span>
</TableCell>
<TableCellChevron to={path} />
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</div> */}
<div className="h-full overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
<Header2 spacing>Trigger registration runs</Header2>
<Paragraph variant="small" spacing>
External Triggers need to be registered with the external service.
You can see the list of attempted registrations below.
</Paragraph>
{trigger.registrationJob ? (
<RunsTable
runs={trigger.registrationJob.runs}
total={trigger.registrationJob.runs.length}
hasFilters={false}
runsParentPath={triggerSourceRunsPath(
organization,
project,
trigger
)}
/>
) : (
<Callout variant="warning">No registration runs found</Callout>
)}
</div>
</PageBody>
</PageContainer>
);
+24 -8
View File
@@ -4,12 +4,12 @@ import { Job } from "~/models/job.server";
import type { Organization } from "~/models/organization.server";
import type { Project } from "~/models/project.server";
type OrgForPath = Pick<Organization, "slug">;
type ProjectForPath = Pick<Project, "slug">;
type JobForPath = Pick<Job, "slug">;
type RunForPath = Pick<Job, "id">;
type IntegrationForPath = Pick<Integration, "slug">;
type TriggerForPath = Pick<TriggerSource, "id">;
export type OrgForPath = Pick<Organization, "slug">;
export type ProjectForPath = Pick<Project, "slug">;
export type JobForPath = Pick<Job, "slug">;
export type RunForPath = Pick<Job, "id">;
export type IntegrationForPath = Pick<Integration, "slug">;
export type TriggerForPath = Pick<TriggerSource, "id">;
export const OrganizationParamsSchema = z.object({
organizationSlug: z.string(),
@@ -186,6 +186,14 @@ export function triggerSourcePath(
)}/triggersources/${triggerSourceParam(trigger)}`;
}
export function triggerSourceRunsPath(
organization: OrgForPath,
project: ProjectForPath,
trigger: TriggerForPath
) {
return `${triggerSourcePath(organization, project, trigger)}/runs`;
}
function triggerSourceParam(trigger: TriggerForPath) {
return trigger.id;
}
@@ -228,16 +236,24 @@ export function jobParam(job: JobForPath) {
}
// Run
export function jobRunsParentPath(
organization: OrgForPath,
project: ProjectForPath,
job: JobForPath
) {
return `${jobPath(organization, project, job)}/runs`;
}
function runPath(
organization: OrgForPath,
project: ProjectForPath,
job: JobForPath,
run: RunForPath
) {
return `${jobPath(organization, project, job)}/runs/${runParam(run)}`;
return `${jobRunsParentPath(organization, project, job)}/${runParam(run)}`;
}
export function runDashboardPath(
export function jobRunDashboardPath(
organization: OrgForPath,
project: ProjectForPath,
job: JobForPath,
+1 -1
View File
@@ -51,7 +51,7 @@ new Job(client, {
version: "0.1.1",
trigger: typeform.onFormResponse({
uid: "QQnotGJM",
tag: "tag3",
tag: "tag4",
}),
run: async (payload, io, ctx) => {},
});