Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
@@ -0,0 +1,172 @@
import { Form, useFetcher } from "@remix-run/react";
import { Button } from "~/components/primitives/Buttons";
import { FormButtons } from "~/components/primitives/FormButtons";
import { ActionFunction, json } from "@remix-run/server-runtime";
import { requireUserId } from "~/services/session.server";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { personalAccessTokensPath } from "~/utils/pathBuilder";
import { NamedIcon } from "~/components/primitives/NamedIcon";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { prisma } from "~/db.server";
import { customAlphabet } from "nanoid";
import { Spinner } from "~/components/primitives/Spinner";
import {
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { ClipboardField } from "~/components/primitives/ClipboardField";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import {
PageDescription,
PageHeader,
PageTitle,
PageTitleRow,
} from "~/components/primitives/PageHeader";
import { DateTime } from "~/components/primitives/DateTime";
import { Badge } from "~/components/primitives/Badge";
import { cn } from "~/utils/cn";

export const loader = async ({ request }: { request: Request }) => {
const userid = await requireUserId(request);
const tokens = await prisma.personalAccessToken.findMany({
where: {
userId: userid,
},
});
return typedjson({ tokens });
};

export const action: ActionFunction = async ({ request }) => {
const userId = await requireUserId(request);
try {
const apiKeyId = customAlphabet(
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
12
);

const personalAccessToken = `tr_pat_${apiKeyId(20)}`;

await prisma.personalAccessToken.create({
data: {
token: personalAccessToken,
userId: userId,
},
});

return redirectWithSuccessMessage(
personalAccessTokensPath(),
request,
"Personal Token Access Generated."
);
} catch (error: any) {
return json({ errors: { body: error.message } }, { status: 400 });
}
};

export default function Page() {
const { tokens } = useTypedLoaderData<typeof loader>();
const fetcher = useFetcher();

const isLoading =
fetcher.state === "submitting" ||
(fetcher.state === "loading" && fetcher.formMethod === "DELETE");

const badgeClass =
"py-1 px-1.5 text-xs font-normal inline-flex items-center justify-center whitespace-nowrap rounded-sm";

return (
<PageContainer>
<PageHeader>
<PageTitleRow>
<PageTitle title="Personal Access Tokens" />
</PageTitleRow>
<PageDescription>Manage your Personal Access Tokens.</PageDescription>
</PageHeader>
<PageBody>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Token</TableHeaderCell>
<TableHeaderCell>Last accessed at</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Action</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{tokens.length > 0 &&
tokens.map((token) => {
return (
<TableRow>
<TableCell>
<ClipboardField
className="w-full max-w-none"
secure
value={token.token}
variant={"primary/medium"}
/>
</TableCell>
<TableCell>
{token.lastAccessedAt ? (
<DateTime date={token.lastAccessedAt} />
) : (
"Not used yet"
)}
</TableCell>
<TableCell>
{token.revokedAt === null ? (
<Badge className={cn(badgeClass, "bg-slate-800 text-green-500")}>
<span>Active</span>
</Badge>
) : (
<Badge className={cn(badgeClass, "bg-rose-600 text-white")}>
<span>Revoked</span>
</Badge>
)}
</TableCell>
<TableCell>
<fetcher.Form method="delete" action={`/personal-access-tokens/${token.id}`}>
<Button
variant="danger/large"
fullWidth
disabled={token.revokedAt !== null}
>
{isLoading ? (
<Spinner />
) : (
<>
<NamedIcon
name="close"
className="mr-1.5 h-4 w-4 text-bright transition group-hover:text-bright"
/>
Revoke
</>
)}
</Button>
</fetcher.Form>
</TableCell>
</TableRow>
);
})}
{tokens.length === 0 && <h1>You have no generatde tokens</h1>}
</TableBody>
</Table>

<div className="my-4 flex w-full justify-end">
<Form method="post" className="max-w-md">
<FormButtons
confirmButton={
<Button type="submit" variant={"primary/large"}>
Generate Token
</Button>
}
/>
</Form>
</div>
</PageBody>
</PageContainer>
);
}
2 changes: 2 additions & 0 deletions apps/webapp/app/routes/_app._orgaccount/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
organizationsPath,
accountPath,
logoutPath,
personalAccessTokensPath,
} from "~/utils/pathBuilder";

