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/project.js
CHANGED
|
@@ -1,23 +1,63 @@
|
|
|
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 — the topics it declares on the broker, the
|
|
5
|
+
* grants it makes to other projects' services and to machines, and what other projects grant it.
|
|
4
6
|
*/
|
|
5
|
-
import { redirect, useLoaderData } from "react-router";
|
|
6
|
-
import {
|
|
7
|
+
import { data, redirect, useActionData, useLoaderData } from "react-router";
|
|
8
|
+
import { ControlPlaneError } from "../client/errors.js";
|
|
9
|
+
import { act, guard, pageData, projectShell, text, useConsoleContext } from "../context.js";
|
|
7
10
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
8
|
-
import {
|
|
11
|
+
import { ConsoleForm, ConsoleLink, Field, Submit, useConsole, when } from "../ui/console.js";
|
|
12
|
+
import { Page, SectionTitle } from "../ui/shell.js";
|
|
9
13
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
10
14
|
import { Lifecycle } from "../ui/status.js";
|
|
11
15
|
import { useProjectStream } from "../ui/use-stream.js";
|
|
16
|
+
import { targetText } from "../ui/grants.js";
|
|
12
17
|
import { HostActions, loadPanels, Panels } from "../extensions/render.js";
|
|
13
18
|
export const meta = ({ loaderData }) => [{ title: `${loaderData?.project.name ?? "Project"} · ankka` }];
|
|
14
19
|
export async function loader({ params, context }) {
|
|
15
20
|
const ctx = useConsoleContext(context);
|
|
16
21
|
const id = params.projectId;
|
|
17
22
|
return guard(ctx, async () => {
|
|
18
|
-
const [project, services, page] = await Promise.all([
|
|
23
|
+
const [project, services, secrets, topics, brokers, grants, received, page, backups, rehearsals, restores, history, setting] = await Promise.all([
|
|
24
|
+
ctx.client.getProject(id),
|
|
25
|
+
ctx.client.listServices(id),
|
|
26
|
+
ctx.client.listProjectSecrets(id),
|
|
27
|
+
ctx.client.listTopics(id),
|
|
28
|
+
ctx.client.listBrokers(id),
|
|
29
|
+
ctx.client.grants(id),
|
|
30
|
+
ctx.client.receivedGrants(id),
|
|
31
|
+
pageData(ctx),
|
|
32
|
+
// The database's backups are the project's, read as the person; a control plane that cannot
|
|
33
|
+
// say leaves the section saying so rather than failing the page.
|
|
34
|
+
ctx.client.projectStatus(id).catch(() => undefined),
|
|
35
|
+
ctx.client.listRehearsals(id).catch(() => []),
|
|
36
|
+
ctx.client.listRestores(id).catch(() => []),
|
|
37
|
+
ctx.client.projectHistory(id).catch(() => []),
|
|
38
|
+
ctx.client.projectDatabase(id).catch(() => undefined),
|
|
39
|
+
]);
|
|
40
|
+
// A verified restore says what the broker holds past its moment, asked as it is read.
|
|
41
|
+
const reports = await Promise.all(restores.filter((r) => r.phase === "Verified" || r.phase === "InUse").map((r) => ctx.client.getRestore(id, r.name).catch(() => r)));
|
|
19
42
|
const organization = await ctx.client.getOrganization(project.organizationId);
|
|
20
|
-
|
|
43
|
+
const primary = { label: "Apply a descriptor", to: `projects/${encodeURIComponent(id)}/services/apply`, operation: "service.apply" };
|
|
44
|
+
return {
|
|
45
|
+
console: projectShell(page, organization, project, services, [], primary),
|
|
46
|
+
project,
|
|
47
|
+
organization,
|
|
48
|
+
services,
|
|
49
|
+
secrets,
|
|
50
|
+
topics,
|
|
51
|
+
brokers,
|
|
52
|
+
backups,
|
|
53
|
+
rehearsals,
|
|
54
|
+
restores: restores.map((r) => reports.find((d) => d.name === r.name) ?? r),
|
|
55
|
+
history,
|
|
56
|
+
setting,
|
|
57
|
+
grants,
|
|
58
|
+
received,
|
|
59
|
+
panels: await loadPanels(ctx, "project", project),
|
|
60
|
+
};
|
|
21
61
|
});
|
|
22
62
|
}
|
|
23
63
|
export async function action({ request, params, context }) {
|
|
@@ -31,6 +71,27 @@ export async function action({ request, params, context }) {
|
|
|
31
71
|
case "rename":
|
|
32
72
|
await ctx.client.renameProject(id, text(form, "name"));
|
|
33
73
|
return redirect(self);
|
|
74
|
+
case "restore": {
|
|
75
|
+
const restore = await ctx.client.restoreProject(id, new Date(text(form, "moment")).toISOString(), text(form, "line") || undefined);
|
|
76
|
+
return redirect(`${self}#restore-${restore.name}`);
|
|
77
|
+
}
|
|
78
|
+
case "rehearse":
|
|
79
|
+
await ctx.client.rehearseProject(id, text(form, "moment") ? new Date(text(form, "moment")).toISOString() : undefined);
|
|
80
|
+
return redirect(`${self}#rehearsals`);
|
|
81
|
+
case "reissue":
|
|
82
|
+
await ctx.client.reissueBackupCredential(id);
|
|
83
|
+
return redirect(`${self}#database`);
|
|
84
|
+
case "database":
|
|
85
|
+
await ctx.client.setProjectDatabase(id, {
|
|
86
|
+
replicas: Number(text(form, "replicas") || "0"),
|
|
87
|
+
synchronous: form.get("synchronous") === "on",
|
|
88
|
+
retentionDays: text(form, "retentionDays") ? Number(text(form, "retentionDays")) : undefined,
|
|
89
|
+
rehearse: text(form, "rehearse") || undefined,
|
|
90
|
+
});
|
|
91
|
+
return redirect(`${self}#database`);
|
|
92
|
+
case "switch":
|
|
93
|
+
await ctx.client.switchService(id, text(form, "service"), text(form, "cluster"));
|
|
94
|
+
return redirect(`${self}#database`);
|
|
34
95
|
case "delete": {
|
|
35
96
|
const project = await ctx.client.getProject(id);
|
|
36
97
|
await ctx.client.deleteProject(id);
|
|
@@ -43,19 +104,131 @@ export async function action({ request, params, context }) {
|
|
|
43
104
|
case "registry-clear":
|
|
44
105
|
await ctx.client.clearRegistry(id);
|
|
45
106
|
return redirect(self);
|
|
107
|
+
case "secret-set":
|
|
108
|
+
// The value crosses once, to the control plane, and is never shown again by anything.
|
|
109
|
+
await ctx.client.setProjectSecret(id, text(form, "secretName"), { [text(form, "secretEntry")]: String(form.get("secretValue") ?? "") });
|
|
110
|
+
return redirect(self);
|
|
111
|
+
case "secret-unset":
|
|
112
|
+
await ctx.client.unsetProjectSecretEntry(id, text(form, "secretName"), text(form, "secretEntry"));
|
|
113
|
+
return redirect(self);
|
|
114
|
+
case "topic-set": {
|
|
115
|
+
// A contract is a name and a schema document, both or neither; the document is read here so
|
|
116
|
+
// that a file that is not JSON is refused beside the form rather than by the platform.
|
|
117
|
+
const contractName = text(form, "topicContract");
|
|
118
|
+
const schemaText = String(form.get("topicSchema") ?? "").trim();
|
|
119
|
+
let contract;
|
|
120
|
+
if (contractName || schemaText) {
|
|
121
|
+
if (!contractName)
|
|
122
|
+
throw new ControlPlaneError(400, "a contract is a name and a schema document: give the contract's name");
|
|
123
|
+
if (!schemaText)
|
|
124
|
+
throw new ControlPlaneError(400, "a contract is a name and a schema document: give the schema");
|
|
125
|
+
let schema;
|
|
126
|
+
try {
|
|
127
|
+
schema = JSON.parse(schemaText);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
throw new ControlPlaneError(400, `the schema is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
131
|
+
}
|
|
132
|
+
contract = { name: contractName, schema };
|
|
133
|
+
}
|
|
134
|
+
// Each setting left empty is left out: the installation's default on a new topic, kept on
|
|
135
|
+
// one already declared. `topicRemoves` is the owner's confirmation of what a change removes.
|
|
136
|
+
const optional = (field) => text(form, field) || undefined;
|
|
137
|
+
const count = (field) => (text(form, field) ? Number(text(form, field)) : undefined);
|
|
138
|
+
await ctx.client.declareTopic(id, text(form, "topicName"), {
|
|
139
|
+
partitions: count("topicPartitions"),
|
|
140
|
+
compacted: form.get("topicCompacted") === "on",
|
|
141
|
+
contract,
|
|
142
|
+
retention: optional("topicRetention"),
|
|
143
|
+
retentionSize: optional("topicRetentionSize"),
|
|
144
|
+
cleanup: optional("topicCleanup"),
|
|
145
|
+
tombstoneWindow: optional("topicTombstoneWindow"),
|
|
146
|
+
minCompactionLag: optional("topicMinCompactionLag"),
|
|
147
|
+
maxCompactionLag: optional("topicMaxCompactionLag"),
|
|
148
|
+
copies: count("topicCopies"),
|
|
149
|
+
minInSync: count("topicMinInSync"),
|
|
150
|
+
removes: optional("topicRemoves"),
|
|
151
|
+
});
|
|
152
|
+
return redirect(self);
|
|
153
|
+
}
|
|
154
|
+
case "topic-schema":
|
|
155
|
+
// The document a member builds against, shown beside the topic rather than sent anywhere.
|
|
156
|
+
return data({ intent, topic: text(form, "topicName"), schema: await ctx.client.topicSchema(id, text(form, "topicName")) });
|
|
157
|
+
case "topic-unset":
|
|
158
|
+
await ctx.client.removeTopic(id, text(form, "topicName"));
|
|
159
|
+
return redirect(self);
|
|
160
|
+
case "broker-set":
|
|
161
|
+
await ctx.client.declareBroker(id, text(form, "brokerName"), { bootstrap: text(form, "brokerBootstrap"), shape: text(form, "brokerShape"), secret: text(form, "brokerSecret") });
|
|
162
|
+
return redirect(self);
|
|
163
|
+
case "broker-unset":
|
|
164
|
+
await ctx.client.removeBroker(id, text(form, "brokerName"));
|
|
165
|
+
return redirect(self);
|
|
166
|
+
case "location-set":
|
|
167
|
+
await ctx.client.setProjectLocation(id, text(form, "location"));
|
|
168
|
+
return redirect(self);
|
|
169
|
+
case "location-clear":
|
|
170
|
+
await ctx.client.clearProjectLocation(id);
|
|
171
|
+
return redirect(self);
|
|
172
|
+
case "grant-make":
|
|
173
|
+
await ctx.client.makeGrant(id, { grantee: text(form, "grantee"), target: grantTarget(form) });
|
|
174
|
+
return redirect(self);
|
|
175
|
+
case "grant-end":
|
|
176
|
+
// A pending grant is withdrawn and an accepted one revoked: the control plane decides which.
|
|
177
|
+
await ctx.client.endGrant(id, text(form, "grantId"));
|
|
178
|
+
return redirect(self);
|
|
46
179
|
default:
|
|
47
180
|
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
48
181
|
}
|
|
49
182
|
});
|
|
50
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The target the form describes, with only its kind's fields: the control plane refuses a field
|
|
186
|
+
* another kind would take ("a topic target has no service"), so an empty or foreign one is left out.
|
|
187
|
+
*/
|
|
188
|
+
function grantTarget(form) {
|
|
189
|
+
const kind = text(form, "grantKind");
|
|
190
|
+
const given = (name) => text(form, name) || undefined;
|
|
191
|
+
// Absent fields are left out of the request body, as `JSON.stringify` drops `undefined`.
|
|
192
|
+
const none = { service: undefined, method: undefined, path: undefined, topic: undefined, right: undefined, decrypt: false };
|
|
193
|
+
switch (kind) {
|
|
194
|
+
case "route":
|
|
195
|
+
return { ...none, kind, service: given("grantService"), method: given("grantMethod")?.toUpperCase(), path: given("grantPath") };
|
|
196
|
+
case "method":
|
|
197
|
+
return { ...none, kind, service: given("grantService"), method: given("grantMethod") };
|
|
198
|
+
case "topic":
|
|
199
|
+
return { ...none, kind, topic: given("grantTopic"), right: given("grantRight"), decrypt: form.get("grantDecrypt") === "on" };
|
|
200
|
+
default:
|
|
201
|
+
return { ...none, kind };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** A live grant is ended by withdrawing it while it waits for an answer, by revoking it once accepted. */
|
|
205
|
+
const endVerb = (state) => (state === "pending" ? "Withdraw" : "Revoke");
|
|
51
206
|
export default function Project() {
|
|
52
|
-
const { project: p, organization: o, services: initial, panels } = useLoaderData();
|
|
207
|
+
const { project: p, organization: o, services: initial, secrets, topics, brokers, grants, received, backups, rehearsals, restores, history, setting, panels } = useLoaderData();
|
|
208
|
+
const databaseRefusal = useRefusal("database");
|
|
209
|
+
// One history: what was done to a topic names it, and is shown with the topics.
|
|
210
|
+
const topicHistory = history.filter((h) => h.topic !== undefined);
|
|
211
|
+
const databaseHistory = history.filter((h) => h.topic === undefined);
|
|
53
212
|
const { services, state } = useProjectStream(p.id, initial);
|
|
54
213
|
const { shows } = useConsole();
|
|
55
214
|
const renameRefusal = useRefusal("rename");
|
|
56
215
|
const deleteRefusal = useRefusal("delete");
|
|
57
216
|
const registryRefusal = useRefusal("registry-set");
|
|
217
|
+
const secretRefusal = useRefusal("secret-set");
|
|
218
|
+
const topicRefusal = useRefusal("topic-set");
|
|
219
|
+
// A change that removes messages, refused until an owner says so: what it removes, to confirm.
|
|
220
|
+
const removal = /this declaration removes (.+); a declaration that removes messages says so with "removes"/.exec(topicRefusal?.reason ?? "")?.[1];
|
|
221
|
+
const brokerRefusal = useRefusal("broker-set");
|
|
222
|
+
const locationRefusal = useRefusal("location-set");
|
|
223
|
+
const grantRefusal = useRefusal("grant-make");
|
|
224
|
+
const shown = useActionData();
|
|
225
|
+
const shownSchema = shown && shown.intent === "topic-schema" && typeof shown.topic === "string" ? shown : undefined;
|
|
58
226
|
const path = `projects/${encodeURIComponent(p.id)}`;
|
|
59
|
-
|
|
227
|
+
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", defaultValue: topicRefusal?.values.topicPartitions ?? "3", hint: "Needed for a new topic. A topic can be given more partitions later, never fewer." }), _jsx(Field, { label: "Retention", name: "topicRetention", placeholder: "90d", autoComplete: "off", defaultValue: topicRefusal?.values.topicRetention, hint: "How long the topic keeps a message: 90d, 36h, or everything. Empty: the installation's default, or what the topic has." }), _jsx(Field, { label: "Retention size", name: "topicRetentionSize", placeholder: "50GiB", autoComplete: "off", defaultValue: topicRefusal?.values.topicRetentionSize, hint: "How much each partition keeps: 50GiB, or none." }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "topicCleanup", children: "Cleanup policy" }), _jsxs("select", { id: "topicCleanup", name: "topicCleanup", defaultValue: topicRefusal?.values.topicCleanup ?? "", children: [_jsx("option", { value: "", children: "Unchanged, or the installation's default" }), _jsx("option", { value: "delete", children: "delete" }), _jsx("option", { value: "compact", children: "compact" }), _jsx("option", { value: "compact,delete", children: "compact,delete" })] })] }), _jsx(Field, { label: "Tombstone window", name: "topicTombstoneWindow", placeholder: "1d", autoComplete: "off", defaultValue: topicRefusal?.values.topicTombstoneWindow, hint: "How long a compacted topic keeps a deletion." }), _jsx(Field, { label: "Minimum compaction lag", name: "topicMinCompactionLag", placeholder: "0s", autoComplete: "off", defaultValue: topicRefusal?.values.topicMinCompactionLag }), _jsx(Field, { label: "Maximum compaction lag", name: "topicMaxCompactionLag", placeholder: "none", autoComplete: "off", defaultValue: topicRefusal?.values.topicMaxCompactionLag }), _jsx(Field, { label: "Copies", name: "topicCopies", type: "number", defaultValue: topicRefusal?.values.topicCopies, hint: "How many copies the broker keeps; fixed once the topic is declared." }), _jsx(Field, { label: "Minimum in-sync copies", name: "topicMinInSync", type: "number", defaultValue: topicRefusal?.values.topicMinInSync, hint: "How many copies must hold a message before it is acknowledged; fixed once declared." }), _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." })] }), removal === undefined ? _jsx(Refused, { intent: "topic-set" }) : null, _jsx("div", { children: _jsx(Submit, { intent: "topic-set", children: "Declare topic" }) })] }), removal !== undefined && topicRefusal ? (_jsxs("div", { className: "ac-card", role: "alertdialog", "aria-labelledby": "topic-removal-title", "data-removal-dialog": true, children: [_jsx("h3", { id: "topic-removal-title", children: "This declaration removes messages" }), _jsxs("p", { children: ["It removes ", removal, ", and they are gone. Only an owner of the organization may send it."] }), _jsxs(ConsoleForm, { intent: "topic-set", className: "ac-inline", children: [Object.entries(topicRefusal.values)
|
|
228
|
+
.filter(([k]) => k !== "intent" && k !== "topicRemoves")
|
|
229
|
+
.map(([k, v]) => (_jsx("input", { type: "hidden", name: k, value: v }, k))), _jsx("input", { type: "hidden", name: "topicRemoves", value: removal }), _jsx(Submit, { intent: "topic-set", children: "Remove them" })] })] })) : null] })] })) : 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-location.set") || shows("project-location.clear") ? (_jsxs("section", { className: "ac-form", "aria-labelledby": "location-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "location-title", children: "Bucket location" }) }), shows("project-location.set") ? (_jsxs("details", { className: "ac-more", open: locationRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Choose where new buckets are made" }), _jsxs(ConsoleForm, { intent: "location-set", className: "ac-form", children: [_jsx(Field, { label: "Location", name: "location", required: true, placeholder: "europe-west6", autoComplete: "off", defaultValue: locationRefusal?.values.location, hint: "Where the project's new buckets in Google Cloud Storage are made, in the installation's own words. A bucket's location is fixed when it is made, so this moves no bucket." }), _jsx(Refused, { intent: "location-set" }), _jsx("div", { children: _jsx(Submit, { intent: "location-set", children: "Set location" }) })] })] })) : null, shows("project-location.clear") ? (_jsxs(ConsoleForm, { intent: "location-clear", children: [_jsx(Submit, { intent: "location-clear", children: "Use the installation's location" }), _jsx(Refused, { intent: "location-clear" })] })) : null] })) : null, shows("grant.make") ? (_jsxs("section", { className: "ac-form", "aria-labelledby": "grant-make-title", children: [_jsx(SectionTitle, { children: _jsx("span", { id: "grant-make-title", children: "Grants" }) }), _jsxs("details", { className: "ac-more", open: grantRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Make a grant" }), _jsxs(ConsoleForm, { intent: "grant-make", className: "ac-form", children: [_jsx(Field, { label: "Grantee", name: "grantee", required: true, placeholder: "service:payments/merchant", autoComplete: "off", defaultValue: grantRefusal?.values.grantee, hint: "service:<project>/<service>, or machine:<organization>/<name>. Another organization's grantee must accept first." }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "grantKind", children: "Grant kind" }), _jsxs("select", { id: "grantKind", name: "grantKind", defaultValue: grantRefusal?.values.grantKind ?? "route", "aria-describedby": "grantKind-hint", children: [_jsx("option", { value: "route", children: "route" }), _jsx("option", { value: "method", children: "method" }), _jsx("option", { value: "topic", children: "topic" }), _jsx("option", { value: "erasure", children: "erasure" })] }), _jsx("p", { className: "ac-hint", id: "grantKind-hint", children: "route: a service, an HTTP method and a path. method: a service and a gRPC method. topic: a declared topic and a right. erasure: nothing more." })] }), _jsx(Field, { label: "Granted service", name: "grantService", placeholder: "wallet", autoComplete: "off", defaultValue: grantRefusal?.values.grantService }), _jsx(Field, { label: "Granted method", name: "grantMethod", placeholder: "POST", autoComplete: "off", defaultValue: grantRefusal?.values.grantMethod, hint: "An HTTP method for a route; Service/Method for a gRPC method." }), _jsx(Field, { label: "Granted path", name: "grantPath", placeholder: "/v1/wallets/{player}", autoComplete: "off", defaultValue: grantRefusal?.values.grantPath }), _jsx(Field, { label: "Granted topic", name: "grantTopic", list: "grant-topics", autoComplete: "off", defaultValue: grantRefusal?.values.grantTopic }), _jsx("datalist", { id: "grant-topics", children: topics.map((t) => (_jsx("option", { value: t.name }, t.name))) }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "grantRight", children: "Granted right" }), _jsxs("select", { id: "grantRight", name: "grantRight", defaultValue: grantRefusal?.values.grantRight ?? "consume", children: [_jsx("option", { value: "consume", children: "consume" }), _jsx("option", { value: "produce", children: "produce" })] })] }), _jsxs("div", { className: "ac-field", children: [_jsxs("label", { htmlFor: "grantDecrypt", children: [_jsx("input", { id: "grantDecrypt", name: "grantDecrypt", type: "checkbox", defaultChecked: grantRefusal?.values.grantDecrypt === "on" }), " Allow decryption"] }), _jsx("p", { className: "ac-hint", id: "grantDecrypt-hint", children: "Only with consume: the grantee may read what the topic's producers encrypted." })] }), _jsx(Refused, { intent: "grant-make" }), _jsx("div", { children: _jsx(Submit, { intent: "grant-make", children: "Make grant" }) })] })] }), _jsx("div", { className: "ac-ops", children: _jsx(HostActions, { operation: "grant.make", entity: p }) })] })) : 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] }));
|
|
230
|
+
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 }), _jsxs("td", { children: [s.hostname ? _jsx("a", { href: s.hostname, children: s.hostname.replace(/^https:\/\//, "") }) : s.exposed ? "Exposed, no address yet" : "Not exposed", s.customHostnames.map((h) => (_jsxs("span", { className: "ac-hint", "data-custom-hostname": h.hostname, children: [", ", h.hostname, h.state === "serving" ? "" : ` (${h.state})`] }, h.hostname)))] })] }, 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: "Retention" }), _jsx("th", { scope: "col", children: "Size" }), _jsx("th", { scope: "col", children: "Cleanup" }), _jsx("th", { scope: "col", children: "Copies" }), _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 }), ["retention", "retentionSize", "cleanup"].map((field) => (_jsxs("td", { "data-setting": field, "data-defaulted": t.settings?.defaulted.includes(field) ? "yes" : "no", children: [t.settings ? t.settings[field] : "—", t.settings?.defaulted.includes(field) ? _jsx("span", { className: "ac-hint", children: " default" }) : null] }, field))), _jsx("td", { "data-setting": "copies", children: t.settings?.copies !== undefined && t.settings?.copies !== null
|
|
231
|
+
? `${t.settings.copies}, ${t.settings.minInSync ?? 1} in sync`
|
|
232
|
+
: `the broker's${t.copiesHeld ? ` (${t.copiesHeld})` : ""}` }), _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": "project-history", children: [_jsx("h2", { id: "project-history", children: "Topic history" }), topicHistory.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No topic has been declared, changed or removed yet." })) : (_jsx("ul", { className: "ac-topics", children: topicHistory.map((h, i) => (_jsxs("li", { "data-history": h.kind, "data-topic": h.topic, children: [h.at ? `${h.at} ` : "", _jsx("code", { children: h.topic }), " ", h.kind.replace("topic-", ""), " by ", h.by ?? "the platform", h.changes.length > 0 ? `: ${h.changes.map((c) => `${c.setting} ${c.from} → ${c.to}`).join("; ")}` : ""] }, i))) }))] }), _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" })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "database-title", id: "database", children: [_jsx("h2", { id: "database-title", children: "Database" }), !backups ? (_jsx("p", { className: "ac-empty", children: "The control plane could not say how the project's database is backed up." })) : (_jsxs(_Fragment, { children: [_jsxs("p", { "data-backed-up": backups.backedUp ? "yes" : "no", children: [backups.backedUp ? "Backed up." : "Not backed up.", backups.detail ? ` ${backups.detail}` : ""] }), backups.database ? (_jsxs("p", { children: [backups.database.readyInstances, " of ", backups.database.instances, " instances ready", backups.database.primary ? `, primary ${backups.database.primary}` : "", backups.database.synchronous ? ", synchronous" : "", backups.database.writesWaitingOn ? ` — ${backups.database.writesWaitingOn}` : ""] })) : null, backups.lines.length > 0 ? (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-label": "Lines of history", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Line" }), _jsx("th", { scope: "col", children: "Backups" }), _jsx("th", { scope: "col", children: "Last base backup" }), _jsx("th", { scope: "col", children: "Restorable from" }), _jsx("th", { scope: "col", children: "To" })] }) }), _jsx("tbody", { children: backups.lines.map((l) => (_jsxs("tr", { "data-line": l.line, children: [_jsx("td", { children: _jsx("code", { children: l.line }) }), _jsxs("td", { children: [l.phase, l.failing ? ` — ${l.failing}` : ""] }), _jsx("td", { children: l.lastBaseBackup ? when(l.lastBaseBackup) : "—" }), _jsx("td", { children: l.firstRestorable ? when(l.firstRestorable) : "—" }), _jsx("td", { children: l.lastRestorable ? when(l.lastRestorable) : "—" })] }, l.line))) })] }) })) : null, backups.clusters.length > 0 ? (_jsx("ul", { className: "ac-topics", "aria-label": "Database clusters", children: backups.clusters.map((c) => (_jsxs("li", { "data-cluster": c.name, "data-phase": c.phase, children: [_jsx("code", { children: c.name }), " (", c.phase, c.leftAt ? `, left ${when(c.leftAt)}` : "", c.since ? `, since ${when(c.since)}` : "", "): ", c.services.length === 0 ? "no service" : c.services.join(", ")] }, c.name))) })) : null, restores.map((r) => (_jsxs("div", { className: "ac-card", id: `restore-${r.name}`, "data-restore": r.name, "data-phase": r.phase, children: [_jsxs("h3", { children: ["Restore ", _jsx("code", { children: r.name }), " of ", r.line, " at ", when(r.moment), ": ", r.phase] }), r.detail ? _jsx("p", { children: r.detail }) : null, r.services.length > 0 ? (_jsx("ul", { className: "ac-topics", children: r.services.map((v) => (_jsxs("li", { "data-verified": v.name, children: [v.name, ": ", v.present ? `${v.journalRows} journal rows, highest sequence ${v.highestSequence}` : "no database at the moment", v.changedSecrets.length > 0 ? `; secrets changed since: ${v.changedSecrets.join(", ")}` : "", v.present && (r.phase === "Verified" || r.phase === "InUse") && shows("service.switch") ? (_jsxs(ConsoleForm, { intent: "switch", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "service", value: v.name }), _jsx("input", { type: "hidden", name: "cluster", value: r.name }), _jsx(Submit, { intent: "switch", children: `Switch ${v.name} to ${r.name}` })] })) : null] }, v.name))) })) : null, r.broker.length > 0 ? (_jsx("ul", { className: "ac-topics", "aria-label": `What ${r.name} cannot take back`, children: r.broker.map((d) => (_jsxs("li", { "data-diverged": d.topic, children: [_jsx("code", { children: d.topic }), d.group ? ` read by ${d.group}: ${d.read ?? 0} of` : ":", " ", d.after, " messages newer than the moment"] }, `${d.topic}/${d.group ?? ""}`))) })) : null, r.note ? _jsx("p", { className: "ac-hint", children: r.note }) : null] }, r.name))), _jsx(Refused, { intent: "switch" })] })), setting ? (_jsxs("p", { "data-setting": `${setting.replicas}`, children: ["Asks for ", setting.replicas === 0 ? "a primary alone" : `${setting.replicas} replica${setting.replicas === 1 ? "" : "s"}`, setting.synchronous ? ", synchronous" : "", setting.retentionDays ? `, backups kept ${setting.retentionDays} days` : "", setting.rehearse ? `, rehearsed ${setting.rehearse}` : "", "."] })) : null, shows("project.database") ? (_jsxs("details", { className: "ac-more", open: databaseRefusal !== undefined || undefined, children: [_jsx("summary", { children: "Replicas, retention and rehearsals" }), _jsxs(ConsoleForm, { intent: "database", className: "ac-form", children: [_jsx(Field, { label: "Replicas", name: "replicas", type: "number", defaultValue: databaseRefusal?.values.replicas ?? String(setting?.replicas ?? 0), hint: "Beside the primary, 0 to 4; one takes its place if it is lost." }), _jsxs("div", { className: "ac-field", children: [_jsxs("label", { htmlFor: "synchronous", children: [_jsx("input", { id: "synchronous", name: "synchronous", type: "checkbox", defaultChecked: setting?.synchronous }), " Synchronous"] }), _jsx("p", { className: "ac-hint", children: "Every write waits until a replica holds it, so none is lost with the primary." })] }), _jsx(Field, { label: "Keep backups for (days)", name: "retentionDays", type: "number", defaultValue: databaseRefusal?.values.retentionDays ?? (setting?.retentionDays ? String(setting.retentionDays) : "") }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "rehearse", children: "Rehearse a restore" }), _jsxs("select", { id: "rehearse", name: "rehearse", defaultValue: setting?.rehearse ?? "", children: [_jsx("option", { value: "", children: "never" }), _jsx("option", { value: "daily", children: "daily" }), _jsx("option", { value: "weekly", children: "weekly" })] })] }), _jsx(Refused, { intent: "database" }), _jsx("div", { children: _jsx(Submit, { intent: "database", children: "Save" }) })] })] })) : null, shows("project.backup-credential") ? (_jsxs(ConsoleForm, { intent: "reissue", className: "ac-inline", children: [_jsx(Submit, { intent: "reissue", children: "Issue the backup credential again" }), _jsx(Refused, { intent: "reissue" })] })) : null, shows("project.restore") ? (_jsxs("details", { className: "ac-more", children: [_jsx("summary", { children: "Restore to a moment" }), _jsxs(ConsoleForm, { intent: "restore", className: "ac-form", children: [_jsx(Field, { label: "Moment", name: "moment", type: "datetime-local", required: true, hint: "A new cluster beside the current one; nothing in the current database changes. Owners only." }), _jsx(Field, { label: "Line", name: "line", placeholder: "ankka-db", hint: "Only when the project's services are on more than one cluster." }), _jsx(Refused, { intent: "restore" }), _jsx("div", { children: _jsx(Submit, { intent: "restore", children: "Restore" }) })] })] })) : null] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "rehearsals-title", id: "rehearsals", children: [_jsx("h2", { id: "rehearsals-title", children: "Rehearsals" }), rehearsals.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No rehearsals yet. A rehearsal restores into a namespace of its own, checks it, times it and removes it." })) : (_jsx("ul", { className: "ac-topics", children: rehearsals.map((r) => (_jsxs("li", { "data-rehearsal": r.name, "data-outcome": r.outcome, children: [_jsx("code", { children: r.name }), " to ", when(r.moment), ": ", r.outcome, r.elapsedSeconds !== undefined ? `, in ${r.elapsedSeconds}s` : "", r.requestedBy ? `, asked by ${r.requestedBy}` : ", on the schedule", r.detail ? ` — ${r.detail}` : ""] }, r.name))) })), shows("project.rehearse") ? (_jsx(ConsoleForm, { intent: "rehearse", className: "ac-inline", children: _jsx(Submit, { intent: "rehearse", children: "Rehearse a restore" }) })) : null, _jsx(Refused, { intent: "rehearse" })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "database-history", children: [_jsx("h2", { id: "database-history", children: "Database history" }), databaseHistory.length === 0 ? (_jsx("p", { className: "ac-empty", children: "Nothing has been done to the project's database yet." })) : (_jsx("ul", { className: "ac-topics", children: databaseHistory.map((h, i) => (_jsxs("li", { "data-history": h.kind, children: [h.kind, h.by ? ` by ${h.by}` : "", h.at ? `, ${when(h.at)}` : "", h.detail ? `: ${h.detail}` : ""] }, i))) }))] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "grants", children: [_jsx("h2", { id: "grants", children: "Grants" }), grants.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No grants. A grant lets a service of another project, or a machine, reach one route, method or topic of this project." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "grants", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Grantee" }), _jsx("th", { scope: "col", children: "Target" }), _jsx("th", { scope: "col", children: "State" }), _jsx("th", { scope: "col", children: "Effect" }), _jsx("th", { scope: "col", children: "Granted" }), _jsx("th", { scope: "col", children: _jsx("span", { className: "ac-visually-hidden", children: "Actions" }) })] }) }), _jsx("tbody", { children: grants.map((g) => (_jsxs("tr", { "data-grant": g.id, "data-state": g.state, children: [_jsx("td", { children: _jsx("code", { children: g.grantee }) }), _jsx("td", { children: targetText(g.target) }), _jsx("td", { children: g.state }), _jsx("td", { "data-effect": true, children: g.effect }), _jsxs("td", { children: [g.granted.at ? when(g.granted.at) : "—", g.granted.by ? ` by ${g.granted.by}` : "", g.ended ? (_jsxs("span", { className: "ac-hint", children: [" ", "\u00B7 ended ", g.ended.at ? when(g.ended.at) : "", g.ended.by ? ` by ${g.ended.by}` : ""] })) : null] }), _jsx("td", { children: shows("grant.end") && (g.state === "pending" || g.state === "accepted") ? (_jsxs(ConsoleForm, { intent: "grant-end", className: "ac-inline", children: [_jsx("input", { type: "hidden", name: "grantId", value: g.id }), _jsx(Submit, { intent: "grant-end", label: `${endVerb(g.state)} the grant to ${g.grantee} of ${targetText(g.target)}`, children: endVerb(g.state) })] })) : null })] }, g.id))) })] }) })), _jsx(Refused, { intent: "grant-end" })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "received", children: [_jsx("h2", { id: "received", children: "Received" }), received.length === 0 ? (_jsx("p", { className: "ac-empty", children: "No other project grants this project's services anything." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-describedby": "received", 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: "Changes" })] }) }), _jsx("tbody", { children: received.map((r) => (_jsxs("tr", { "data-received": r.id, "data-state": r.state, children: [_jsxs("td", { children: [r.grantingProject, " ", _jsxs("span", { className: "ac-hint", children: ["of ", r.grantingOrganization] })] }), _jsx("td", { children: _jsx("code", { children: r.grantee }) }), _jsxs("td", { children: [targetText(r.target), r.topic ? (_jsxs("span", { className: "ac-hint", children: [" ", "\u00B7 ", r.topic.partitions, " partitions", r.topic.compacted ? ", compacted" : "", r.topic.retention ? `, kept ${r.topic.retention}` : ""] })) : null] }), _jsx("td", { children: r.state }), _jsx("td", { children: _jsx("ul", { className: "ac-topics", children: r.changes.map((c, i) => (_jsxs("li", { children: [c.change, c.at ? ` ${when(c.at)}` : "", c.by ? ` by ${c.by}` : ""] }, `${c.change}-${i}`))) }) })] }, r.id))) })] }) }))] }), _jsx(Panels, { kind: "project", entity: p, loaded: panels })] }));
|
|
60
233
|
}
|
|
61
234
|
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;
|
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Applies a descriptor: pasted,
|
|
4
|
-
*
|
|
3
|
+
* Applies a descriptor: pasted, read from a file, or started from the one a service applied at an
|
|
4
|
+
* earlier generation (`?name=cart&generation=1`). The console checks only that the text is JSON and
|
|
5
|
+
* names a service; every rule about what a descriptor may say is the control plane's, and its
|
|
5
6
|
* refusal is shown problem by problem beside the text.
|
|
6
7
|
*/
|
|
7
8
|
import { redirect, useLoaderData } from "react-router";
|
|
8
|
-
import { act, guard, pageData, useConsoleContext } from "../context.js";
|
|
9
|
+
import { act, guard, pageData, projectShell, useConsoleContext } from "../context.js";
|
|
9
10
|
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
10
|
-
import {
|
|
11
|
+
import { ConsoleForm, Submit } from "../ui/console.js";
|
|
12
|
+
import { Page } from "../ui/shell.js";
|
|
11
13
|
import { Refused, useRefusal } from "../ui/refused.js";
|
|
12
14
|
import { data } from "react-router";
|
|
13
15
|
export const meta = () => [{ title: "Apply a descriptor · ankka" }];
|
|
14
16
|
export async function loader({ request, params, context }) {
|
|
15
17
|
const ctx = useConsoleContext(context);
|
|
16
18
|
return guard(ctx, async () => {
|
|
17
|
-
const project = await ctx.client.getProject(params.projectId);
|
|
19
|
+
const [project, services, page] = await Promise.all([ctx.client.getProject(params.projectId), ctx.client.listServices(params.projectId), pageData(ctx)]);
|
|
18
20
|
const organization = await ctx.client.getOrganization(project.organizationId);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
+
const query = new URL(request.url).searchParams;
|
|
22
|
+
const name = query.get("name");
|
|
23
|
+
const generation = query.get("generation");
|
|
24
|
+
const from = name && generation ? JSON.stringify(await ctx.client.descriptor(project.id, name, Number(generation)), null, 2) : undefined;
|
|
25
|
+
const tail = name
|
|
26
|
+
? [{ label: name, to: `projects/${encodeURIComponent(project.id)}/services/${encodeURIComponent(name)}` }, { label: "Apply a descriptor" }]
|
|
27
|
+
: [{ label: "Apply a descriptor" }];
|
|
28
|
+
return { console: projectShell(page, organization, project, services, tail, undefined, name ?? undefined), project, organization, name, generation, from };
|
|
21
29
|
});
|
|
22
30
|
}
|
|
23
31
|
const notApplied = (reason, descriptor) => data({ intent: "apply", status: 400, reason, problems: [reason], values: { descriptor } }, { status: 400 });
|
|
@@ -53,13 +61,8 @@ const example = `{
|
|
|
53
61
|
}
|
|
54
62
|
}`;
|
|
55
63
|
export default function ApplyDescriptor() {
|
|
56
|
-
const {
|
|
64
|
+
const { name, generation, from } = useLoaderData();
|
|
57
65
|
const refusal = useRefusal("apply");
|
|
58
|
-
return (_jsxs("
|
|
59
|
-
{ label: "Organizations", to: "" },
|
|
60
|
-
{ label: o.name, to: `organizations/${encodeURIComponent(o.id)}` },
|
|
61
|
-
{ label: p.name, to: `projects/${encodeURIComponent(p.id)}` },
|
|
62
|
-
{ label: "Apply a descriptor" },
|
|
63
|
-
] }), _jsxs("h1", { children: ["Apply a descriptor", name ? ` to ${name}` : ""] }), _jsxs("p", { className: "ac-lede", children: ["A descriptor is a service's ", _jsx("code", { children: "service.json" }), ". Applying it creates the service, or updates it if one with that name exists."] }), _jsxs(ConsoleForm, { intent: "apply", encType: "multipart/form-data", className: "ac-form", children: [_jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "descriptor", children: "Descriptor" }), _jsx("textarea", { id: "descriptor", name: "descriptor", spellCheck: false, placeholder: example, defaultValue: refusal?.values.descriptor, "aria-describedby": "descriptor-hint", "aria-invalid": refusal ? true : undefined }), _jsx("p", { className: "ac-hint", id: "descriptor-hint", children: "Paste it here, or choose the file below instead." })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "file", children: "Or a service.json file" }), _jsx("input", { id: "file", name: "file", type: "file", accept: "application/json,.json" })] }), _jsx(Refused, { intent: "apply" }), _jsx("div", { children: _jsx(Submit, { intent: "apply", children: "Apply" }) })] })] }));
|
|
66
|
+
return (_jsxs(Page, { children: [_jsxs("h1", { children: ["Apply a descriptor", name ? ` to ${name}` : ""] }), _jsxs("p", { className: "ac-lede", children: ["A descriptor is a service's ", _jsx("code", { children: "service.json" }), ". Applying it creates the service, or updates it if one with that name exists."] }), from ? _jsxs("p", { className: "ac-notice", children: ["This is the descriptor ", name, " applied at generation ", generation, ". Applying it, changed or not, is a new generation."] }) : null, _jsxs(ConsoleForm, { intent: "apply", encType: "multipart/form-data", className: "ac-card ac-form", children: [_jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "descriptor", children: "Descriptor" }), _jsx("textarea", { id: "descriptor", name: "descriptor", spellCheck: false, placeholder: example, defaultValue: refusal?.values.descriptor ?? from, "aria-describedby": "descriptor-hint", "aria-invalid": refusal ? true : undefined }), _jsx("p", { className: "ac-hint", id: "descriptor-hint", children: "Paste it here, or choose the file below instead." })] }), _jsxs("div", { className: "ac-field", children: [_jsx("label", { htmlFor: "file", children: "Or a service.json file" }), _jsx("input", { id: "file", name: "file", type: "file", accept: "application/json,.json" })] }), _jsx(Refused, { intent: "apply" }), _jsx("div", { children: _jsx(Submit, { intent: "apply", primary: true, children: "Apply" }) })] })] }));
|
|
64
67
|
}
|
|
65
68
|
export const ErrorBoundary = ConsoleErrorBoundary;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A service's history: every change its members made, newest first, each with the generation it
|
|
3
|
+
* produced and who made it, and for each that recorded a descriptor the image it ran and a digest
|
|
4
|
+
* of it. A row whose descriptor differs from the service's own offers a rollback to it. A section
|
|
5
|
+
* of the service, beside its overview, topology and logs.
|
|
6
|
+
*/
|
|
7
|
+
import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
|
|
8
|
+
import { ConsoleErrorBoundary } from "../ui/errors.tsx";
|
|
9
|
+
export declare const meta: MetaFunction<typeof loader>;
|
|
10
|
+
export declare function loader({ params, context }: LoaderFunctionArgs): Promise<{
|
|
11
|
+
console: import("../context.ts").ConsolePageData;
|
|
12
|
+
name: string;
|
|
13
|
+
projectId: string;
|
|
14
|
+
history: {
|
|
15
|
+
kind: string;
|
|
16
|
+
generation: number;
|
|
17
|
+
actor: {
|
|
18
|
+
subject: string;
|
|
19
|
+
display: string | undefined;
|
|
20
|
+
administrative: boolean;
|
|
21
|
+
} | undefined;
|
|
22
|
+
at: string | undefined;
|
|
23
|
+
image: string | undefined;
|
|
24
|
+
digest: string | undefined;
|
|
25
|
+
hostname: string | undefined;
|
|
26
|
+
rolledBackTo: number | undefined;
|
|
27
|
+
detail: string | undefined;
|
|
28
|
+
}[];
|
|
29
|
+
}>;
|
|
30
|
+
export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
|
|
31
|
+
export default function ServiceHistory(): import("react").JSX.Element;
|
|
32
|
+
export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A service's history: every change its members made, newest first, each with the generation it
|
|
4
|
+
* produced and who made it, and for each that recorded a descriptor the image it ran and a digest
|
|
5
|
+
* of it. A row whose descriptor differs from the service's own offers a rollback to it. A section
|
|
6
|
+
* of the service, beside its overview, topology and logs.
|
|
7
|
+
*/
|
|
8
|
+
import { redirect, useLoaderData } from "react-router";
|
|
9
|
+
import { act, applyPrimary, guard, pageData, projectShell, text, useConsoleContext } from "../context.js";
|
|
10
|
+
import { ConsoleErrorBoundary } from "../ui/errors.js";
|
|
11
|
+
import { ConsoleForm, ConsoleLink, Submit, useConsole, when } from "../ui/console.js";
|
|
12
|
+
import { Refused } from "../ui/refused.js";
|
|
13
|
+
import { HostActions } from "../extensions/render.js";
|
|
14
|
+
import { Page, ServiceSections } from "../ui/shell.js";
|
|
15
|
+
export const meta = ({ loaderData }) => [{ title: `History of ${loaderData?.name ?? "a service"} · ankka` }];
|
|
16
|
+
export async function loader({ params, context }) {
|
|
17
|
+
const ctx = useConsoleContext(context);
|
|
18
|
+
const { projectId, name } = params;
|
|
19
|
+
return guard(ctx, async () => {
|
|
20
|
+
const [history, project, services, page] = await Promise.all([
|
|
21
|
+
ctx.client.history(projectId, name),
|
|
22
|
+
ctx.client.getProject(projectId),
|
|
23
|
+
ctx.client.listServices(projectId),
|
|
24
|
+
pageData(ctx),
|
|
25
|
+
]);
|
|
26
|
+
const organization = await ctx.client.getOrganization(project.organizationId);
|
|
27
|
+
return {
|
|
28
|
+
console: projectShell(page, organization, project, services, [{ label: name, to: `projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(name)}` }, { label: "History" }], applyPrimary(projectId, name), name),
|
|
29
|
+
name,
|
|
30
|
+
projectId,
|
|
31
|
+
history: [...history].reverse(),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function action({ request, params, context }) {
|
|
36
|
+
const ctx = useConsoleContext(context);
|
|
37
|
+
const { projectId, name } = params;
|
|
38
|
+
const form = await request.formData();
|
|
39
|
+
const intent = text(form, "intent");
|
|
40
|
+
return act(ctx, intent, form, async () => {
|
|
41
|
+
if (intent !== "rollback")
|
|
42
|
+
throw new Response(`unknown operation '${intent}'`, { status: 400 });
|
|
43
|
+
await ctx.client.rollback(projectId, name, Number(text(form, "generation")));
|
|
44
|
+
return redirect(ctx.href(`projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(name)}/history`));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The generations a rollback can be offered at: each whose entry recorded a descriptor different
|
|
49
|
+
* from the one the service has now. The newest entry that recorded one is the service's own, since
|
|
50
|
+
* only an apply or a rollback changes it. An entry recorded before digests were kept offers
|
|
51
|
+
* nothing, though the CLI can still roll back to it; the control plane decides either way.
|
|
52
|
+
*/
|
|
53
|
+
function rollbackTargets(history) {
|
|
54
|
+
const recorded = history.filter((h) => h.digest !== undefined);
|
|
55
|
+
if (recorded.length === 0)
|
|
56
|
+
return new Set();
|
|
57
|
+
const current = recorded.reduce((a, b) => (b.generation > a.generation ? b : a)).digest;
|
|
58
|
+
return new Set(recorded.filter((h) => h.digest !== current).map((h) => h.generation));
|
|
59
|
+
}
|
|
60
|
+
function RollBack({ generation, image, entity }) {
|
|
61
|
+
const { shows } = useConsole();
|
|
62
|
+
return (_jsxs(_Fragment, { children: [shows("service.rollback") ? (_jsxs("details", { className: "ac-more", children: [_jsx("summary", { children: "Roll back" }), _jsxs("p", { children: ["Apply generation ", generation, "'s descriptor again", image ? ` (image ${image})` : "", " as a new generation."] }), _jsxs(ConsoleForm, { intent: "rollback", children: [_jsx("input", { type: "hidden", name: "generation", value: generation }), _jsxs(Submit, { intent: "rollback", children: ["Roll back to generation ", generation] })] })] })) : null, _jsx(HostActions, { operation: "service.rollback", entity: entity })] }));
|
|
63
|
+
}
|
|
64
|
+
const words = {
|
|
65
|
+
applied: "Applied",
|
|
66
|
+
"rolled-back": "Rolled back",
|
|
67
|
+
restarted: "Restarted",
|
|
68
|
+
paused: "Paused",
|
|
69
|
+
resumed: "Resumed",
|
|
70
|
+
exposed: "Exposed",
|
|
71
|
+
unexposed: "Unexposed",
|
|
72
|
+
deleted: "Deleted",
|
|
73
|
+
"storage-credential-reissued": "Storage credential issued again",
|
|
74
|
+
"storage-moved": "Bucket move asked for",
|
|
75
|
+
"storage-settings-reapplied": "Bucket settings reapplied",
|
|
76
|
+
};
|
|
77
|
+
export default function ServiceHistory() {
|
|
78
|
+
const { name, projectId, history } = useLoaderData();
|
|
79
|
+
const offered = rollbackTargets(history);
|
|
80
|
+
return (_jsxs(Page, { children: [_jsxs("div", { className: "ac-head", children: [_jsxs("h1", { children: ["History of ", name] }), _jsx(ServiceSections, { projectId: projectId, name: name, current: "history" })] }), _jsxs("section", { className: "ac-card", "aria-labelledby": "history", children: [_jsx("h2", { id: "history", children: "History" }), history.length === 0 ? (_jsx("p", { className: "ac-empty", children: "Nothing recorded yet." })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "What" }), _jsx("th", { scope: "col", className: "ac-num", children: "Generation" }), _jsx("th", { scope: "col", children: "Image" }), _jsx("th", { scope: "col", children: "Digest" }), _jsx("th", { scope: "col", children: "Who" }), _jsx("th", { scope: "col", children: "When" }), _jsx("th", { scope: "col", "aria-label": "Roll back" })] }) }), _jsx("tbody", { children: history.map((h, i) => (_jsxs("tr", { children: [_jsx("td", { children: h.rolledBackTo !== undefined ? `Rolled back to generation ${h.rolledBackTo}` : (words[h.kind] ?? h.kind) }), _jsx("td", { className: "ac-num", children: h.generation }), _jsx("td", { children: h.image ?? "—" }), _jsx("td", { children: h.digest ? (_jsx(ConsoleLink, { to: `projects/${encodeURIComponent(projectId)}/services/apply?name=${encodeURIComponent(name)}&generation=${h.generation}`, "aria-label": `Generation ${h.generation}'s descriptor`, children: _jsx("code", { title: h.digest, children: h.digest.slice(0, 12) }) })) : ("—") }), _jsxs("td", { children: [h.actor ? (h.actor.display ?? h.actor.subject) : "—", h.actor?.administrative ? " (as administrator)" : ""] }), _jsx("td", { children: when(h.at) }), _jsx("td", { children: offered.has(h.generation) ? _jsx(RollBack, { generation: h.generation, image: h.image, entity: { name, projectId, entry: h } }) : null })] }, i))) })] }) })), _jsx(Refused, { intent: "rollback" })] })] }));
|
|
81
|
+
}
|
|
82
|
+
export const ErrorBoundary = ConsoleErrorBoundary;
|