ankka-console 0.11.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.
@@ -1,7 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
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
+ * 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.
5
6
  */
6
7
  import { data, redirect, useActionData, useLoaderData } from "react-router";
7
8
  import { ControlPlaneError } from "../client/errors.js";
@@ -12,20 +13,32 @@ import { Page, SectionTitle } from "../ui/shell.js";
12
13
  import { Refused, useRefusal } from "../ui/refused.js";
13
14
  import { Lifecycle } from "../ui/status.js";
14
15
  import { useProjectStream } from "../ui/use-stream.js";
16
+ import { targetText } from "../ui/grants.js";
15
17
  import { HostActions, loadPanels, Panels } from "../extensions/render.js";
16
18
  export const meta = ({ loaderData }) => [{ title: `${loaderData?.project.name ?? "Project"} · ankka` }];
17
19
  export async function loader({ params, context }) {
18
20
  const ctx = useConsoleContext(context);
19
21
  const id = params.projectId;
20
22
  return guard(ctx, async () => {
21
- const [project, services, secrets, topics, brokers, page] = await Promise.all([
23
+ const [project, services, secrets, topics, brokers, grants, received, page, backups, rehearsals, restores, history, setting] = await Promise.all([
22
24
  ctx.client.getProject(id),
23
25
  ctx.client.listServices(id),
24
26
  ctx.client.listProjectSecrets(id),
25
27
  ctx.client.listTopics(id),
26
28
  ctx.client.listBrokers(id),
29
+ ctx.client.grants(id),
30
+ ctx.client.receivedGrants(id),
27
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),
28
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)));
29
42
  const organization = await ctx.client.getOrganization(project.organizationId);
30
43
  const primary = { label: "Apply a descriptor", to: `projects/${encodeURIComponent(id)}/services/apply`, operation: "service.apply" };
31
44
  return {
@@ -36,6 +49,13 @@ export async function loader({ params, context }) {
36
49
  secrets,
37
50
  topics,
38
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,
39
59
  panels: await loadPanels(ctx, "project", project),
40
60
  };
41
61
  });
@@ -51,6 +71,27 @@ export async function action({ request, params, context }) {
51
71
  case "rename":
52
72
  await ctx.client.renameProject(id, text(form, "name"));
53
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`);
54
95
  case "delete": {
55
96
  const project = await ctx.client.getProject(id);
56
97
  await ctx.client.deleteProject(id);
@@ -90,10 +131,23 @@ export async function action({ request, params, context }) {
90
131
  }
91
132
  contract = { name: contractName, schema };
92
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);
93
138
  await ctx.client.declareTopic(id, text(form, "topicName"), {
94
- partitions: Number(text(form, "topicPartitions")),
139
+ partitions: count("topicPartitions"),
95
140
  compacted: form.get("topicCompacted") === "on",
96
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"),
97
151
  });
98
152
  return redirect(self);
99
153
  }
@@ -109,13 +163,52 @@ export async function action({ request, params, context }) {
109
163
  case "broker-unset":
110
164
  await ctx.client.removeBroker(id, text(form, "brokerName"));
111
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);
112
179
  default:
113
180
  throw new Response(`unknown operation '${intent}'`, { status: 400 });
114
181
  }
115
182
  });
116
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");
117
206
  export default function Project() {
118
- const { project: p, organization: o, services: initial, secrets, topics, brokers, 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);
119
212
  const { services, state } = useProjectStream(p.id, initial);
120
213
  const { shows } = useConsole();
121
214
  const renameRefusal = useRefusal("rename");
@@ -123,11 +216,19 @@ export default function Project() {
123
216
  const registryRefusal = useRefusal("registry-set");
124
217
  const secretRefusal = useRefusal("secret-set");
125
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];
126
221
  const brokerRefusal = useRefusal("broker-set");
222
+ const locationRefusal = useRefusal("location-set");
223
+ const grantRefusal = useRefusal("grant-make");
127
224
  const shown = useActionData();
128
225
  const shownSchema = shown && shown.intent === "topic-schema" && typeof shown.topic === "string" ? shown : undefined;
129
226
  const path = `projects/${encodeURIComponent(p.id)}`;
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 })] }));
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 })] }));
132
233
  }
133
234
  export const ErrorBoundary = ConsoleErrorBoundary;
@@ -22,7 +22,9 @@ export declare function loader({ params, context }: LoaderFunctionArgs): Promise
22
22
  at: string | undefined;
23
23
  image: string | undefined;
24
24
  digest: string | undefined;
25
+ hostname: string | undefined;
25
26
  rolledBackTo: number | undefined;
27
+ detail: string | undefined;
26
28
  }[];
27
29
  }>;
28
30
  export declare function action({ request, params, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
@@ -70,6 +70,9 @@ const words = {
70
70
  exposed: "Exposed",
71
71
  unexposed: "Unexposed",
72
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",
73
76
  };
74
77
  export default function ServiceHistory() {
75
78
  const { name, projectId, history } = useLoaderData();
@@ -29,6 +29,19 @@ export declare function loader({ params, context }: LoaderFunctionArgs): Promise
29
29
  processPort: number | undefined;
30
30
  broker: string | undefined;
31
31
  undeclaredTopics: string[] | undefined;
32
+ secretStore: {
33
+ backend: string;
34
+ keyRead: boolean;
35
+ move: {
36
+ phase: string;
37
+ outcome: string;
38
+ names: {
39
+ name: string;
40
+ state: string;
41
+ }[];
42
+ detail: string | undefined;
43
+ } | undefined;
44
+ } | undefined;
32
45
  topicChecks: {
33
46
  topic: string | undefined;
34
47
  service: string;
@@ -50,10 +63,54 @@ export declare function loader({ params, context }: LoaderFunctionArgs): Promise
50
63
  contract: string | undefined;
51
64
  lag: number | undefined;
52
65
  failing: string | undefined;
66
+ gap: {
67
+ partitions: {
68
+ partition: number;
69
+ beginning: number;
70
+ earliestRetained: string | undefined;
71
+ }[];
72
+ compacted: boolean;
73
+ gone: boolean;
74
+ readAt: string | undefined;
75
+ } | undefined;
76
+ }[] | undefined;
77
+ warnings: {
78
+ kind: string;
79
+ component: string;
80
+ topic: string;
81
+ message: string;
53
82
  }[] | undefined;
54
83
  objectStorage: string | undefined;
55
84
  bucket: string | undefined;
56
85
  bucketAddress: string | undefined;
86
+ objectStore: string | undefined;
87
+ bucketLocation: string | undefined;
88
+ softDeleteDays: number | undefined;
89
+ storageMove: string | undefined;
90
+ databaseCluster: string | undefined;
91
+ grants: string | undefined;
92
+ crossProjectTopics: {
93
+ project: string;
94
+ topic: string;
95
+ right: string;
96
+ status: string;
97
+ }[] | undefined;
98
+ customHostnames: {
99
+ hostname: string;
100
+ state: string;
101
+ reason: string | undefined;
102
+ record: {
103
+ name: string;
104
+ kind: string;
105
+ value: string;
106
+ } | undefined;
107
+ note: string | undefined;
108
+ }[];
109
+ proofRecord: {
110
+ name: string;
111
+ kind: string;
112
+ value: string;
113
+ } | undefined;
57
114
  };
58
115
  project: {
59
116
  id: string;
@@ -34,6 +34,19 @@ export declare function loader({ params, context }: LoaderFunctionArgs): Promise
34
34
  processPort: number | undefined;
35
35
  broker: string | undefined;
36
36
  undeclaredTopics: string[] | undefined;
37
+ secretStore: {
38
+ backend: string;
39
+ keyRead: boolean;
40
+ move: {
41
+ phase: string;
42
+ outcome: string;
43
+ names: {
44
+ name: string;
45
+ state: string;
46
+ }[];
47
+ detail: string | undefined;
48
+ } | undefined;
49
+ } | undefined;
37
50
  topicChecks: {
38
51
  topic: string | undefined;
39
52
  service: string;
@@ -55,10 +68,54 @@ export declare function loader({ params, context }: LoaderFunctionArgs): Promise
55
68
  contract: string | undefined;
56
69
  lag: number | undefined;
57
70
  failing: string | undefined;
71
+ gap: {
72
+ partitions: {
73
+ partition: number;
74
+ beginning: number;
75
+ earliestRetained: string | undefined;
76
+ }[];
77
+ compacted: boolean;
78
+ gone: boolean;
79
+ readAt: string | undefined;
80
+ } | undefined;
81
+ }[] | undefined;
82
+ warnings: {
83
+ kind: string;
84
+ component: string;
85
+ topic: string;
86
+ message: string;
58
87
  }[] | undefined;
59
88
  objectStorage: string | undefined;
60
89
  bucket: string | undefined;
61
90
  bucketAddress: string | undefined;
91
+ objectStore: string | undefined;
92
+ bucketLocation: string | undefined;
93
+ softDeleteDays: number | undefined;
94
+ storageMove: string | undefined;
95
+ databaseCluster: string | undefined;
96
+ grants: string | undefined;
97
+ crossProjectTopics: {
98
+ project: string;
99
+ topic: string;
100
+ right: string;
101
+ status: string;
102
+ }[] | undefined;
103
+ customHostnames: {
104
+ hostname: string;
105
+ state: string;
106
+ reason: string | undefined;
107
+ record: {
108
+ name: string;
109
+ kind: string;
110
+ value: string;
111
+ } | undefined;
112
+ note: string | undefined;
113
+ }[];
114
+ proofRecord: {
115
+ name: string;
116
+ kind: string;
117
+ value: string;
118
+ } | undefined;
62
119
  };
63
120
  project: {
64
121
  id: string;