Commit

Runners are owner-only: the page, its actions, the Settings entry and the links to it

Members no longer see the workspace's machines or registration tokens. A self-hosted job's badge links to Runners only for owners; mission control's stuck-job card offers the link to owners only.

syntaqxcommitted Parentb6c6403Browse files
4 files+37−210/4 viewed
+5−3
723723 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
724724 Secrets and variables
725725 </SidebarLink>
726− <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
727− Runners
728− </SidebarLink>
726+ {owner && (
727+ <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
728+ Runners
729+ </SidebarLink>
730+ )}
729731 </SidebarGroup>
730732 <SidebarGroup title="Connections" className="mt-3">
731733 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
+3−2
6666 repos as reposApi,
6767 work,
6868 } from "../lib/services.server";
69−import { assertSameOrigin, getViewer, requireUser } from "../lib/session.server";
69+import { assertSameOrigin, getViewer, requireUser, roleIn } from "../lib/session.server";
7070 import { runners } from "../lib/runners.server";
7171 import { readableRepos } from "../lib/access.server";
7272
431431 { label: "Labels", value: job.labels, tone: null },
432432 { label: "Waiting", value: `${minutes} min`, tone: "warn" },
433433 ],
434− link: { label: "Runners", to: `/${namespace}/-/runners` },
434+ // Runners are the owners' to see to.
435+ ...(namespace && roleIn(viewer, namespace) === "owner" ? { link: { label: "Runners", to: `/${namespace}/-/runners` } } : {}),
435436 });
436437 }
437438 for (const run of liveRuns) {
+23−9
11 import { AlertTriangle, ChevronRight, Cloud, Download, GitBranch, GitCommitHorizontal, Info, Package, RotateCw, ServerCog, Square, XCircle } from "lucide-react";
22 import { type ReactNode } from "react";
3−import { Form, Link, useNavigation, useSearchParams } from "react-router";
3+import { Form, Link, useLoaderData, useNavigation, useSearchParams } from "react-router";
44
55 import type { Annotation, Job, StepState } from "@g1t/contracts";
66
1010 import { Button, ErrorText, TimeAgo } from "../../components/ui";
1111 import { listArtifacts } from "../../lib/artifacts.server";
1212 import { actions } from "../../lib/services.server";
13−import { assertSameOrigin, getViewer, requireUser, unwrap } from "../../lib/session.server";
13+import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
1414 import { accessTo, refusal } from "../../lib/access.server";
1515 import { useRefreshWhile } from "../../lib/refresh";
1616
2424 const detail = unwrap(await actions.run({ namespace: params.owner, name: params.repo }, viewer, params.id));
2525 const artifacts = await listArtifacts(params.id).catch(() => []);
2626 // Cancelling and re-running need Write.
27− return { detail, artifacts, member: (await accessTo(context, params)).can.run };
27+ return {
28+ detail,
29+ artifacts,
30+ member: (await accessTo(context, params)).can.run,
31+ // The runners page is the workspace owners'.
32+ runnersPage: roleIn(viewer, params.owner) === "owner",
33+ };
2834 }
2935
3036 export async function action({ request, params, context }: Route.ActionArgs) {
8995
9096 /** Where the job ran: g1t's own runners, or a self-hosted one by name. */
9197 function RanOn({ job, workspace }: { job: Job; workspace: string }) {
98+ const runnersPage = useLoaderData<typeof loader>().runnersPage;
9299 if (job.selfHosted) {
93− return (
94− <Link
95− to={`/${workspace}/-/runners`}
96− className="ml-auto inline-flex items-center gap-1.5 rounded-full border border-line px-2.5 py-0.5 text-xs text-muted hover:text-fg"
97− title="Self-hosted runner"
98− >
100+ const badge = "ml-auto inline-flex items-center gap-1.5 rounded-full border border-line px-2.5 py-0.5 text-xs text-muted";
101+ const label = (
102+ <>
99103 <ServerCog size={13} />
100104 {job.runner ? `Self-hosted: ${job.runner}` : "Self-hosted"}
105+ </>
106+ );
107+ // A link to the runners for those who manage them, a label for everyone else.
108+ return runnersPage ? (
109+ <Link to={`/${workspace}/-/runners`} className={`${badge} hover:text-fg`} title="Self-hosted runner">
110+ {label}
101111 </Link>
112+ ) : (
113+ <span className={badge} title="Self-hosted runner">
114+ {label}
115+ </span>
102116 );
103117 }
104118 if (!job.startedAt) return null;
+6−7
1010 }
1111
1212 export async function loader({ params, context, request }: Route.LoaderArgs) {
13− const role = roleIn(getViewer(context), params.owner);
14− if (!role) throw new Response(null, { status: 404 });
13+ // The workspace's own machines and their tokens: owners only.
14+ if (roleIn(getViewer(context), params.owner) !== "owner") throw new Response(null, { status: 404 });
1515 const user = requireUser(context, request);
1616 const workspace = params.owner.toLowerCase();
17− // Members see the runners; owners add, remove and group them.
18− const names = role === "owner" ? (await repos.list(user, { namespace: workspace })).map((repo) => repo.name) : [];
19− return { role, ...(await loadRunners({ workspace }, user, names)) };
17+ const names = (await repos.list(user, { namespace: workspace })).map((repo) => repo.name);
18+ return loadRunners({ workspace }, user, names);
2019 }
2120
2221 export async function action({ request, params, context }: Route.ActionArgs) {
2322 assertSameOrigin(request);
23+ if (roleIn(getViewer(context), params.owner) !== "owner") throw new Response(null, { status: 404 });
2424 const user = requireUser(context, request);
2525 return actOnRunners({ workspace: params.owner.toLowerCase() }, user, await request.formData());
2626 }
2727
2828 export default function WorkspaceRunners({ loaderData, actionData }: Route.ComponentProps) {
29− const { role, ...data } = loaderData;
30− return <RunnersPanel data={data} action={actionData} scope="workspace" manage={role === "owner"} />;
29+ return <RunnersPanel data={loaderData} action={actionData} scope="workspace" manage />;
3130 }