ankka-console 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/control-plane.d.ts +87 -1
- package/dist/client/control-plane.js +201 -1
- package/dist/client/schemas.d.ts +1835 -0
- package/dist/client/schemas.js +595 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.js +73 -0
- package/dist/extensions/render.d.ts +2 -0
- package/dist/extensions/render.js +5 -0
- package/dist/extensions/types.d.ts +1 -1
- package/dist/extensions/types.js +29 -0
- package/dist/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +7 -1
- package/dist/routes/auth.sign-out.js +8 -3
- package/dist/routes/front.d.ts +62 -2
- package/dist/routes/front.js +45 -10
- package/dist/routes/logs.d.ts +92 -0
- package/dist/routes/logs.js +15 -14
- package/dist/routes/machines.d.ts +78 -0
- package/dist/routes/machines.js +78 -0
- package/dist/routes/members.js +11 -8
- package/dist/routes/organization-new.js +7 -4
- package/dist/routes/organization.d.ts +29 -1
- package/dist/routes/organization.js +38 -9
- package/dist/routes/project-new.js +8 -4
- package/dist/routes/project.d.ts +347 -2
- package/dist/routes/project.js +182 -9
- package/dist/routes/service-apply.d.ts +5 -2
- package/dist/routes/service-apply.js +17 -14
- package/dist/routes/service-history.d.ts +32 -0
- package/dist/routes/service-history.js +82 -0
- package/dist/routes/service-topology.d.ts +210 -0
- package/dist/routes/service-topology.js +54 -0
- package/dist/routes/service.d.ts +91 -10
- package/dist/routes/service.js +87 -23
- package/dist/routes/stream.service.js +18 -1
- package/dist/routes/tokens.js +8 -6
- package/dist/routes.js +3 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +1 -0
- package/dist/styles.css +2 -481
- package/dist/testing/fake-control-plane.d.ts +334 -1
- package/dist/testing/fake-control-plane.js +1194 -15
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/scenarios.js +6 -0
- package/dist/theme.css +66 -0
- package/dist/ui/console.d.ts +19 -6
- package/dist/ui/console.js +9 -2
- package/dist/ui/errors.js +7 -5
- package/dist/ui/grants.d.ts +3 -0
- package/dist/ui/grants.js +13 -0
- package/dist/ui/hosting.d.ts +11 -0
- package/dist/ui/hosting.js +20 -0
- package/dist/ui/primitives/button.d.ts +11 -0
- package/dist/ui/primitives/button.js +21 -0
- package/dist/ui/primitives/select.d.ts +10 -0
- package/dist/ui/primitives/select.js +4 -0
- package/dist/ui/primitives/switch.d.ts +9 -0
- package/dist/ui/primitives/switch.js +4 -0
- package/dist/ui/shape.d.ts +4 -0
- package/dist/ui/shape.js +36 -0
- package/dist/ui/shell.d.ts +76 -0
- package/dist/ui/shell.js +114 -0
- package/dist/ui/tokens.d.ts +82 -0
- package/dist/ui/tokens.js +172 -0
- package/dist/ui/topology/Graph.d.ts +13 -0
- package/dist/ui/topology/Graph.js +56 -0
- package/dist/ui/topology/Table.d.ts +10 -0
- package/dist/ui/topology/Table.js +14 -0
- package/dist/ui/topology/layout.d.ts +113 -0
- package/dist/ui/topology/layout.js +177 -0
- package/dist/ui/use-stream.d.ts +251 -1
- package/dist/ui/use-stream.js +11 -1
- package/package.json +11 -3
package/dist/routes/logs.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* A service's logs, with the CLI's
|
|
4
|
-
* restart, how many lines, how far back
|
|
3
|
+
* A service's logs, with the CLI's choices: one instance or all, the container before the last
|
|
4
|
+
* restart, how many lines, how far back, and — for a pod that holds two — the platform's container
|
|
5
|
+
* instead of the developer's. With scripts running the page follows new lines as they are
|
|
5
6
|
* written, until the person pauses it; without, it shows what was there when the page was asked for.
|
|
6
7
|
*/
|
|
7
8
|
import { useMemo, useState } from "react";
|
|
8
9
|
import { Form, useLoaderData } from "react-router";
|
|
9
|
-
import { guard, pageData, useConsoleContext } from "../context.js";
|
|
10
|
+
import { applyPrimary, guard, pageData, projectShell, useConsoleContext } from "../context.js";
|
|
10
11
|
import { ControlPlaneError } from "../client/errors.js";
|
|
11
12
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
12
|
-
import {
|
|
13
|
+
import { useConsole } from "../ui/console.js";
|
|
14
|
+
import { Page, ServiceSections } from "../ui/shell.js";
|
|
13
15
|
import { useServiceStream } from "../ui/use-stream.js";
|
|
14
16
|
import { lines } from "../stream/log-follow.js";
|
|
15
17
|
export const meta = ({ loaderData }) => [{ title: `Logs of ${loaderData?.service.name ?? "a service"} · ankka` }];
|
|
@@ -20,11 +22,12 @@ export async function loader({ request, params, context }) {
|
|
|
20
22
|
const query = {
|
|
21
23
|
instance: q.get("instance") || undefined,
|
|
22
24
|
previous: q.get("previous") === "true",
|
|
25
|
+
platform: q.get("platform") === "true",
|
|
23
26
|
tail: q.get("tail") ? Number(q.get("tail")) : 200,
|
|
24
27
|
since: q.get("since") ? Number(q.get("since")) : undefined,
|
|
25
28
|
};
|
|
26
29
|
return guard(ctx, async () => {
|
|
27
|
-
const [service, project, page] = await Promise.all([ctx.client.getService(projectId, name), ctx.client.getProject(projectId), pageData(ctx)]);
|
|
30
|
+
const [service, project, services, page] = await Promise.all([ctx.client.getService(projectId, name), ctx.client.getProject(projectId), ctx.client.listServices(projectId), pageData(ctx)]);
|
|
28
31
|
const organization = await ctx.client.getOrganization(project.organizationId);
|
|
29
32
|
let instances = [];
|
|
30
33
|
let none;
|
|
@@ -40,11 +43,12 @@ export async function loader({ request, params, context }) {
|
|
|
40
43
|
}
|
|
41
44
|
// Every instance the service has had output from, for the choice; the current read may be of one.
|
|
42
45
|
const known = query.instance ? [query.instance, ...instances.map((i) => i.instance).filter((i) => i !== query.instance)] : instances.map((i) => i.instance);
|
|
43
|
-
|
|
46
|
+
const shell = projectShell(page, organization, project, services, [{ label: name, to: `projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(name)}` }, { label: "Logs" }], applyPrimary(projectId, name), name);
|
|
47
|
+
return { console: shell, service, project, organization, instances, none, query, known };
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
export default function Logs() {
|
|
47
|
-
const { service, project: p,
|
|
51
|
+
const { service, project: p, instances, none, query, known } = useLoaderData();
|
|
48
52
|
const { mount } = useConsole();
|
|
49
53
|
const [follow, setFollow] = useState(true);
|
|
50
54
|
const initial = useMemo(() => Object.fromEntries(instances.map((i) => [i.instance, { lines: i.lines, error: i.error }])), [instances]);
|
|
@@ -53,7 +57,10 @@ export default function Logs() {
|
|
|
53
57
|
params.set("instance", query.instance);
|
|
54
58
|
if (query.previous)
|
|
55
59
|
params.set("previous", "true");
|
|
60
|
+
if (query.platform)
|
|
61
|
+
params.set("platform", "true");
|
|
56
62
|
params.set("tail", String(query.tail));
|
|
63
|
+
const twoContainers = service.hosting === "process" || service.hosting === "web";
|
|
57
64
|
const { byInstance, state } = useServiceStream(p.id, service.name, service, {
|
|
58
65
|
follow: follow && !query.previous,
|
|
59
66
|
query: params.toString(),
|
|
@@ -61,12 +68,6 @@ export default function Logs() {
|
|
|
61
68
|
initial,
|
|
62
69
|
});
|
|
63
70
|
const shown = Object.entries(byInstance);
|
|
64
|
-
return (_jsxs("section", { className: "ac-
|
|
65
|
-
{ label: "Organizations", to: "" },
|
|
66
|
-
{ label: o.name, to: `organizations/${encodeURIComponent(o.id)}` },
|
|
67
|
-
{ label: p.name, to: `projects/${encodeURIComponent(p.id)}` },
|
|
68
|
-
{ label: service.name, to: `projects/${encodeURIComponent(p.id)}/services/${encodeURIComponent(service.name)}` },
|
|
69
|
-
{ label: "Logs" },
|
|
70
|
-
] }), _jsxs("h1", { children: ["Logs of ", service.name] }), _jsxs(Form, { method: "get", className: "ac-inline", "aria-label": "Which logs", children: [_jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "instance", children: "Instance" }), _jsxs("select", { id: "instance", name: "instance", defaultValue: query.instance ?? "", children: [_jsx("option", { value: "", children: "Every instance" }), known.map((i) => (_jsx("option", { value: i, children: i }, i)))] })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "tail", children: "Last lines" }), _jsx("input", { id: "tail", name: "tail", type: "number", min: 1, max: 5000, defaultValue: query.tail })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "since", children: "Last seconds" }), _jsx("input", { id: "since", name: "since", type: "number", min: 1, defaultValue: query.since, placeholder: "Any" })] }), _jsx("div", { className: "ac-field", children: _jsxs("label", { children: [_jsx("input", { type: "checkbox", name: "previous", value: "true", defaultChecked: query.previous }), " The container before the last restart"] }) }), _jsx("button", { type: "submit", className: "ac-button", children: "Show" })] }), query.previous ? null : (_jsxs("p", { className: "ac-actions", children: [follow ? _jsx("span", { className: state === "live" ? "ac-live" : undefined, children: state === "live" ? "Following new lines" : "Connecting to follow new lines" }) : _jsx("span", { children: "Paused" }), _jsx("button", { type: "button", className: "ac-button ac-button-quiet", onClick: () => setFollow((f) => !f), hidden: state === "off" && !follow ? false : undefined, children: follow ? "Pause following" : "Follow new lines" }), _jsx("noscript", { children: _jsx("a", { href: `${mount}projects/${encodeURIComponent(p.id)}/services/${encodeURIComponent(service.name)}/logs?${params}`, children: "Refresh" }) })] })), none ? _jsxs("p", { className: "ac-empty", children: [none, ". Logs exist only while an instance runs."] }) : null, !none && shown.length === 0 ? _jsx("p", { className: "ac-empty", children: "No output yet." }) : null, shown.map(([instance, log]) => (_jsxs("section", { "aria-labelledby": `log-${instance}`, children: [_jsx("h2", { id: `log-${instance}`, children: instance }), log.error ? _jsx("p", { className: "ac-refusal", children: log.error }) : null, _jsx("pre", { className: "ac-log", tabIndex: 0, "data-instance": instance, children: log.lines.join("\n") })] }, instance)))] }));
|
|
71
|
+
return (_jsxs(Page, { children: [_jsxs("div", { className: "ac-head", children: [_jsxs("h1", { children: ["Logs of ", service.name] }), _jsx(ServiceSections, { projectId: p.id, name: service.name, current: "logs" })] }), _jsxs(Form, { method: "get", className: "ac-card ac-inline", "aria-label": "Which logs", children: [_jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "instance", children: "Instance" }), _jsxs("select", { id: "instance", name: "instance", defaultValue: query.instance ?? "", children: [_jsx("option", { value: "", children: "Every instance" }), known.map((i) => (_jsx("option", { value: i, children: i }, i)))] })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "tail", children: "Last lines" }), _jsx("input", { id: "tail", name: "tail", type: "number", min: 1, max: 5000, defaultValue: query.tail })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "since", children: "Last seconds" }), _jsx("input", { id: "since", name: "since", type: "number", min: 1, defaultValue: query.since, placeholder: "Any" })] }), _jsx("div", { className: "ac-field", children: _jsxs("label", { children: [_jsx("input", { type: "checkbox", name: "previous", value: "true", defaultChecked: query.previous }), " The container before the last restart"] }) }), twoContainers ? (_jsx("div", { className: "ac-field", children: _jsxs("label", { children: [_jsx("input", { type: "checkbox", name: "platform", value: "true", defaultChecked: query.platform }), " ", service.hosting === "web" ? "The platform's proxy instead of your program" : "The platform's sidecar instead of your process"] }) })) : null, _jsx("button", { type: "submit", className: "ac-button", children: "Show" })] }), query.previous ? null : (_jsxs("p", { className: "ac-actions", children: [follow ? _jsx("span", { className: state === "live" ? "ac-live" : undefined, children: state === "live" ? "Following new lines" : "Connecting to follow new lines" }) : _jsx("span", { children: "Paused" }), _jsx("button", { type: "button", className: "ac-button ac-button-quiet", onClick: () => setFollow((f) => !f), hidden: state === "off" && !follow ? false : undefined, children: follow ? "Pause following" : "Follow new lines" }), _jsx("noscript", { children: _jsx("a", { href: `${mount}projects/${encodeURIComponent(p.id)}/services/${encodeURIComponent(service.name)}/logs?${params}`, children: "Refresh" }) })] })), none ? _jsxs("p", { className: "ac-empty", children: [none, ". Logs exist only while an instance runs."] }) : null, !none && shown.length === 0 ? _jsx("p", { className: "ac-empty", children: "No output yet." }) : null, shown.map(([instance, log]) => (_jsxs("section", { className: "ac-card", "aria-labelledby": `log-${instance}`, children: [_jsx("h2", { id: `log-${instance}`, children: instance }), log.error ? _jsx("p", { className: "ac-refusal", children: log.error }) : null, _jsx("pre", { className: "ac-log", tabIndex: 0, "data-instance": instance, children: log.lines.join("\n") })] }, instance)))] }));
|
|
71
72
|
}
|
|
72
73
|
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An organization's machines (feature 040): programs outside the installation that hold a grant —
|
|
3
|
+
* an affiliate network reading a topic, say — each with a client id and a secret it trades for a
|
|
4
|
+
* token. A new machine's secret is shown exactly once, as a deploy token's is: the registration
|
|
5
|
+
* redirects (so a reload can never register twice), carrying the secret across in a sealed cookie
|
|
6
|
+
* that this page reads and clears on its next render.
|
|
7
|
+
*/
|
|
8
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
9
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
10
|
+
export declare const meta: MetaFunction;
|
|
11
|
+
export declare function loader({ request, params, context }: LoaderFunctionArgs): Promise<{
|
|
12
|
+
console: import("../context.ts").ConsolePageData;
|
|
13
|
+
organization: {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
projects: number;
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
role: NonNullable<"member" | "owner"> | undefined;
|
|
19
|
+
quota: {
|
|
20
|
+
projects: number | undefined;
|
|
21
|
+
services: number | undefined;
|
|
22
|
+
instances: number | undefined;
|
|
23
|
+
} | undefined;
|
|
24
|
+
usage: {
|
|
25
|
+
projects: number;
|
|
26
|
+
services: number;
|
|
27
|
+
instances: number;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
machines: {
|
|
31
|
+
name: string;
|
|
32
|
+
clientId: string;
|
|
33
|
+
registeredBy: string | undefined;
|
|
34
|
+
registeredAt: string | undefined;
|
|
35
|
+
byteRates: {
|
|
36
|
+
produceBytesPerSecond: number;
|
|
37
|
+
consumeBytesPerSecond: number;
|
|
38
|
+
requestPercentage: number;
|
|
39
|
+
} | undefined;
|
|
40
|
+
}[];
|
|
41
|
+
grants: {
|
|
42
|
+
id: string;
|
|
43
|
+
grantingProject: string;
|
|
44
|
+
grantingOrganization: string;
|
|
45
|
+
grantee: string;
|
|
46
|
+
target: {
|
|
47
|
+
kind: string;
|
|
48
|
+
service: string | undefined;
|
|
49
|
+
method: string | undefined;
|
|
50
|
+
path: string | undefined;
|
|
51
|
+
topic: string | undefined;
|
|
52
|
+
right: string | undefined;
|
|
53
|
+
decrypt: boolean;
|
|
54
|
+
};
|
|
55
|
+
state: string;
|
|
56
|
+
changes: {
|
|
57
|
+
change: string;
|
|
58
|
+
by: string | undefined;
|
|
59
|
+
at: string | undefined;
|
|
60
|
+
}[];
|
|
61
|
+
topic: {
|
|
62
|
+
partitions: number;
|
|
63
|
+
compacted: boolean;
|
|
64
|
+
retention: string | undefined;
|
|
65
|
+
copies: number | undefined;
|
|
66
|
+
} | undefined;
|
|
67
|
+
}[];
|
|
68
|
+
registered: {
|
|
69
|
+
name: string;
|
|
70
|
+
clientId: string;
|
|
71
|
+
clientSecret: string;
|
|
72
|
+
tokenUrl: string;
|
|
73
|
+
brokerBootstrap: string | undefined;
|
|
74
|
+
} | null;
|
|
75
|
+
}>;
|
|
76
|
+
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
|
|
77
|
+
export default function Machines(): import("react").JSX.Element;
|
|
78
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* An organization's machines (feature 040): programs outside the installation that hold a grant —
|
|
4
|
+
* an affiliate network reading a topic, say — each with a client id and a secret it trades for a
|
|
5
|
+
* token. A new machine's secret is shown exactly once, as a deploy token's is: the registration
|
|
6
|
+
* redirects (so a reload can never register twice), carrying the secret across in a sealed cookie
|
|
7
|
+
* that this page reads and clears on its next render.
|
|
8
|
+
*/
|
|
9
|
+
import { redirect, useLoaderData } from "react-router";
|
|
10
|
+
import { act, guard, organizationShell, pageData, text, useConsoleContext } from "../context.js";
|
|
11
|
+
import { targetText } from "../ui/grants.js";
|
|
12
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
13
|
+
import { ConsoleForm, Field, Submit, useConsole, when } from "../ui/console.js";
|
|
14
|
+
import { Page, SectionTitle } from "../ui/shell.js";
|
|
15
|
+
import { Refused, useRefusal } from "../ui/refused.js";
|
|
16
|
+
import { HostActions } from "../extensions/render.js";
|
|
17
|
+
export const meta = () => [{ title: "Machines · ankka" }];
|
|
18
|
+
export async function loader({ request, params, context }) {
|
|
19
|
+
const ctx = useConsoleContext(context);
|
|
20
|
+
const id = params.organizationId;
|
|
21
|
+
return guard(ctx, async () => {
|
|
22
|
+
const [organization, machines, grants, projects, page] = await Promise.all([
|
|
23
|
+
ctx.client.getOrganization(id),
|
|
24
|
+
ctx.client.machines(id),
|
|
25
|
+
ctx.client.organizationGrants(id),
|
|
26
|
+
ctx.client.listProjects(),
|
|
27
|
+
pageData(ctx),
|
|
28
|
+
]);
|
|
29
|
+
const registered = await ctx.runtime.machineFlash.take(request, ctx.headers);
|
|
30
|
+
// Shown from the one-time cookie alone: the listing is a projection and may not show the new machine yet.
|
|
31
|
+
return {
|
|
32
|
+
console: organizationShell(page, organization, projects, "machines", [{ label: "Machines" }]),
|
|
33
|
+
organization,
|
|
34
|
+
machines,
|
|
35
|
+
grants,
|
|
36
|
+
registered,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function action({ request, params, context }) {
|
|
41
|
+
const ctx = useConsoleContext(context);
|
|
42
|
+
const id = params.organizationId;
|
|
43
|
+
const form = await request.formData();
|
|
44
|
+
const intent = text(form, "intent");
|
|
45
|
+
const self = ctx.href(`organizations/${encodeURIComponent(id)}/machines`);
|
|
46
|
+
return act(ctx, intent, form, async () => {
|
|
47
|
+
if (intent === "register") {
|
|
48
|
+
const registered = await ctx.client.registerMachine(id, text(form, "machineName"));
|
|
49
|
+
await ctx.runtime.machineFlash.write(registered, ctx.headers);
|
|
50
|
+
return redirect(self);
|
|
51
|
+
}
|
|
52
|
+
if (intent === "delete") {
|
|
53
|
+
await ctx.client.deleteMachine(id, text(form, "machineName"));
|
|
54
|
+
return redirect(self);
|
|
55
|
+
}
|
|
56
|
+
if (intent === "byte-rates") {
|
|
57
|
+
await ctx.client.setMachineByteRates(id, text(form, "rateMachine"), {
|
|
58
|
+
produceBytesPerSecond: Number(text(form, "produceBytesPerSecond")),
|
|
59
|
+
consumeBytesPerSecond: Number(text(form, "consumeBytesPerSecond")),
|
|
60
|
+
requestPercentage: Number(text(form, "requestPercentage")),
|
|
61
|
+
});
|
|
62
|
+
return redirect(self);
|
|
63
|
+
}
|
|
64
|
+
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export default function Machines() {
|
|
68
|
+
const { organization: o, machines, grants, registered } = useLoaderData();
|
|
69
|
+
const { shows, shell } = useConsole();
|
|
70
|
+
const manages = shell.organization?.manages ?? false;
|
|
71
|
+
const registerRefusal = useRefusal("register");
|
|
72
|
+
const ratesRefusal = useRefusal("byte-rates");
|
|
73
|
+
const inspector = manages ? (_jsxs(_Fragment, { children: [shows("machine.register") ? (_jsxs("section", { "aria-labelledby": "register-machine", className: "ac-form", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "register-machine", children: "Register a machine" }) }), _jsxs(ConsoleForm, { intent: "register", className: "ac-inline", children: [_jsx(Field, { label: "Machine name", name: "machineName", required: true, placeholder: "affiliate-network", hint: "Lowercase letters, digits and '-'. A grant names it as machine:<organization>/<name>.", defaultValue: registerRefusal?.values.machineName, autoComplete: "off" }), _jsx(Submit, { intent: "register", primary: true, children: "Register machine" })] }), _jsx(Refused, { intent: "register" }), _jsx("div", { className: "ac-ops", children: _jsx(HostActions, { operation: "machine.register", entity: o }) })] })) : null, shows("machine.byte-rates") && machines.length > 0 ? (_jsxs("section", { "aria-labelledby": "byte-rates-title", className: "ac-form", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "byte-rates-title", children: "Byte rates" }) }), _jsxs("details", { className: "ac-more", open: ratesRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Limit a machine on the broker" }), _jsxs(ConsoleForm, { intent: "byte-rates", className: "ac-form", children: [_jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "rateMachine", children: "Machine" }), _jsx("select", { id: "rateMachine", name: "rateMachine", defaultValue: ratesRefusal?.values.rateMachine ?? machines[0]?.name, children: machines.map((m) => (_jsx("option", { value: m.name, children: m.name }, m.name))) })] }), _jsx(Field, { label: "Produce bytes a second", name: "produceBytesPerSecond", type: "number", required: true, min: 1, defaultValue: ratesRefusal?.values.produceBytesPerSecond }), _jsx(Field, { label: "Consume bytes a second", name: "consumeBytesPerSecond", type: "number", required: true, min: 1, defaultValue: ratesRefusal?.values.consumeBytesPerSecond }), _jsx(Field, { label: "Request percentage", name: "requestPercentage", type: "number", required: true, min: 1, max: 100, hint: "The share of a broker thread's time its requests may take, 1 to 100.", defaultValue: ratesRefusal?.values.requestPercentage }), _jsx(Refused, { intent: "byte-rates" }), _jsx("div", { children: _jsx(Submit, { intent: "byte-rates", children: "Set byte rates" }) })] })] })] })) : null] })) : null;
|
|
74
|
+
return (_jsxs(Page, { inspector: inspector, children: [_jsxs("h1", { children: ["Machines of ", o.name] }), _jsx("p", { className: "ac-lede", children: "A machine is a program outside the installation that another project can grant a route, a method or a topic. It signs in with its client id and secret; owners register, limit and delete machines, and every member can see them." }), registered ? (_jsxs("div", { className: "ac-notice", role: "status", children: [_jsxs("p", { children: [_jsxs("strong", { children: ["Copy the client secret for ", registered.name, " now."] }), " It is shown this once and cannot be retrieved later."] }), _jsxs("dl", { className: "ac-facts", children: [_jsx("dt", { children: "Client id" }), _jsx("dd", { children: _jsx("code", { children: registered.clientId }) }), _jsx("dt", { children: "Token address" }), _jsx("dd", { children: _jsx("code", { children: registered.tokenUrl }) }), registered.brokerBootstrap ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Broker" }), _jsx("dd", { children: _jsx("code", { children: registered.brokerBootstrap }) })] })) : null] }), _jsx("code", { className: "ac-secret", "data-secret": true, children: registered.clientSecret })] })) : null, _jsxs("section", { className: "ac-card", "aria-label": "Machines", children: [machines.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No machines registered." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Machines" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Client id" }), _jsx("th", { scope: "col", children: "Registered" }), _jsx("th", { scope: "col", children: "Byte rates" }), _jsx("th", { scope: "col", children: _jsx("span", { className: "ac-visually-hidden", children: "Actions" }) })] }) }), _jsx("tbody", { children: machines.map((m) => (_jsxs("tr", { "data-machine": m.name, children: [_jsx("td", { children: m.name }), _jsx("td", { children: _jsx("code", { children: m.clientId }) }), _jsxs("td", { children: [m.registeredAt ? when(m.registeredAt) : "—", m.registeredBy ? ` by ${m.registeredBy}` : ""] }), _jsx("td", { "data-byte-rates": true, children: m.byteRates
|
|
75
|
+
? `produce ${m.byteRates.produceBytesPerSecond} B/s, consume ${m.byteRates.consumeBytesPerSecond} B/s, ${m.byteRates.requestPercentage}% of requests`
|
|
76
|
+
: "The installation's default" }), _jsx("td", { children: manages && shows("machine.delete") ? (_jsxs(ConsoleForm, { intent: "delete", children: [_jsx("input", { type: "hidden", name: "machineName", value: m.name }), _jsx(Submit, { intent: "delete", className: "ac-button ac-button-small ac-button-danger", label: `Delete ${m.name}`, children: "Delete" })] })) : null })] }, m.name))) })] }) })), _jsx(Refused, { intent: "delete" })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "held", children: [_jsx("h2", { id: "held", children: "Grants held" }), grants.length === 0 ? (_jsx("p", { className: "ac-empty", children: "Nothing is granted to this organization's machines or services by another project." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "held", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "From" }), _jsx("th", { scope: "col", children: "Grantee" }), _jsx("th", { scope: "col", children: "Target" }), _jsx("th", { scope: "col", children: "State" })] }) }), _jsx("tbody", { children: grants.map((g) => (_jsxs("tr", { "data-held": g.id, children: [_jsxs("td", { children: [g.grantingProject, " ", _jsxs("span", { className: "ac-hint", children: ["of ", g.grantingOrganization] })] }), _jsx("td", { children: _jsx("code", { children: g.grantee }) }), _jsx("td", { children: targetText(g.target) }), _jsx("td", { children: g.state })] }, g.id))) })] }) }))] })] }));
|
|
77
|
+
}
|
|
78
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|
package/dist/routes/members.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* An organization's members and the invitations waiting to be claimed. Everyone in it sees the
|
|
4
4
|
* list; only owners see the controls. A machine — a deploy token — is a member like anyone else,
|
|
5
5
|
* and is shown as the machine it is.
|
|
6
6
|
*/
|
|
7
7
|
import { redirect, useLoaderData } from "react-router";
|
|
8
|
-
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
8
|
+
import { act, guard, organizationShell, pageData, text, useConsoleContext } from "../context.js";
|
|
9
9
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
10
|
-
import {
|
|
10
|
+
import { ConsoleForm, Field, Submit, useConsole, when } from "../ui/console.js";
|
|
11
|
+
import { Page, SectionTitle } from "../ui/shell.js";
|
|
12
|
+
import { Select } from "../ui/primitives/select.js";
|
|
11
13
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
12
14
|
import { HostActions } from "../extensions/render.js";
|
|
13
15
|
export const meta = () => [{ title: "Members · ankka" }];
|
|
@@ -15,8 +17,8 @@ export async function loader({ params, context }) {
|
|
|
15
17
|
const ctx = useConsoleContext(context);
|
|
16
18
|
const id = params.organizationId;
|
|
17
19
|
return guard(ctx, async () => {
|
|
18
|
-
const [organization, members, page] = await Promise.all([ctx.client.getOrganization(id), ctx.client.members(id), pageData(ctx)]);
|
|
19
|
-
return { console: page, organization, ...members };
|
|
20
|
+
const [organization, members, projects, page] = await Promise.all([ctx.client.getOrganization(id), ctx.client.members(id), ctx.client.listProjects(), pageData(ctx)]);
|
|
21
|
+
return { console: organizationShell(page, organization, projects, "members", [{ label: "Members" }]), organization, ...members };
|
|
20
22
|
});
|
|
21
23
|
}
|
|
22
24
|
const role = (form) => (text(form, "role") === "owner" ? "owner" : "member");
|
|
@@ -51,9 +53,10 @@ export default function Members() {
|
|
|
51
53
|
const { shows } = useConsole();
|
|
52
54
|
const owner = o.role === "owner" || (page.principal?.platformAdmin ?? false);
|
|
53
55
|
const inviteRefusal = useRefusal("invite");
|
|
54
|
-
|
|
56
|
+
const inspector = owner && shows("member.invite") ? (_jsxs("section", { "aria-labelledby": "invite", className: "ac-form", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "invite", children: "Invite someone" }) }), _jsx("p", { className: "ac-hint", children: "They become a member the first time they sign in with this email address, once the identity provider has verified it." }), _jsxs(ConsoleForm, { intent: "invite", className: "ac-inline", children: [_jsx(Field, { label: "Email", name: "email", type: "email", required: true, autoComplete: "off", defaultValue: inviteRefusal?.values.email }), _jsxs(Select, { label: "Role", id: "invite-role", name: "role", defaultValue: inviteRefusal?.values.role ?? "member", children: [_jsx("option", { value: "member", children: "Member" }), _jsx("option", { value: "owner", children: "Owner" })] }), _jsx(Submit, { intent: "invite", primary: true, children: "Invite" })] }), _jsx(Refused, { intent: "invite" }), _jsx("div", { className: "ac-ops", children: _jsx(HostActions, { operation: "member.invite", entity: o }) })] })) : null;
|
|
57
|
+
return (_jsxs(Page, { inspector: inspector, children: [_jsxs("h1", { children: ["Members of ", o.name] }), _jsx("p", { className: "ac-lede", children: "Owners manage members and deploy tokens; every member can deploy." }), _jsx("div", { className: "ac-card ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Members" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Who" }), _jsx("th", { scope: "col", children: "Role" }), _jsx("th", { scope: "col", children: "Since" }), _jsx("th", { scope: "col", children: "Added by" }), owner ? _jsx("th", { scope: "col", children: "Change" }) : null] }) }), _jsx("tbody", { children: members.map((m) => {
|
|
55
58
|
const machine = m.subject.startsWith("token:");
|
|
56
|
-
return (_jsxs("tr", { "data-member": m.subject, children: [_jsxs("td", { children: [machine ? "Deploy token " : null, _jsx("strong", { children: m.display ?? m.email ?? m.subject }), m.email && m.display ? _jsx("div", { className: "ac-hint", children: m.email }) : null, _jsx("div", { className: "ac-hint", children: m.subject })] }), _jsx("td", { children: roleWord(m.role) }), _jsx("td", { children: when(m.since) }), _jsx("td", { children: m.addedBy ?? "—" }), owner ? (_jsx("td", { children: machine ? (_jsx("span", { className: "ac-hint", children: "Revoke it on the deploy tokens page" })) : (_jsxs("div", { className: "ac-actions", children: [shows("member.role") ? (_jsxs(ConsoleForm, { intent: "role", children: [_jsx("input", { type: "hidden", name: "subject", value: m.subject }), _jsx("input", { type: "hidden", name: "role", value: m.role === "owner" ? "member" : "owner" }), _jsx(Submit, { intent: "role", children: m.role === "owner" ? "Make member" : "Make owner" })] })) : null, shows("member.remove") ? (_jsxs(ConsoleForm, { intent: "remove", children: [_jsx("input", { type: "hidden", name: "subject", value: m.subject }), _jsx(Submit, { intent: "remove",
|
|
57
|
-
}) })] }) }), _jsx(Refused, { intent: "role" }), _jsx(Refused, { intent: "remove" }), _jsx("h2", { children: "Invitations" }), invitations.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No invitations are waiting." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Invitations waiting to be claimed" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Email" }), _jsx("th", { scope: "col", children: "Role" }), _jsx("th", { scope: "col", children: "Invited" }), owner ? _jsx("th", { scope: "col", children: "Change" }) : null] }) }), _jsx("tbody", { children: invitations.map((i) => (_jsxs("tr", { "data-invitation": i.email, children: [_jsx("td", { children: i.email }), _jsx("td", { children: roleWord(i.role) }), _jsxs("td", { children: [when(i.invitedAt), i.invitedBy ? ` by ${i.invitedBy}` : ""] }), owner ? (_jsx("td", { children: shows("invitation.withdraw") ? (_jsxs(ConsoleForm, { intent: "withdraw", children: [_jsx("input", { type: "hidden", name: "email", value: i.email }), _jsx(Submit, { intent: "withdraw", children: "Withdraw" })] })) : null })) : null] }, i.email))) })] }) })), _jsx(Refused, { intent: "withdraw" })
|
|
59
|
+
return (_jsxs("tr", { "data-member": m.subject, children: [_jsxs("td", { children: [machine ? "Deploy token " : null, _jsx("strong", { children: m.display ?? m.email ?? m.subject }), m.email && m.display ? _jsx("div", { className: "ac-hint", children: m.email }) : null, _jsx("div", { className: "ac-hint", children: m.subject })] }), _jsx("td", { children: roleWord(m.role) }), _jsx("td", { children: when(m.since) }), _jsx("td", { children: m.addedBy ?? "—" }), owner ? (_jsx("td", { children: machine ? (_jsx("span", { className: "ac-hint", children: "Revoke it on the deploy tokens page" })) : (_jsxs("div", { className: "ac-actions", children: [shows("member.role") ? (_jsxs(ConsoleForm, { intent: "role", children: [_jsx("input", { type: "hidden", name: "subject", value: m.subject }), _jsx("input", { type: "hidden", name: "role", value: m.role === "owner" ? "member" : "owner" }), _jsx(Submit, { intent: "role", className: "ac-button ac-button-small", children: m.role === "owner" ? "Make member" : "Make owner" })] })) : null, shows("member.remove") ? (_jsxs(ConsoleForm, { intent: "remove", children: [_jsx("input", { type: "hidden", name: "subject", value: m.subject }), _jsx(Submit, { intent: "remove", className: "ac-button ac-button-small ac-button-danger", children: "Remove" })] })) : null] })) })) : null] }, m.subject));
|
|
60
|
+
}) })] }) }), _jsx(Refused, { intent: "role" }), _jsx(Refused, { intent: "remove" }), _jsxs("section", { className: "ac-card", "aria-labelledby": "invitations", children: [_jsx("h2", { id: "invitations", children: "Invitations" }), invitations.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No invitations are waiting." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Invitations waiting to be claimed" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Email" }), _jsx("th", { scope: "col", children: "Role" }), _jsx("th", { scope: "col", children: "Invited" }), owner ? _jsx("th", { scope: "col", children: "Change" }) : null] }) }), _jsx("tbody", { children: invitations.map((i) => (_jsxs("tr", { "data-invitation": i.email, children: [_jsx("td", { children: i.email }), _jsx("td", { children: roleWord(i.role) }), _jsxs("td", { children: [when(i.invitedAt), i.invitedBy ? ` by ${i.invitedBy}` : ""] }), owner ? (_jsx("td", { children: shows("invitation.withdraw") ? (_jsxs(ConsoleForm, { intent: "withdraw", children: [_jsx("input", { type: "hidden", name: "email", value: i.email }), _jsx(Submit, { intent: "withdraw", className: "ac-button ac-button-small", children: "Withdraw" })] })) : null })) : null] }, i.email))) })] }) })), _jsx(Refused, { intent: "withdraw" })] })] }));
|
|
58
61
|
}
|
|
59
62
|
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -5,14 +5,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* is a projection and may not show it yet.
|
|
6
6
|
*/
|
|
7
7
|
import { redirect } from "react-router";
|
|
8
|
-
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
8
|
+
import { act, guard, pageData, text, useConsoleContext, withShell } from "../context.js";
|
|
9
9
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
10
|
-
import {
|
|
10
|
+
import { ConsoleForm, Field, Submit } from "../ui/console.js";
|
|
11
|
+
import { Page } from "../ui/shell.js";
|
|
11
12
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
12
13
|
export const meta = () => [{ title: "Create an organization · ankka" }];
|
|
13
14
|
export async function loader({ context }) {
|
|
14
15
|
const ctx = useConsoleContext(context);
|
|
15
|
-
return guard(ctx, async () => ({
|
|
16
|
+
return guard(ctx, async () => ({
|
|
17
|
+
console: withShell(await pageData(ctx), { area: "organizations", crumbs: [{ label: "Organizations", to: "" }, { label: "New organization" }] }),
|
|
18
|
+
}));
|
|
16
19
|
}
|
|
17
20
|
export async function action({ request, context }) {
|
|
18
21
|
const ctx = useConsoleContext(context);
|
|
@@ -25,6 +28,6 @@ export async function action({ request, context }) {
|
|
|
25
28
|
}
|
|
26
29
|
export default function NewOrganization() {
|
|
27
30
|
const refusal = useRefusal("create");
|
|
28
|
-
return (_jsxs(
|
|
31
|
+
return (_jsxs(Page, { children: [_jsx("h1", { children: "Create an organization" }), _jsx("p", { className: "ac-lede", children: "You become its first owner. Invite others once it exists." }), _jsxs(ConsoleForm, { to: "organizations/new", intent: "create", className: "ac-card ac-form", children: [_jsx(Field, { label: "Id", name: "id", required: true, pattern: "[a-z]([\\-a-z0-9]{0,61}[a-z0-9])?", hint: "Lowercase letters, digits and hyphens, starting with a letter. It cannot be changed, and once deleted it cannot be used again.", defaultValue: refusal?.values.id, autoComplete: "off" }), _jsx(Field, { label: "Name", name: "name", required: true, hint: "What people see. You can rename it later.", defaultValue: refusal?.values.name }), _jsx(Refused, { intent: "create" }), _jsx("div", { children: _jsx(Submit, { intent: "create", primary: true, children: "Create organization" }) })] })] }));
|
|
29
32
|
}
|
|
30
33
|
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* An organization: its projects,
|
|
2
|
+
* An organization: its projects, the grants other organizations' projects offer it (feature 040), and
|
|
3
|
+
* what its owners and the platform administrator may do to it.
|
|
3
4
|
*/
|
|
4
5
|
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
5
6
|
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
@@ -35,6 +36,33 @@ export declare function loader({ params, context }: LoaderFunctionArgs): Promise
|
|
|
35
36
|
setBy: string | undefined;
|
|
36
37
|
} | undefined;
|
|
37
38
|
}[];
|
|
39
|
+
offered: {
|
|
40
|
+
id: string;
|
|
41
|
+
grantingProject: string;
|
|
42
|
+
grantingOrganization: string;
|
|
43
|
+
grantee: string;
|
|
44
|
+
target: {
|
|
45
|
+
kind: string;
|
|
46
|
+
service: string | undefined;
|
|
47
|
+
method: string | undefined;
|
|
48
|
+
path: string | undefined;
|
|
49
|
+
topic: string | undefined;
|
|
50
|
+
right: string | undefined;
|
|
51
|
+
decrypt: boolean;
|
|
52
|
+
};
|
|
53
|
+
state: string;
|
|
54
|
+
changes: {
|
|
55
|
+
change: string;
|
|
56
|
+
by: string | undefined;
|
|
57
|
+
at: string | undefined;
|
|
58
|
+
}[];
|
|
59
|
+
topic: {
|
|
60
|
+
partitions: number;
|
|
61
|
+
compacted: boolean;
|
|
62
|
+
retention: string | undefined;
|
|
63
|
+
copies: number | undefined;
|
|
64
|
+
} | undefined;
|
|
65
|
+
}[];
|
|
38
66
|
ownerless: boolean;
|
|
39
67
|
panels: import("../extensions/render.tsx").LoadedPanels;
|
|
40
68
|
}>;
|
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* An organization: its projects,
|
|
3
|
+
* An organization: its projects, the grants other organizations' projects offer it (feature 040), and
|
|
4
|
+
* what its owners and the platform administrator may do to it.
|
|
4
5
|
*/
|
|
5
6
|
import { redirect, useLoaderData } from "react-router";
|
|
6
|
-
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
7
|
+
import { act, guard, organizationShell, pageData, text, useConsoleContext } from "../context.js";
|
|
7
8
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
8
|
-
import {
|
|
9
|
+
import { ConsoleForm, ConsoleLink, Field, Submit, useConsole, when } from "../ui/console.js";
|
|
10
|
+
import { Page } from "../ui/shell.js";
|
|
9
11
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
10
12
|
import { HostActions, loadPanels, Panels } from "../extensions/render.js";
|
|
13
|
+
import { targetText } from "../ui/grants.js";
|
|
11
14
|
export const meta = ({ loaderData }) => [{ title: `${loaderData?.organization.name ?? "Organization"} · ankka` }];
|
|
12
15
|
export async function loader({ params, context }) {
|
|
13
16
|
const ctx = useConsoleContext(context);
|
|
14
17
|
const id = params.organizationId;
|
|
15
18
|
return guard(ctx, async () => {
|
|
16
|
-
const [organization, projects, page] = await Promise.all([
|
|
19
|
+
const [organization, projects, offered, page] = await Promise.all([
|
|
20
|
+
ctx.client.getOrganization(id),
|
|
21
|
+
ctx.client.listProjects(),
|
|
22
|
+
ctx.client.organizationGrants(id),
|
|
23
|
+
pageData(ctx),
|
|
24
|
+
]);
|
|
17
25
|
// An organization whose owners have all left can be given one by an administrator.
|
|
18
26
|
const ownerless = page.principal?.platformAdmin ? !(await ctx.client.members(id)).members.some((m) => m.role === "owner") : false;
|
|
27
|
+
const own = projects.filter((p) => p.organizationId === id);
|
|
19
28
|
return {
|
|
20
|
-
console: page,
|
|
29
|
+
console: organizationShell(page, organization, own, "organizations", [], {
|
|
30
|
+
label: "Create a project",
|
|
31
|
+
to: `organizations/${encodeURIComponent(id)}/projects/new`,
|
|
32
|
+
operation: "project.create",
|
|
33
|
+
}),
|
|
21
34
|
organization,
|
|
22
|
-
projects:
|
|
35
|
+
projects: own,
|
|
36
|
+
offered,
|
|
23
37
|
ownerless,
|
|
24
38
|
panels: await loadPanels(ctx, "organization", organization),
|
|
25
39
|
};
|
|
@@ -60,14 +74,25 @@ export async function action({ request, params, context }) {
|
|
|
60
74
|
case "repair":
|
|
61
75
|
await ctx.client.repair(id, text(form, "subject"), "owner");
|
|
62
76
|
return redirect(self);
|
|
77
|
+
case "grant-accept":
|
|
78
|
+
await ctx.client.answerGrant(id, text(form, "grantId"), "accept");
|
|
79
|
+
return redirect(self);
|
|
80
|
+
case "grant-decline":
|
|
81
|
+
await ctx.client.answerGrant(id, text(form, "grantId"), "decline");
|
|
82
|
+
return redirect(self);
|
|
83
|
+
case "grant-relinquish":
|
|
84
|
+
await ctx.client.answerGrant(id, text(form, "grantId"), "relinquish");
|
|
85
|
+
return redirect(self);
|
|
63
86
|
default:
|
|
64
87
|
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
65
88
|
}
|
|
66
89
|
});
|
|
67
90
|
}
|
|
68
91
|
const limit = (n, what) => (n === undefined ? `any number of ${what}` : `${n} ${what}`);
|
|
92
|
+
/** A grant in one phrase for a control's accessible name: what it opens, to whom, from where. */
|
|
93
|
+
const grantName = (g) => `${targetText(g.target)} to ${g.grantee} from ${g.grantingProject}`;
|
|
69
94
|
export default function Organization() {
|
|
70
|
-
const { organization: o, projects, ownerless, panels, console: page } = useLoaderData();
|
|
95
|
+
const { organization: o, projects, offered, ownerless, panels, console: page } = useLoaderData();
|
|
71
96
|
const { shows } = useConsole();
|
|
72
97
|
const admin = page.principal?.platformAdmin ?? false;
|
|
73
98
|
const owner = o.role === "owner" || admin;
|
|
@@ -75,8 +100,12 @@ export default function Organization() {
|
|
|
75
100
|
const deleteRefusal = useRefusal("delete");
|
|
76
101
|
const adminRefused = [useRefusal("disable"), useRefusal("enable"), useRefusal("quota-set"), useRefusal("quota-clear"), useRefusal("repair")].some(Boolean);
|
|
77
102
|
const path = `organizations/${encodeURIComponent(o.id)}`;
|
|
78
|
-
|
|
103
|
+
const inspector = (_jsxs(_Fragment, { children: [_jsx(HostActions, { operation: "project.create", entity: o }), admin ? (_jsxs("details", { className: "ac-more", open: adminRefused || undefined, children: [_jsx("summary", { children: "Platform administration" }), _jsx("div", { className: "ac-ops", children: o.disabled
|
|
79
104
|
? shows("organization.enable") && (_jsx(ConsoleForm, { intent: "enable", children: _jsx(Submit, { intent: "enable", children: "Enable organization" }) }))
|
|
80
|
-
: shows("organization.disable") && (_jsx(ConsoleForm, { intent: "disable", children: _jsx(Submit, { intent: "disable", danger: true, children: "Disable organization" }) })) }), _jsx(Refused, { intent: "disable" }), _jsx(Refused, { intent: "enable" }), shows("organization.quota.set") ? (_jsxs(ConsoleForm, { intent: "quota-set", className: "ac-inline", "aria-label": "Quota", children: [_jsx(Field, { label: "Projects", name: "projects", type: "number", min: 0, defaultValue: o.quota?.projects, hint: "Empty for no limit" }), _jsx(Field, { label: "Services", name: "services", type: "number", min: 0, defaultValue: o.quota?.services }), _jsx(Field, { label: "Instances", name: "instances", type: "number", min: 0, defaultValue: o.quota?.instances }), _jsx(Submit, { intent: "quota-set", children: "Set quota" })] })) : null, _jsx(Refused, { intent: "quota-set" }), o.quota && shows("organization.quota.clear") ? (_jsx(ConsoleForm, { intent: "quota-clear", children: _jsx(Submit, { intent: "quota-clear", children: "Clear quota" }) })) : null, ownerless && shows("member.repair") ? (_jsxs(ConsoleForm, { intent: "repair", className: "ac-inline", children: [_jsx(Field, { label: "Subject to make owner", name: "subject", required: true, hint: "This organization has no owner. Name the subject of the person to make one." }), _jsx(Submit, { intent: "repair", children: "Add owner" })] })) : null, _jsx(Refused, { intent: "repair" })] })) : null] }));
|
|
105
|
+
: shows("organization.disable") && (_jsx(ConsoleForm, { intent: "disable", children: _jsx(Submit, { intent: "disable", danger: true, children: "Disable organization" }) })) }), _jsx(Refused, { intent: "disable" }), _jsx(Refused, { intent: "enable" }), shows("organization.quota.set") ? (_jsxs(ConsoleForm, { intent: "quota-set", className: "ac-inline", "aria-label": "Quota", children: [_jsx(Field, { label: "Projects", name: "projects", type: "number", min: 0, defaultValue: o.quota?.projects, hint: "Empty for no limit" }), _jsx(Field, { label: "Services", name: "services", type: "number", min: 0, defaultValue: o.quota?.services }), _jsx(Field, { label: "Instances", name: "instances", type: "number", min: 0, defaultValue: o.quota?.instances }), _jsx(Submit, { intent: "quota-set", children: "Set quota" })] })) : null, _jsx(Refused, { intent: "quota-set" }), o.quota && shows("organization.quota.clear") ? (_jsx(ConsoleForm, { intent: "quota-clear", children: _jsx(Submit, { intent: "quota-clear", children: "Clear quota" }) })) : null, ownerless && shows("member.repair") ? (_jsxs(ConsoleForm, { intent: "repair", className: "ac-inline", children: [_jsx(Field, { label: "Subject to make owner", name: "subject", required: true, hint: "This organization has no owner. Name the subject of the person to make one." }), _jsx(Submit, { intent: "repair", children: "Add owner" })] })) : null, _jsx(Refused, { intent: "repair" })] })) : null, owner && (shows("organization.rename") || shows("organization.delete")) ? (_jsxs("details", { className: "ac-more", open: renameRefusal !== undefined || deleteRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Rename or delete" }), shows("organization.rename") ? (_jsxs(ConsoleForm, { intent: "rename", className: "ac-inline", children: [_jsx(Field, { label: "New name", name: "name", required: true, defaultValue: renameRefusal?.values.name ?? o.name }), _jsx(Submit, { intent: "rename", children: "Rename" })] })) : null, _jsx(Refused, { intent: "rename" }), shows("organization.delete") ? (_jsxs("div", { className: "ac-danger-zone", children: [_jsxs("p", { children: ["Deleting ", o.name, " is permanent, and its id can never be used again. It must have no projects."] }), _jsx(ConsoleForm, { intent: "delete", children: _jsx(Submit, { intent: "delete", danger: true, children: "Delete organization" }) }), _jsx(Refused, { intent: "delete" })] })) : null, _jsx(HostActions, { operation: "organization.delete", entity: o })] })) : null] }));
|
|
106
|
+
return (_jsxs(Page, { inspector: inspector, children: [_jsx("h1", { children: o.name }), _jsxs("dl", { className: "ac-card ac-facts", children: [_jsx("dt", { children: "Id" }), _jsx("dd", { children: o.id }), _jsx("dt", { children: "Your role" }), _jsx("dd", { children: o.role === "owner" ? "Owner" : o.role === "member" ? "Member" : "Administrator (not a member)" }), _jsx("dt", { children: "State" }), _jsx("dd", { children: o.disabled ? "Disabled: its services are suspended and every change is refused" : "Active" }), _jsx("dt", { children: "Holds" }), _jsxs("dd", { children: [o.usage.projects, " projects, ", o.usage.services, " services, ", o.usage.instances, " instances"] }), o.quota ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Quota" }), _jsxs("dd", { children: [limit(o.quota.projects, "projects"), ", ", limit(o.quota.services, "services"), ", ", limit(o.quota.instances, "instances")] })] })) : null] }), _jsxs("section", { className: "ac-card", children: [_jsx("h2", { children: "Projects" }), projects.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No projects yet. A project groups services and gives them a namespace of their own." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsxs("caption", { className: "ac-visually-hidden", children: ["Projects in ", o.name] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Id" }), _jsx("th", { scope: "col", className: "ac-num", children: "Services" }), _jsx("th", { scope: "col", children: "Registry" })] }) }), _jsx("tbody", { children: projects.map((p) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(ConsoleLink, { to: `projects/${encodeURIComponent(p.id)}`, children: p.name }) }), _jsx("td", { children: p.id }), _jsx("td", { className: "ac-num", children: p.services }), _jsx("td", { children: p.registry ? p.registry.server : "Public images only" })] }, p.id))) })] }) }))] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "offered", children: [_jsx("h2", { id: "offered", children: "Offered grants" }), offered.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No other organization's project offers this organization's machines or services anything. A grant from another organization waits here until an owner accepts it." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "offered", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "From" }), _jsx("th", { scope: "col", children: "Grantee" }), _jsx("th", { scope: "col", children: "Target" }), _jsx("th", { scope: "col", children: "State" }), _jsx("th", { scope: "col", children: "Last change" }), _jsx("th", { scope: "col", children: _jsx("span", { className: "ac-visually-hidden", children: "Actions" }) })] }) }), _jsx("tbody", { children: offered.map((g) => {
|
|
107
|
+
const last = g.changes[g.changes.length - 1];
|
|
108
|
+
return (_jsxs("tr", { "data-offered": g.id, "data-state": g.state, children: [_jsxs("td", { children: [g.grantingProject, " ", _jsxs("span", { className: "ac-hint", children: ["of ", g.grantingOrganization] })] }), _jsx("td", { children: _jsx("code", { children: g.grantee }) }), _jsx("td", { children: targetText(g.target) }), _jsx("td", { children: g.state }), _jsx("td", { children: last ? (_jsxs(_Fragment, { children: [last.change, last.at ? ` ${when(last.at)}` : "", last.by ? ` by ${last.by}` : ""] })) : ("—") }), _jsxs("td", { children: [owner && g.state === "pending" && shows("grant.answer") ? (_jsxs("div", { className: "ac-ops", children: [_jsxs(ConsoleForm, { intent: "grant-accept", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "grantId", value: g.id }), _jsx(Submit, { intent: "grant-accept", label: `Accept ${grantName(g)}`, children: "Accept" })] }), _jsxs(ConsoleForm, { intent: "grant-decline", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "grantId", value: g.id }), _jsx(Submit, { intent: "grant-decline", label: `Decline ${grantName(g)}`, children: "Decline" })] })] })) : null, owner && g.state === "accepted" && shows("grant.relinquish") ? (_jsxs(ConsoleForm, { intent: "grant-relinquish", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "grantId", value: g.id }), _jsx(Submit, { intent: "grant-relinquish", danger: true, label: `Relinquish ${grantName(g)}`, children: "Relinquish" })] })) : null] })] }, g.id));
|
|
109
|
+
}) })] }) })), _jsx(Refused, { intent: "grant-accept" }), _jsx(Refused, { intent: "grant-decline" }), _jsx(Refused, { intent: "grant-relinquish" })] }), _jsx(Panels, { kind: "organization", entity: o, loaded: panels })] }));
|
|
81
110
|
}
|
|
82
111
|
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Creates a project in an organization, then goes to it, read by id. */
|
|
3
3
|
import { redirect, useLoaderData } from "react-router";
|
|
4
|
-
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
4
|
+
import { act, guard, organizationShell, pageData, text, useConsoleContext } from "../context.js";
|
|
5
5
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ConsoleForm, Field, Submit } from "../ui/console.js";
|
|
7
|
+
import { Page } from "../ui/shell.js";
|
|
7
8
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
8
9
|
export const meta = () => [{ title: "Create a project · ankka" }];
|
|
9
10
|
export async function loader({ params, context }) {
|
|
10
11
|
const ctx = useConsoleContext(context);
|
|
11
|
-
return guard(ctx, async () =>
|
|
12
|
+
return guard(ctx, async () => {
|
|
13
|
+
const [organization, projects, page] = await Promise.all([ctx.client.getOrganization(params.organizationId), ctx.client.listProjects(), pageData(ctx)]);
|
|
14
|
+
return { console: organizationShell(page, organization, projects, "projects", [{ label: "New project" }]), organization };
|
|
15
|
+
});
|
|
12
16
|
}
|
|
13
17
|
export async function action({ request, params, context }) {
|
|
14
18
|
const ctx = useConsoleContext(context);
|
|
@@ -22,6 +26,6 @@ export async function action({ request, params, context }) {
|
|
|
22
26
|
export default function NewProject() {
|
|
23
27
|
const { organization: o } = useLoaderData();
|
|
24
28
|
const refusal = useRefusal("create");
|
|
25
|
-
return (_jsxs(
|
|
29
|
+
return (_jsxs(Page, { children: [_jsxs("h1", { children: ["Create a project in ", o.name] }), _jsx("p", { className: "ac-lede", children: "A project gets a namespace of its own; each of its services gets its own database." }), _jsxs(ConsoleForm, { intent: "create", className: "ac-card ac-form", children: [_jsx(Field, { label: "Id", name: "id", required: true, pattern: "[a-z]([\\-a-z0-9]{0,61}[a-z0-9])?", hint: "Lowercase letters, digits and hyphens, starting with a letter. Part of every exposed service's address, and never reusable once deleted.", defaultValue: refusal?.values.id, autoComplete: "off" }), _jsx(Field, { label: "Name", name: "name", required: true, defaultValue: refusal?.values.name }), _jsx(Refused, { intent: "create" }), _jsx("div", { children: _jsx(Submit, { intent: "create", primary: true, children: "Create project" }) })] })] }));
|
|
26
30
|
}
|
|
27
31
|
export const ErrorBoundary = ConsoleErrorBoundary;
|