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.
- package/dist/client/control-plane.d.ts +63 -1
- package/dist/client/control-plane.js +142 -1
- package/dist/client/schemas.d.ts +1312 -2
- package/dist/client/schemas.js +423 -2
- package/dist/context.d.ts +1 -1
- package/dist/context.js +1 -1
- package/dist/extensions/types.d.ts +1 -1
- package/dist/extensions/types.js +22 -0
- package/dist/routes/front.d.ts +62 -2
- package/dist/routes/front.js +37 -8
- package/dist/routes/logs.d.ts +57 -0
- package/dist/routes/machines.d.ts +78 -0
- package/dist/routes/machines.js +78 -0
- package/dist/routes/organization.d.ts +29 -1
- package/dist/routes/organization.js +27 -5
- package/dist/routes/project.d.ts +275 -1
- package/dist/routes/project.js +107 -6
- package/dist/routes/service-history.d.ts +2 -0
- package/dist/routes/service-history.js +3 -0
- package/dist/routes/service-topology.d.ts +57 -0
- package/dist/routes/service.d.ts +57 -0
- package/dist/routes/service.js +63 -4
- package/dist/routes.js +1 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +1 -0
- package/dist/testing/fake-control-plane.d.ts +179 -1
- package/dist/testing/fake-control-plane.js +819 -7
- package/dist/ui/grants.d.ts +3 -0
- package/dist/ui/grants.js +13 -0
- package/dist/ui/shell.d.ts +1 -1
- package/dist/ui/shell.js +9 -2
- package/dist/ui/use-stream.d.ts +114 -0
- package/package.json +1 -1
package/dist/routes/project.js
CHANGED
|
@@ -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 —
|
|
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:
|
|
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",
|
|
131
|
-
|
|
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;
|
package/dist/routes/service.d.ts
CHANGED
|
@@ -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;
|