export default function Page() {
Expand All @@ -38,6 +39,7 @@ export default function Page() {
tabs={[
{ label: "Organizations", to: organizationsPath() },
{ label: "Account", to: accountPath() },
{ label: "Personal Access Tokens", to: personalAccessTokensPath() },
]}
/>
</PageHeader>
Expand Down
44 changes: 44 additions & 0 deletions apps/webapp/app/routes/api.v2.cancel-job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { authenticateApiRequest } from "~/services/PATAuth.server";
import { generateErrorMessage } from "zod-error";
import { CancelRunService } from "~/services/runs/cancelRun.server";
import { logger } from "~/services/logger.server";
import { CancelJobSchema } from "@trigger.dev/core";


export async function action({ request }: ActionArgs) {
const authenticationResult = await authenticateApiRequest(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing Personal Access Token" }, { status: 401 });
}
const anyBody = await request.json();
const body = CancelJobSchema.safeParse(anyBody);

if (!body.success) {
return json({ message: generateErrorMessage(body.error.issues) }, { status: 422 });
}
const { runId } = body.data;

try {
const cancelRunService = new CancelRunService();
await cancelRunService.call({ runId });

return json({ message: "Canceled run. Any pending tasks will be canceled." }, { status: 200 });
} catch (error) {
if (error instanceof Error) {
logger.error("Failed to cancel run", {
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
});
return json({ errors: { body: error.message } }, { status: 400 });
} else {
logger.error("Failed to cancel run", { error });
return json({ errors: { body: "Unknown error" } }, { status: 400 });
}
}
}
89 changes: 89 additions & 0 deletions apps/webapp/app/routes/api.v2.job-runs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { authenticateApiRequest } from "~/services/PATAuth.server";
import { generateErrorMessage } from "zod-error";
import { requireUserIdByPAT } from "~/services/session.server";
import { prisma } from "~/db.server";
import { JobRunsSchema } from "@trigger.dev/core";


export async function action({ request }: ActionArgs) {
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}

const authenticationResult = await authenticateApiRequest(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing Personal Access Token" }, { status: 401 });
}
const userId = await requireUserIdByPAT(authenticationResult);

if (!userId) {
return json({ error: "Invalid or Missing Personal Access Token" }, { status: 401 });
}
const anyBody = await request.json();

const body = JobRunsSchema.safeParse(anyBody);

if (!body.success) {
return json({ message: generateErrorMessage(body.error.issues) }, { status: 422 });
}
const { jobSlug, projectSlug, organizationSlug, status, environment } = body.data;

const runs = await prisma.jobRun.findMany({
select: {
id: true,
number: true,
startedAt: true,
completedAt: true,
createdAt: true,
isTest: true,
status: true,
environment: {
select: {
type: true,
slug: true,
orgMember: {
select: {
userId: true,
},
},
},
},
version: {
select: {
version: true,
},
},
},
where: {
...(status ? { status: status } : {}),
job: {
slug: jobSlug,
},
project: {
slug: projectSlug,
},
organization: { slug: organizationSlug, members: { some: { userId } } },
environment: {
...(environment ? { type: environment } : {}),
OR: [
{
orgMember: null,
},
{
orgMember: {
userId,
},
},
],
},
},
orderBy: [{ id: "desc" }],
});

return json({
data: runs,
});
}
41 changes: 41 additions & 0 deletions apps/webapp/app/routes/api.v2.rerun-job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { authenticateApiRequest } from "~/services/PATAuth.server";
import { generateErrorMessage } from "zod-error";
import { ContinueRunService } from "~/services/runs/continueRun.server";
import { ReRunService } from "~/services/runs/reRun.server";
import { RerunJobSchema } from "@trigger.dev/core";

export async function action({ request }: ActionArgs) {
const authenticationResult = await authenticateApiRequest(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing Personal Access Token" }, { status: 401 });
}
const anyBody = await request.json();

const body = RerunJobSchema.safeParse(anyBody);

if (!body.success) {
return json({ message: generateErrorMessage(body.error.issues) }, { status: 422 });
}
try {
const { runId, intent } = body.data
if (intent === "start") {
const rerunService = new ReRunService();
const run = await rerunService.call({ runId });

if (!run) {
return json({ message: "Unable to retry run" }, { status: 400 });
}
return json({ message: `Created new run`, runId: run.id });
} else if (intent === "continue") {

const continueService = new ContinueRunService();
await continueService.call({ runId });
return json({ message: `Resuming run ${runId}` });
}
} catch (error) {
return json({ errors: { body: (error as Error).message } }, { status: 400 });
}
}
Loading