ankka-console 0.10.0 → 0.11.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 +25 -1
- package/dist/client/control-plane.js +60 -1
- package/dist/client/schemas.d.ts +525 -0
- package/dist/client/schemas.js +174 -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 +7 -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.js +12 -6
- package/dist/routes/logs.d.ts +35 -0
- package/dist/routes/logs.js +15 -14
- package/dist/routes/members.js +11 -8
- package/dist/routes/organization-new.js +7 -4
- package/dist/routes/organization.js +13 -6
- package/dist/routes/project-new.js +8 -4
- package/dist/routes/project.d.ts +73 -2
- package/dist/routes/project.js +81 -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 +30 -0
- package/dist/routes/service-history.js +79 -0
- package/dist/routes/service-topology.d.ts +153 -0
- package/dist/routes/service-topology.js +54 -0
- package/dist/routes/service.d.ts +34 -10
- package/dist/routes/service.js +27 -22
- package/dist/routes/stream.service.js +18 -1
- package/dist/routes/tokens.js +8 -6
- package/dist/routes.js +2 -0
- package/dist/styles.css +2 -481
- package/dist/testing/fake-control-plane.d.ts +155 -0
- package/dist/testing/fake-control-plane.js +379 -12
- 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/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 +107 -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 +137 -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;
|
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;
|
|
@@ -3,9 +3,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
* An organization: its projects, and what its owners and the platform administrator may do to it.
|
|
4
4
|
*/
|
|
5
5
|
import { redirect, useLoaderData } from "react-router";
|
|
6
|
-
import { act, guard, pageData, text, useConsoleContext } from "../context.js";
|
|
6
|
+
import { act, guard, organizationShell, pageData, text, useConsoleContext } from "../context.js";
|
|
7
7
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
8
|
-
import {
|
|
8
|
+
import { ConsoleForm, ConsoleLink, Field, Submit, useConsole } from "../ui/console.js";
|
|
9
|
+
import { Page } from "../ui/shell.js";
|
|
9
10
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
10
11
|
import { HostActions, loadPanels, Panels } from "../extensions/render.js";
|
|
11
12
|
export const meta = ({ loaderData }) => [{ title: `${loaderData?.organization.name ?? "Organization"} · ankka` }];
|
|
@@ -16,10 +17,15 @@ export async function loader({ params, context }) {
|
|
|
16
17
|
const [organization, projects, page] = await Promise.all([ctx.client.getOrganization(id), ctx.client.listProjects(), pageData(ctx)]);
|
|
17
18
|
// An organization whose owners have all left can be given one by an administrator.
|
|
18
19
|
const ownerless = page.principal?.platformAdmin ? !(await ctx.client.members(id)).members.some((m) => m.role === "owner") : false;
|
|
20
|
+
const own = projects.filter((p) => p.organizationId === id);
|
|
19
21
|
return {
|
|
20
|
-
console: page,
|
|
22
|
+
console: organizationShell(page, organization, own, "organizations", [], {
|
|
23
|
+
label: "Create a project",
|
|
24
|
+
to: `organizations/${encodeURIComponent(id)}/projects/new`,
|
|
25
|
+
operation: "project.create",
|
|
26
|
+
}),
|
|
21
27
|
organization,
|
|
22
|
-
projects:
|
|
28
|
+
projects: own,
|
|
23
29
|
ownerless,
|
|
24
30
|
panels: await loadPanels(ctx, "organization", organization),
|
|
25
31
|
};
|
|
@@ -75,8 +81,9 @@ export default function Organization() {
|
|
|
75
81
|
const deleteRefusal = useRefusal("delete");
|
|
76
82
|
const adminRefused = [useRefusal("disable"), useRefusal("enable"), useRefusal("quota-set"), useRefusal("quota-clear"), useRefusal("repair")].some(Boolean);
|
|
77
83
|
const path = `organizations/${encodeURIComponent(o.id)}`;
|
|
78
|
-
|
|
84
|
+
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
85
|
? 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] }));
|
|
86
|
+
: 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] }));
|
|
87
|
+
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))) })] }) }))] }), _jsx(Panels, { kind: "organization", entity: o, loaded: panels })] }));
|
|
81
88
|
}
|
|
82
89
|
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;
|
package/dist/routes/project.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A project: its services, kept current while the page is open,
|
|
2
|
+
* A project: its services, kept current while the page is open, its registry credential, its
|
|
3
|
+
* project secrets — by name and entry, never a value — and the topics it declares on the broker.
|
|
3
4
|
*/
|
|
4
5
|
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
5
6
|
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
@@ -51,9 +52,79 @@ export declare function loader({ params, context }: LoaderFunctionArgs): Promise
|
|
|
51
52
|
paused: boolean;
|
|
52
53
|
hosting: string;
|
|
53
54
|
protocol: string | undefined;
|
|
55
|
+
mounts: {
|
|
56
|
+
path: string;
|
|
57
|
+
service: string;
|
|
58
|
+
state: string;
|
|
59
|
+
}[];
|
|
60
|
+
callers: string[];
|
|
61
|
+
processPort: number | undefined;
|
|
62
|
+
broker: string | undefined;
|
|
63
|
+
undeclaredTopics: string[] | undefined;
|
|
64
|
+
topicChecks: {
|
|
65
|
+
topic: string | undefined;
|
|
66
|
+
service: string;
|
|
67
|
+
component: string;
|
|
68
|
+
direction: string;
|
|
69
|
+
stated: string | undefined;
|
|
70
|
+
state: string;
|
|
71
|
+
}[] | undefined;
|
|
72
|
+
topicSources: {
|
|
73
|
+
kind: string;
|
|
74
|
+
component: string;
|
|
75
|
+
topic: string;
|
|
76
|
+
group: string;
|
|
77
|
+
start: string;
|
|
78
|
+
version: number;
|
|
79
|
+
recordedVersion: number | undefined;
|
|
80
|
+
behind: boolean;
|
|
81
|
+
broker: string | undefined;
|
|
82
|
+
contract: string | undefined;
|
|
83
|
+
lag: number | undefined;
|
|
84
|
+
failing: string | undefined;
|
|
85
|
+
}[] | undefined;
|
|
86
|
+
objectStorage: string | undefined;
|
|
87
|
+
bucket: string | undefined;
|
|
88
|
+
bucketAddress: string | undefined;
|
|
89
|
+
}[];
|
|
90
|
+
secrets: {
|
|
91
|
+
name: string;
|
|
92
|
+
entries: string[];
|
|
93
|
+
setAt: string | undefined;
|
|
94
|
+
setBy: string | undefined;
|
|
95
|
+
}[];
|
|
96
|
+
topics: {
|
|
97
|
+
name: string;
|
|
98
|
+
partitions: number;
|
|
99
|
+
phase: string | undefined;
|
|
100
|
+
detail: string | undefined;
|
|
101
|
+
compacted: boolean;
|
|
102
|
+
contract: {
|
|
103
|
+
name: string;
|
|
104
|
+
fingerprint: string;
|
|
105
|
+
} | undefined;
|
|
106
|
+
checks: {
|
|
107
|
+
topic: string | undefined;
|
|
108
|
+
service: string;
|
|
109
|
+
component: string;
|
|
110
|
+
direction: string;
|
|
111
|
+
stated: string | undefined;
|
|
112
|
+
state: string;
|
|
113
|
+
}[];
|
|
114
|
+
}[];
|
|
115
|
+
brokers: {
|
|
116
|
+
name: string;
|
|
117
|
+
bootstrap: string;
|
|
118
|
+
shape: string;
|
|
119
|
+
secret: string;
|
|
120
|
+
declaredAt: string | undefined;
|
|
54
121
|
}[];
|
|
55
122
|
panels: import("../extensions/render.tsx").LoadedPanels;
|
|
56
123
|
}>;
|
|
57
|
-
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> |
|
|
124
|
+
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | import("react-router").UNSAFE_DataWithResponseInit<{
|
|
125
|
+
intent: "topic-schema";
|
|
126
|
+
topic: string;
|
|
127
|
+
schema: unknown;
|
|
128
|
+
}> | Response>;
|
|
58
129
|
export default function Project(): import("react").JSX.Element;
|
|
59
130
|
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
package/dist/routes/project.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* A project: its services, kept current while the page is open,
|
|
3
|
+
* A project: its services, kept current while the page is open, its registry credential, its
|
|
4
|
+
* project secrets — by name and entry, never a value — and the topics it declares on the broker.
|
|
4
5
|
*/
|
|
5
|
-
import { redirect, useLoaderData } from "react-router";
|
|
6
|
-
import {
|
|
6
|
+
import { data, redirect, useActionData, useLoaderData } from "react-router";
|
|
7
|
+
import { ControlPlaneError } from "../client/errors.js";
|
|
8
|
+
import { act, guard, pageData, projectShell, text, useConsoleContext } from "../context.js";
|
|
7
9
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
8
|
-
import {
|
|
10
|
+
import { ConsoleForm, ConsoleLink, Field, Submit, useConsole, when } from "../ui/console.js";
|
|
11
|
+
import { Page, SectionTitle } from "../ui/shell.js";
|
|
9
12
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
10
13
|
import { Lifecycle } from "../ui/status.js";
|
|
11
14
|
import { useProjectStream } from "../ui/use-stream.js";
|
|
@@ -15,9 +18,26 @@ export async function loader({ params, context }) {
|
|
|
15
18
|
const ctx = useConsoleContext(context);
|
|
16
19
|
const id = params.projectId;
|
|
17
20
|
return guard(ctx, async () => {
|
|
18
|
-
const [project, services, page] = await Promise.all([
|
|
21
|
+
const [project, services, secrets, topics, brokers, page] = await Promise.all([
|
|
22
|
+
ctx.client.getProject(id),
|
|
23
|
+
ctx.client.listServices(id),
|
|
24
|
+
ctx.client.listProjectSecrets(id),
|
|
25
|
+
ctx.client.listTopics(id),
|
|
26
|
+
ctx.client.listBrokers(id),
|
|
27
|
+
pageData(ctx),
|
|
28
|
+
]);
|
|
19
29
|
const organization = await ctx.client.getOrganization(project.organizationId);
|
|
20
|
-
|
|
30
|
+
const primary = { label: "Apply a descriptor", to: `projects/${encodeURIComponent(id)}/services/apply`, operation: "service.apply" };
|
|
31
|
+
return {
|
|
32
|
+
console: projectShell(page, organization, project, services, [], primary),
|
|
33
|
+
project,
|
|
34
|
+
organization,
|
|
35
|
+
services,
|
|
36
|
+
secrets,
|
|
37
|
+
topics,
|
|
38
|
+
brokers,
|
|
39
|
+
panels: await loadPanels(ctx, "project", project),
|
|
40
|
+
};
|
|
21
41
|
});
|
|
22
42
|
}
|
|
23
43
|
export async function action({ request, params, context }) {
|
|
@@ -43,19 +63,71 @@ export async function action({ request, params, context }) {
|
|
|
43
63
|
case "registry-clear":
|
|
44
64
|
await ctx.client.clearRegistry(id);
|
|
45
65
|
return redirect(self);
|
|
66
|
+
case "secret-set":
|
|
67
|
+
// The value crosses once, to the control plane, and is never shown again by anything.
|
|
68
|
+
await ctx.client.setProjectSecret(id, text(form, "secretName"), { [text(form, "secretEntry")]: String(form.get("secretValue") ?? "") });
|
|
69
|
+
return redirect(self);
|
|
70
|
+
case "secret-unset":
|
|
71
|
+
await ctx.client.unsetProjectSecretEntry(id, text(form, "secretName"), text(form, "secretEntry"));
|
|
72
|
+
return redirect(self);
|
|
73
|
+
case "topic-set": {
|
|
74
|
+
// A contract is a name and a schema document, both or neither; the document is read here so
|
|
75
|
+
// that a file that is not JSON is refused beside the form rather than by the platform.
|
|
76
|
+
const contractName = text(form, "topicContract");
|
|
77
|
+
const schemaText = String(form.get("topicSchema") ?? "").trim();
|
|
78
|
+
let contract;
|
|
79
|
+
if (contractName || schemaText) {
|
|
80
|
+
if (!contractName)
|
|
81
|
+
throw new ControlPlaneError(400, "a contract is a name and a schema document: give the contract's name");
|
|
82
|
+
if (!schemaText)
|
|
83
|
+
throw new ControlPlaneError(400, "a contract is a name and a schema document: give the schema");
|
|
84
|
+
let schema;
|
|
85
|
+
try {
|
|
86
|
+
schema = JSON.parse(schemaText);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
throw new ControlPlaneError(400, `the schema is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
90
|
+
}
|
|
91
|
+
contract = { name: contractName, schema };
|
|
92
|
+
}
|
|
93
|
+
await ctx.client.declareTopic(id, text(form, "topicName"), {
|
|
94
|
+
partitions: Number(text(form, "topicPartitions")),
|
|
95
|
+
compacted: form.get("topicCompacted") === "on",
|
|
96
|
+
contract,
|
|
97
|
+
});
|
|
98
|
+
return redirect(self);
|
|
99
|
+
}
|
|
100
|
+
case "topic-schema":
|
|
101
|
+
// The document a member builds against, shown beside the topic rather than sent anywhere.
|
|
102
|
+
return data({ intent, topic: text(form, "topicName"), schema: await ctx.client.topicSchema(id, text(form, "topicName")) });
|
|
103
|
+
case "topic-unset":
|
|
104
|
+
await ctx.client.removeTopic(id, text(form, "topicName"));
|
|
105
|
+
return redirect(self);
|
|
106
|
+
case "broker-set":
|
|
107
|
+
await ctx.client.declareBroker(id, text(form, "brokerName"), { bootstrap: text(form, "brokerBootstrap"), shape: text(form, "brokerShape"), secret: text(form, "brokerSecret") });
|
|
108
|
+
return redirect(self);
|
|
109
|
+
case "broker-unset":
|
|
110
|
+
await ctx.client.removeBroker(id, text(form, "brokerName"));
|
|
111
|
+
return redirect(self);
|
|
46
112
|
default:
|
|
47
113
|
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
48
114
|
}
|
|
49
115
|
});
|
|
50
116
|
}
|
|
51
117
|
export default function Project() {
|
|
52
|
-
const { project: p, organization: o, services: initial, panels } = useLoaderData();
|
|
118
|
+
const { project: p, organization: o, services: initial, secrets, topics, brokers, panels } = useLoaderData();
|
|
53
119
|
const { services, state } = useProjectStream(p.id, initial);
|
|
54
120
|
const { shows } = useConsole();
|
|
55
121
|
const renameRefusal = useRefusal("rename");
|
|
56
122
|
const deleteRefusal = useRefusal("delete");
|
|
57
123
|
const registryRefusal = useRefusal("registry-set");
|
|
124
|
+
const secretRefusal = useRefusal("secret-set");
|
|
125
|
+
const topicRefusal = useRefusal("topic-set");
|
|
126
|
+
const brokerRefusal = useRefusal("broker-set");
|
|
127
|
+
const shown = useActionData();
|
|
128
|
+
const shownSchema = shown && shown.intent === "topic-schema" && typeof shown.topic === "string" ? shown : undefined;
|
|
58
129
|
const path = `projects/${encodeURIComponent(p.id)}`;
|
|
59
|
-
|
|
130
|
+
const inspector = (_jsxs(_Fragment, { children: [_jsx("div", { className: "ac-ops", children: _jsx(HostActions, { operation: "service.apply", entity: p }) }), _jsxs("section", { className: "ac-form", "aria-labelledby": "registry-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "registry-title", children: "Registry" }) }), shows("registry.set") ? (_jsxs("details", { className: "ac-more", open: registryRefusal !== undefined || undefined, children: [_jsx("summary", { children: p.registry ? "Replace the registry credential" : "Set a registry credential" }), _jsxs(ConsoleForm, { intent: "registry-set", className: "ac-form", children: [_jsx(Field, { label: "Registry server", name: "server", required: true, placeholder: "ghcr.io", defaultValue: registryRefusal?.values.server ?? p.registry?.server }), _jsx(Field, { label: "Username", name: "username", required: true, autoComplete: "off", defaultValue: registryRefusal?.values.username ?? p.registry?.username }), _jsx(Field, { label: "Password or token", name: "password", type: "password", required: true, autoComplete: "new-password", hint: "Sent once to the platform, which keeps it only in the cluster. It is never shown again." }), _jsx(Refused, { intent: "registry-set" }), _jsx("div", { children: _jsx(Submit, { intent: "registry-set", children: "Save credential" }) })] })] })) : null, p.registry && shows("registry.clear") ? (_jsxs(ConsoleForm, { intent: "registry-clear", children: [_jsx(Submit, { intent: "registry-clear", children: "Clear credential" }), _jsx(Refused, { intent: "registry-clear" })] })) : null] }), shows("project-secret.set") ? (_jsxs("section", { className: "ac-form", "aria-labelledby": "secret-set-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "secret-set-title", children: "Project secrets" }) }), _jsxs("details", { className: "ac-more", open: secretRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Set a project secret entry" }), _jsxs(ConsoleForm, { intent: "secret-set", className: "ac-form", children: [_jsx(Field, { label: "Project secret", name: "secretName", required: true, placeholder: "checkout", defaultValue: secretRefusal?.values.secretName }), _jsx(Field, { label: "Entry", name: "secretEntry", required: true, placeholder: "STRIPE_KEY", autoComplete: "off", defaultValue: secretRefusal?.values.secretEntry }), _jsx(Field, { label: "Value", name: "secretValue", type: "password", required: true, autoComplete: "new-password", hint: "Sent once to the platform, which keeps it only in the cluster. It is never shown again." }), _jsx(Refused, { intent: "secret-set" }), _jsx("div", { children: _jsx(Submit, { intent: "secret-set", children: "Save entry" }) })] })] })] })) : null, shows("project-topic.set") ? (_jsxs("section", { className: "ac-form", "aria-labelledby": "topic-set-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "topic-set-title", children: "Topics" }) }), _jsxs("details", { className: "ac-more", open: topicRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Declare a topic" }), _jsxs(ConsoleForm, { intent: "topic-set", className: "ac-form", children: [_jsx(Field, { label: "Topic", name: "topicName", required: true, placeholder: "transactions", defaultValue: topicRefusal?.values.topicName }), _jsx(Field, { label: "Partitions", name: "topicPartitions", type: "number", required: true, defaultValue: topicRefusal?.values.topicPartitions ?? "3", hint: "A topic can be given more partitions later, never fewer." }), _jsxs("div", { className: "ac-field", children: [_jsxs("label", { htmlFor: "topicCompacted", children: [_jsx("input", { id: "topicCompacted", name: "topicCompacted", type: "checkbox", defaultChecked: topicRefusal?.values.topicCompacted === "on" }), " Compacted"] }), _jsx("p", { className: "ac-hint", id: "topicCompacted-hint", children: "The broker keeps the last message under each key, as a graph's delta topic needs." })] }), _jsx(Field, { label: "Contract", name: "topicContract", placeholder: "order.v1", autoComplete: "off", defaultValue: topicRefusal?.values.topicContract, hint: "What the topic carries, which every side must state. Optional, with its schema." }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "topicSchema", children: "Schema" }), _jsx("textarea", { id: "topicSchema", name: "topicSchema", spellCheck: false, placeholder: '{"type": "object"}', defaultValue: topicRefusal?.values.topicSchema, "aria-describedby": "topicSchema-hint" }), _jsx("p", { className: "ac-hint", id: "topicSchema-hint", children: "The contract's JSON Schema document, held by the project for members to build against." })] }), _jsx(Refused, { intent: "topic-set" }), _jsx("div", { children: _jsx(Submit, { intent: "topic-set", children: "Declare topic" }) })] })] })] })) : null, shows("project-broker.set") ? (_jsxs("section", { className: "ac-form", "aria-labelledby": "broker-set-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "broker-set-title", children: "Brokers" }) }), _jsxs("details", { className: "ac-more", open: brokerRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Declare a broker" }), _jsxs(ConsoleForm, { intent: "broker-set", className: "ac-form", children: [_jsx(Field, { label: "Broker", name: "brokerName", required: true, placeholder: "legacy", defaultValue: brokerRefusal?.values.brokerName, hint: "The name a component gives for a topic that lives there." }), _jsx(Field, { label: "Bootstrap", name: "brokerBootstrap", required: true, placeholder: "kafka.legacy:9094", autoComplete: "off", defaultValue: brokerRefusal?.values.brokerBootstrap, hint: "host:port, several separated by commas." }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "brokerShape", children: "Shape" }), _jsxs("select", { id: "brokerShape", name: "brokerShape", defaultValue: brokerRefusal?.values.brokerShape ?? "sasl", "aria-describedby": "brokerShape-hint", children: [_jsx("option", { value: "sasl", children: "sasl" }), _jsx("option", { value: "certificate", children: "certificate" })] }), _jsx("p", { className: "ac-hint", id: "brokerShape-hint", children: "sasl: the secret holds ca.crt, username and password. certificate: ca.crt, tls.crt and tls.key. Both over TLS." })] }), _jsx(Field, { label: "Credential secret", name: "brokerSecret", required: true, placeholder: "legacy-credential", autoComplete: "off", list: "broker-secrets", defaultValue: brokerRefusal?.values.brokerSecret, hint: "The project secret holding the credential, mounted for the platform's program alone." }), _jsx("datalist", { id: "broker-secrets", children: secrets.map((s) => (_jsx("option", { value: s.name }, s.name))) }), _jsx(Refused, { intent: "broker-set" }), _jsx("div", { children: _jsx(Submit, { intent: "broker-set", children: "Declare broker" }) })] })] })] })) : null, shows("project.rename") || shows("project.delete") ? (_jsxs("details", { className: "ac-more", open: renameRefusal !== undefined || deleteRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Rename or delete" }), shows("project.rename") ? (_jsxs(ConsoleForm, { intent: "rename", className: "ac-inline", children: [_jsx(Field, { label: "New name", name: "name", required: true, defaultValue: renameRefusal?.values.name ?? p.name }), _jsx(Submit, { intent: "rename", children: "Rename" })] })) : null, _jsx(Refused, { intent: "rename" }), shows("project.delete") ? (_jsxs("div", { className: "ac-danger-zone", children: [_jsxs("p", { children: ["Deleting ", p.name, " is permanent, and its id can never be used again. It must have no services."] }), _jsx(ConsoleForm, { intent: "delete", children: _jsx(Submit, { intent: "delete", danger: true, children: "Delete project" }) }), _jsx(Refused, { intent: "delete" })] })) : null] })) : null] }));
|
|
131
|
+
return (_jsxs(Page, { inspector: inspector, children: [_jsx("h1", { children: p.name }), _jsxs("dl", { className: "ac-card ac-facts", children: [_jsx("dt", { children: "Id" }), _jsx("dd", { children: p.id }), _jsx("dt", { children: "Organization" }), _jsx("dd", { children: o.name }), _jsx("dt", { children: "Registry" }), _jsx("dd", { children: p.registry ? (_jsxs(_Fragment, { children: ["Private images are pulled from ", _jsx("strong", { children: p.registry.server }), " as ", _jsx("strong", { children: p.registry.username }), ", set ", when(p.registry.setAt), p.registry.setBy ? ` by ${p.registry.setBy}` : "", "."] })) : ("Services in this project pull only public images. Set a credential to pull private ones.") })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "services", children: [_jsx("h2", { id: "services", children: "Services" }), state === "live" ? _jsx("p", { className: "ac-live", children: "Updating as the platform reports" }) : null, services.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No services yet. Apply a descriptor to deploy one." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "services", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Service" }), _jsx("th", { scope: "col", children: "State" }), _jsx("th", { scope: "col", className: "ac-num", children: "Instances" }), _jsx("th", { scope: "col", children: "Image" }), _jsx("th", { scope: "col", className: "ac-num", children: "Generation" }), _jsx("th", { scope: "col", children: "Address" })] }) }), _jsx("tbody", { children: services.map((s) => (_jsxs("tr", { "data-service": s.name, children: [_jsx("td", { children: _jsx(ConsoleLink, { to: `${path}/services/${encodeURIComponent(s.name)}`, children: s.name }) }), _jsx("td", { children: _jsx(Lifecycle, { lifecycle: s.lifecycle, confirmed: s.confirmed }) }), _jsxs("td", { className: "ac-num", children: [s.readyInstances, " of ", s.desiredInstances] }), _jsx("td", { children: s.image }), _jsx("td", { className: "ac-num", children: s.generation }), _jsx("td", { children: s.hostname ? _jsx("a", { href: s.hostname, children: s.hostname.replace(/^https:\/\//, "") }) : s.exposed ? "Exposed, no address yet" : "Not exposed" })] }, s.name))) })] }) }))] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "secrets", children: [_jsx("h2", { id: "secrets", children: "Project secrets" }), secrets.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No project secrets. A descriptor's variable can take an entry of one by secretKeyRef." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "secrets", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Secret" }), _jsx("th", { scope: "col", children: "Entry" }), _jsx("th", { scope: "col", children: "Set" }), _jsx("th", { scope: "col", children: _jsx("span", { className: "ac-visually-hidden", children: "Actions" }) })] }) }), _jsx("tbody", { children: secrets.flatMap((s) => s.entries.map((entry) => (_jsxs("tr", { "data-secret": s.name, "data-entry": entry, children: [_jsx("td", { children: s.name }), _jsx("td", { children: entry }), _jsxs("td", { children: [when(s.setAt), s.setBy ? ` by ${s.setBy}` : ""] }), _jsx("td", { children: shows("project-secret.unset") ? (_jsxs(ConsoleForm, { intent: "secret-unset", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "secretName", value: s.name }), _jsx("input", { type: "hidden", name: "secretEntry", value: entry }), _jsx(Submit, { intent: "secret-unset", children: `Remove ${entry}` })] })) : null })] }, `${s.name}/${entry}`)))) })] }) })), _jsx(Refused, { intent: "secret-unset" })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "topics", children: [_jsx("h2", { id: "topics", children: "Topics" }), topics.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No topics declared. Every service of the project uses a declared topic by its name." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "topics", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Topic" }), _jsx("th", { scope: "col", className: "ac-num", children: "Partitions" }), _jsx("th", { scope: "col", children: "Compacted" }), _jsx("th", { scope: "col", children: "Contract" }), _jsx("th", { scope: "col", children: "Broker" }), _jsx("th", { scope: "col", children: "Checks" }), _jsx("th", { scope: "col", children: _jsx("span", { className: "ac-visually-hidden", children: "Actions" }) })] }) }), _jsx("tbody", { children: topics.map((t) => (_jsxs("tr", { "data-topic": t.name, children: [_jsx("td", { children: _jsx("code", { children: t.name }) }), _jsx("td", { className: "ac-num", children: t.partitions }), _jsx("td", { "data-compacted": t.compacted ? "yes" : "no", children: t.compacted ? "yes" : "no" }), _jsx("td", { children: t.contract ? (_jsxs(_Fragment, { children: [_jsx("code", { children: t.contract.name }), " ", _jsx("span", { className: "ac-hint", children: t.contract.fingerprint.slice(0, 15) })] })) : ("none") }), _jsxs("td", { children: [t.phase ?? "Nothing reported yet", t.detail ? ` — ${t.detail}` : ""] }), _jsx("td", { children: t.checks.length === 0 ? (t.contract ? "no side seen yet" : "—") : (_jsx("ul", { className: "ac-topics", children: t.checks.map((c) => (_jsxs("li", { "data-check": c.state, children: [c.service, " ", c.direction, ": ", c.state, c.state === "mismatch" ? ` (${c.stated ?? "none"})` : ""] }, `${c.service}/${c.component}/${c.direction}`))) })) }), _jsxs("td", { children: [t.contract ? (_jsxs(ConsoleForm, { intent: "topic-schema", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "topicName", value: t.name }), _jsx(Submit, { intent: "topic-schema", children: `Show schema of ${t.name}` })] })) : null, shows("project-topic.unset") ? (_jsxs(ConsoleForm, { intent: "topic-unset", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "topicName", value: t.name }), _jsx(Submit, { intent: "topic-unset", children: `Stop declaring ${t.name}` })] })) : null] })] }, t.name))) })] }) })), shownSchema ? (_jsxs("div", { className: "ac-card", "data-schema": shownSchema.topic, children: [_jsxs("h3", { children: ["Schema of ", _jsx("code", { children: shownSchema.topic })] }), _jsx("pre", { children: JSON.stringify(shownSchema.schema, null, 2) })] })) : null, _jsx(Refused, { intent: "topic-unset" }), _jsx(Refused, { intent: "topic-schema" })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "brokers", children: [_jsx("h2", { id: "brokers", children: "Brokers" }), brokers.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No brokers declared beside the installation's. A component may name one for a topic that lives elsewhere." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "brokers", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Broker" }), _jsx("th", { scope: "col", children: "Bootstrap" }), _jsx("th", { scope: "col", children: "Shape" }), _jsx("th", { scope: "col", children: "Secret" }), _jsx("th", { scope: "col", children: "Declared" }), _jsx("th", { scope: "col", children: _jsx("span", { className: "ac-visually-hidden", children: "Actions" }) })] }) }), _jsx("tbody", { children: brokers.map((b) => (_jsxs("tr", { "data-broker": b.name, children: [_jsx("td", { children: _jsx("code", { children: b.name }) }), _jsx("td", { children: b.bootstrap }), _jsx("td", { children: b.shape }), _jsx("td", { children: b.secret }), _jsx("td", { children: b.declaredAt ? when(b.declaredAt) : "—" }), _jsx("td", { children: shows("project-broker.unset") ? (_jsxs(ConsoleForm, { intent: "broker-unset", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "brokerName", value: b.name }), _jsx(Submit, { intent: "broker-unset", children: `Stop declaring ${b.name}` })] })) : null })] }, b.name))) })] }) })), _jsx(Refused, { intent: "broker-unset" })] }), _jsx(Panels, { kind: "project", entity: p, loaded: panels })] }));
|
|
60
132
|
}
|
|
61
133
|
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Applies a descriptor: pasted,
|
|
3
|
-
*
|
|
2
|
+
* Applies a descriptor: pasted, read from a file, or started from the one a service applied at an
|
|
3
|
+
* earlier generation (`?name=cart&generation=1`). The console checks only that the text is JSON and
|
|
4
|
+
* names a service; every rule about what a descriptor may say is the control plane's, and its
|
|
4
5
|
* refusal is shown problem by problem beside the text.
|
|
5
6
|
*/
|
|
6
7
|
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
@@ -38,6 +39,8 @@ export declare function loader({ request, params, context }: LoaderFunctionArgs)
|
|
|
38
39
|
};
|
|
39
40
|
};
|
|
40
41
|
name: string | null;
|
|
42
|
+
generation: string | null;
|
|
43
|
+
from: string | undefined;
|
|
41
44
|
}>;
|
|
42
45
|
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<ActionRefusal> | Response>;
|
|
43
46
|
export default function ApplyDescriptor(): import("react").JSX.Element;
|