ankka-console 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/client/control-plane.d.ts +87 -1
  2. package/dist/client/control-plane.js +201 -1
  3. package/dist/client/schemas.d.ts +1835 -0
  4. package/dist/client/schemas.js +595 -0
  5. package/dist/context.d.ts +98 -0
  6. package/dist/context.js +73 -0
  7. package/dist/extensions/render.d.ts +2 -0
  8. package/dist/extensions/render.js +5 -0
  9. package/dist/extensions/types.d.ts +1 -1
  10. package/dist/extensions/types.js +29 -0
  11. package/dist/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  12. package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
  13. package/dist/index.d.ts +9 -2
  14. package/dist/index.js +7 -1
  15. package/dist/routes/auth.sign-out.js +8 -3
  16. package/dist/routes/front.d.ts +62 -2
  17. package/dist/routes/front.js +45 -10
  18. package/dist/routes/logs.d.ts +92 -0
  19. package/dist/routes/logs.js +15 -14
  20. package/dist/routes/machines.d.ts +78 -0
  21. package/dist/routes/machines.js +78 -0
  22. package/dist/routes/members.js +11 -8
  23. package/dist/routes/organization-new.js +7 -4
  24. package/dist/routes/organization.d.ts +29 -1
  25. package/dist/routes/organization.js +38 -9
  26. package/dist/routes/project-new.js +8 -4
  27. package/dist/routes/project.d.ts +347 -2
  28. package/dist/routes/project.js +182 -9
  29. package/dist/routes/service-apply.d.ts +5 -2
  30. package/dist/routes/service-apply.js +17 -14
  31. package/dist/routes/service-history.d.ts +32 -0
  32. package/dist/routes/service-history.js +82 -0
  33. package/dist/routes/service-topology.d.ts +210 -0
  34. package/dist/routes/service-topology.js +54 -0
  35. package/dist/routes/service.d.ts +91 -10
  36. package/dist/routes/service.js +87 -23
  37. package/dist/routes/stream.service.js +18 -1
  38. package/dist/routes/tokens.js +8 -6
  39. package/dist/routes.js +3 -0
  40. package/dist/runtime.d.ts +3 -1
  41. package/dist/runtime.js +1 -0
  42. package/dist/styles.css +2 -481
  43. package/dist/testing/fake-control-plane.d.ts +334 -1
  44. package/dist/testing/fake-control-plane.js +1194 -15
  45. package/dist/testing/index.d.ts +1 -1
  46. package/dist/testing/scenarios.js +6 -0
  47. package/dist/theme.css +66 -0
  48. package/dist/ui/console.d.ts +19 -6
  49. package/dist/ui/console.js +9 -2
  50. package/dist/ui/errors.js +7 -5
  51. package/dist/ui/grants.d.ts +3 -0
  52. package/dist/ui/grants.js +13 -0
  53. package/dist/ui/hosting.d.ts +11 -0
  54. package/dist/ui/hosting.js +20 -0
  55. package/dist/ui/primitives/button.d.ts +11 -0
  56. package/dist/ui/primitives/button.js +21 -0
  57. package/dist/ui/primitives/select.d.ts +10 -0
  58. package/dist/ui/primitives/select.js +4 -0
  59. package/dist/ui/primitives/switch.d.ts +9 -0
  60. package/dist/ui/primitives/switch.js +4 -0
  61. package/dist/ui/shape.d.ts +4 -0
  62. package/dist/ui/shape.js +36 -0
  63. package/dist/ui/shell.d.ts +76 -0
  64. package/dist/ui/shell.js +114 -0
  65. package/dist/ui/tokens.d.ts +82 -0
  66. package/dist/ui/tokens.js +172 -0
  67. package/dist/ui/topology/Graph.d.ts +13 -0
  68. package/dist/ui/topology/Graph.js +56 -0
  69. package/dist/ui/topology/Table.d.ts +10 -0
  70. package/dist/ui/topology/Table.js +14 -0
  71. package/dist/ui/topology/layout.d.ts +113 -0
  72. package/dist/ui/topology/layout.js +177 -0
  73. package/dist/ui/use-stream.d.ts +251 -1
  74. package/dist/ui/use-stream.js +11 -1
  75. package/package.json +11 -3
package/dist/context.d.ts CHANGED
@@ -69,15 +69,113 @@ export interface ActionRefusal {
69
69
  */
70
70
  export declare function act<T>(ctx: ConsoleContext, intent: string, form: FormData, work: () => Promise<T>): Promise<T | ReturnType<typeof data<ActionRefusal>>>;
71
71
  export declare const text: (form: FormData, name: string) => string;
72
+ /** The rail's areas, in the order it shows them. */
73
+ export declare const areas: readonly ["organizations", "projects", "services", "members", "tokens", "machines"];
74
+ export type Area = (typeof areas)[number];
75
+ export interface Crumb {
76
+ label: string;
77
+ to?: string;
78
+ }
79
+ /** One entry of the shell's listing: a project's service, or an organization's project. */
80
+ export type ListingItem = {
81
+ kind: "service";
82
+ key: string;
83
+ name: string;
84
+ to: string;
85
+ lifecycle: string;
86
+ confirmed: boolean;
87
+ ready: number;
88
+ desired: number;
89
+ } | {
90
+ kind: "project";
91
+ key: string;
92
+ name: string;
93
+ to: string;
94
+ services: number;
95
+ };
96
+ /**
97
+ * What the shell around a page shows: the area the rail marks, where the bar says the member is,
98
+ * the page's primary operation, the organization and project the rail's areas open, and the listing
99
+ * beside the page. A page with no listing (the front page) leaves it out.
100
+ */
101
+ export interface ShellData {
102
+ area?: Area;
103
+ crumbs: Crumb[];
104
+ primary?: {
105
+ label: string;
106
+ to: string;
107
+ operation?: Operation;
108
+ };
109
+ /** `manages`: the member may manage its members and deploy tokens (an owner, or an administrator). */
110
+ organization?: {
111
+ id: string;
112
+ name: string;
113
+ manages: boolean;
114
+ };
115
+ project?: {
116
+ id: string;
117
+ name: string;
118
+ };
119
+ listing?: {
120
+ title: string;
121
+ label: string;
122
+ items: ListingItem[];
123
+ current?: string;
124
+ };
125
+ }
72
126
  /** What every package page's loader data carries for the pages and the host's layout. */
73
127
  export interface ConsolePageData {
74
128
  mount: string;
75
129
  principal: Principal | null;
76
130
  hidden: Operation[];
131
+ shell?: ShellData;
77
132
  }
133
+ /** The listing of a project's services, in name order, marking the one being read. */
134
+ export declare function servicesListing(project: {
135
+ id: string;
136
+ name: string;
137
+ }, services: {
138
+ name: string;
139
+ lifecycle: string;
140
+ confirmed: boolean;
141
+ readyInstances: number;
142
+ desiredInstances: number;
143
+ }[], current?: string): NonNullable<ShellData["listing"]>;
144
+ /** The listing of an organization's projects, in name order, marking the one being read. */
145
+ export declare function projectsListing(organization: {
146
+ id: string;
147
+ name: string;
148
+ }, projects: {
149
+ id: string;
150
+ name: string;
151
+ organizationId: string;
152
+ services: number;
153
+ }[], current?: string): NonNullable<ShellData["listing"]>;
78
154
  /**
79
155
  * The page data every package page returns. It is also what makes a page require a session: nobody
80
156
  * signed in is `SignInRequired`, which `guard` turns into the trip to sign in, even for a page whose
81
157
  * loader asks the control plane nothing.
82
158
  */
83
159
  export declare function pageData(ctx: ConsoleContext): Promise<ConsolePageData>;
160
+ /** A page's data with the shell around it; loaders call it once everything the shell names is read. */
161
+ export declare function withShell(page: ConsolePageData, shell: ShellData): ConsolePageData;
162
+ type OrganizationLike = {
163
+ id: string;
164
+ name: string;
165
+ role?: "owner" | "member" | null;
166
+ };
167
+ /** The shell of a page that belongs to an organization: its projects beside the page. */
168
+ export declare function organizationShell(page: ConsolePageData, o: OrganizationLike, projects: {
169
+ id: string;
170
+ name: string;
171
+ organizationId: string;
172
+ services: number;
173
+ }[], area: Area, tail: Crumb[], primary?: ShellData["primary"]): ConsolePageData;
174
+ /** The shell of a page that belongs to a project: its services beside the page. */
175
+ export declare function projectShell(page: ConsolePageData, o: OrganizationLike, p: {
176
+ id: string;
177
+ name: string;
178
+ }, services: Parameters<typeof servicesListing>[1], tail: Crumb[], primary?: ShellData["primary"], current?: string): ConsolePageData;
179
+ /** A service's pages put applying a new descriptor first. */
180
+ export declare function applyPrimary(projectId: string, name: string): NonNullable<ShellData["primary"]>;
181
+ export {};
package/dist/context.js CHANGED
@@ -143,6 +143,40 @@ export const text = (form, name) => {
143
143
  const v = form.get(name);
144
144
  return typeof v === "string" ? v.trim() : "";
145
145
  };
146
+ /** The rail's areas, in the order it shows them. */
147
+ export const areas = ["organizations", "projects", "services", "members", "tokens", "machines"];
148
+ const byName = (a, b) => a.name.localeCompare(b.name);
149
+ /** The listing of a project's services, in name order, marking the one being read. */
150
+ export function servicesListing(project, services, current) {
151
+ const base = `projects/${encodeURIComponent(project.id)}/services/`;
152
+ return {
153
+ title: project.name,
154
+ label: `Services in ${project.name}`,
155
+ current,
156
+ items: [...services].sort(byName).map((s) => ({
157
+ kind: "service",
158
+ key: s.name,
159
+ name: s.name,
160
+ to: base + encodeURIComponent(s.name),
161
+ lifecycle: s.lifecycle,
162
+ confirmed: s.confirmed,
163
+ ready: s.readyInstances,
164
+ desired: s.desiredInstances,
165
+ })),
166
+ };
167
+ }
168
+ /** The listing of an organization's projects, in name order, marking the one being read. */
169
+ export function projectsListing(organization, projects, current) {
170
+ return {
171
+ title: organization.name,
172
+ label: `Projects in ${organization.name}`,
173
+ current,
174
+ items: projects
175
+ .filter((p) => p.organizationId === organization.id)
176
+ .sort(byName)
177
+ .map((p) => ({ kind: "project", key: p.id, name: p.name, to: `projects/${encodeURIComponent(p.id)}`, services: p.services })),
178
+ };
179
+ }
146
180
  /**
147
181
  * The page data every package page returns. It is also what makes a page require a session: nobody
148
182
  * signed in is `SignInRequired`, which `guard` turns into the trip to sign in, even for a page whose
@@ -154,3 +188,42 @@ export async function pageData(ctx) {
154
188
  throw new SignInRequired();
155
189
  return { mount: ctx.mount, principal, hidden: ctx.extensions.hidden ?? [] };
156
190
  }
191
+ /** A page's data with the shell around it; loaders call it once everything the shell names is read. */
192
+ export function withShell(page, shell) {
193
+ return { ...page, shell };
194
+ }
195
+ /** The shell of a page that belongs to an organization: its projects beside the page. */
196
+ export function organizationShell(page, o, projects, area, tail, primary) {
197
+ const orgPath = `organizations/${encodeURIComponent(o.id)}`;
198
+ return withShell(page, {
199
+ area,
200
+ crumbs: [{ label: "Organizations", to: "" }, { label: o.name, to: orgPath }, ...tail],
201
+ primary,
202
+ organization: { id: o.id, name: o.name, manages: o.role === "owner" || (page.principal?.platformAdmin ?? false) },
203
+ listing: projectsListing(o, projects),
204
+ });
205
+ }
206
+ /** The shell of a page that belongs to a project: its services beside the page. */
207
+ export function projectShell(page, o, p, services, tail, primary, current) {
208
+ return withShell(page, {
209
+ area: current === undefined ? "projects" : "services",
210
+ crumbs: [
211
+ { label: "Organizations", to: "" },
212
+ { label: o.name, to: `organizations/${encodeURIComponent(o.id)}` },
213
+ { label: p.name, to: `projects/${encodeURIComponent(p.id)}` },
214
+ ...tail,
215
+ ],
216
+ primary,
217
+ organization: { id: o.id, name: o.name, manages: o.role === "owner" || (page.principal?.platformAdmin ?? false) },
218
+ project: { id: p.id, name: p.name },
219
+ listing: servicesListing(p, services, current),
220
+ });
221
+ }
222
+ /** A service's pages put applying a new descriptor first. */
223
+ export function applyPrimary(projectId, name) {
224
+ return {
225
+ label: "Apply a new descriptor",
226
+ to: `projects/${encodeURIComponent(projectId)}/services/apply?name=${encodeURIComponent(name)}`,
227
+ operation: "service.apply",
228
+ };
229
+ }
@@ -14,6 +14,8 @@ export declare function Panels<K extends PanelKind>({ kind, entity, loaded }: {
14
14
  entity: PanelEntities[K];
15
15
  loaded?: LoadedPanels;
16
16
  }): import("react").JSX.Element | null;
17
+ /** Whether the host added anything beside an operation, so a page knows to make room for it. */
18
+ export declare function useHostActions(operation: Operation): boolean;
17
19
  export declare function HostActions({ operation, entity }: {
18
20
  operation: Operation;
19
21
  entity?: unknown;
@@ -42,6 +42,11 @@ export function Panels({ kind, entity, loaded }) {
42
42
  return (_jsxs("section", { className: "ac-panel", "aria-labelledby": `panel-${p.id}`, "data-panel": p.id, children: [_jsx("h2", { id: `panel-${p.id}`, children: p.title }), result && !result.ok ? (_jsxs("p", { className: "ac-panel-failed", children: [p.title, " could not be loaded: ", result.error] })) : (_jsx(PanelBoundary, { title: p.title, children: _jsx(p.Component, { entity: entity, data: result?.ok ? result.data : undefined }) }))] }, p.id));
43
43
  }) }));
44
44
  }
45
+ /** Whether the host added anything beside an operation, so a page knows to make room for it. */
46
+ export function useHostActions(operation) {
47
+ const { extensions } = useConsole();
48
+ return (extensions.actions?.[operation]?.length ?? 0) > 0;
49
+ }
45
50
  export function HostActions({ operation, entity }) {
46
51
  const { extensions } = useConsole();
47
52
  const actions = extensions.actions?.[operation] ?? [];
@@ -1,7 +1,7 @@
1
1
  import type { ComponentType } from "react";
2
2
  import type { OrganizationSummary, ProjectDetail, ServiceStatus } from "../client/schemas.ts";
3
3
  /** Every operation the console offers, by the name a host uses to add beside it or hide it. */
4
- export declare const operations: readonly ["organization.create", "organization.rename", "organization.delete", "organization.disable", "organization.enable", "organization.quota.set", "organization.quota.clear", "member.invite", "member.role", "member.remove", "invitation.withdraw", "member.repair", "token.create", "token.revoke", "project.create", "project.rename", "project.delete", "registry.set", "registry.clear", "service.apply", "service.pause", "service.resume", "service.restart", "service.expose", "service.unexpose", "service.delete", "service.logs"];
4
+ export declare const operations: readonly ["organization.create", "organization.rename", "organization.delete", "organization.disable", "organization.enable", "organization.quota.set", "organization.quota.clear", "member.invite", "member.role", "member.remove", "invitation.withdraw", "member.repair", "token.create", "token.revoke", "machine.register", "machine.delete", "machine.byte-rates", "project.create", "project.rename", "project.delete", "registry.set", "registry.clear", "project-secret.set", "project-secret.unset", "project-topic.set", "project-topic.unset", "project-broker.set", "project-broker.unset", "project.restore", "project.rehearse", "project.database", "project.backup-credential", "installation.release", "service.switch", "grant.make", "grant.end", "grant.answer", "grant.relinquish", "service.apply", "service.pause", "service.resume", "service.restart", "service.rollback", "service.expose", "service.unexpose", "service-hostname.add", "service-hostname.remove", "service.delete", "service.logs", "service.storage-credential", "service.storage-move", "service.storage-settings", "project-location.set", "project-location.clear"];
5
5
  export type Operation = (typeof operations)[number];
6
6
  export interface PanelEntities {
7
7
  organization: OrganizationSummary;
@@ -14,17 +14,46 @@ export const operations = [
14
14
  "member.repair",
15
15
  "token.create",
16
16
  "token.revoke",
17
+ "machine.register",
18
+ "machine.delete",
19
+ "machine.byte-rates",
17
20
  "project.create",
18
21
  "project.rename",
19
22
  "project.delete",
20
23
  "registry.set",
21
24
  "registry.clear",
25
+ "project-secret.set",
26
+ "project-secret.unset",
27
+ "project-topic.set",
28
+ "project-topic.unset",
29
+ "project-broker.set",
30
+ "project-broker.unset",
31
+ "project.restore",
32
+ "project.rehearse",
33
+ "project.database",
34
+ "project.backup-credential",
35
+ "installation.release",
36
+ "service.switch",
37
+ "grant.make",
38
+ "grant.end",
39
+ // An owner of the grantee's side accepting or declining a pending grant, and giving one up: the
40
+ // organization page's "Offered grants" card.
41
+ "grant.answer",
42
+ "grant.relinquish",
22
43
  "service.apply",
23
44
  "service.pause",
24
45
  "service.resume",
25
46
  "service.restart",
47
+ "service.rollback",
26
48
  "service.expose",
27
49
  "service.unexpose",
50
+ "service-hostname.add",
51
+ "service-hostname.remove",
28
52
  "service.delete",
29
53
  "service.logs",
54
+ "service.storage-credential",
55
+ "service.storage-move",
56
+ "service.storage-settings",
57
+ "project-location.set",
58
+ "project-location.clear",
30
59
  ];
package/dist/index.d.ts CHANGED
@@ -1,5 +1,12 @@
1
- export { ConsoleProvider, useConsole, ConsoleLink, ConsoleForm } from "./ui/console.tsx";
1
+ export { ConsoleProvider, useConsole, ConsoleLink, ConsoleForm, Breadcrumbs, Field, Submit, OperationForm } from "./ui/console.tsx";
2
2
  export type { UseConsole } from "./ui/console.tsx";
3
- export type { Principal } from "./context.ts";
3
+ export { Shell, Backdrop, Bar, Rail, Listing, Inspector, Page, SectionTitle, SegmentedLinks, ServiceSections } from "./ui/shell.tsx";
4
+ export type { SegmentLink, ServiceSection } from "./ui/shell.tsx";
5
+ export { Shape } from "./ui/shape.tsx";
6
+ export { Lifecycle } from "./ui/status.tsx";
7
+ export { button } from "./ui/primitives/button.ts";
8
+ export { Switch } from "./ui/primitives/switch.tsx";
9
+ export { Select } from "./ui/primitives/select.tsx";
10
+ export type { Area, Crumb, ListingItem, Principal, ShellData } from "./context.ts";
4
11
  export { operations } from "./extensions/types.ts";
5
12
  export type { Action, ConsoleExtensions, Operation, Panel, PanelKind, PanelLoadContext } from "./extensions/types.ts";
package/dist/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  // The browser-safe entry point: what a host's layout and its own components use. Everything that
2
2
  // runs only on the server — the routes table, the middleware, sessions, the server itself — is
3
3
  // `ankka-console/server`, so a host's browser bundle never resolves a Node module.
4
- export { ConsoleProvider, useConsole, ConsoleLink, ConsoleForm } from "./ui/console.js";
4
+ export { ConsoleProvider, useConsole, ConsoleLink, ConsoleForm, Breadcrumbs, Field, Submit, OperationForm } from "./ui/console.js";
5
+ export { Shell, Backdrop, Bar, Rail, Listing, Inspector, Page, SectionTitle, SegmentedLinks, ServiceSections } from "./ui/shell.js";
6
+ export { Shape } from "./ui/shape.js";
7
+ export { Lifecycle } from "./ui/status.js";
8
+ export { button } from "./ui/primitives/button.js";
9
+ export { Switch } from "./ui/primitives/switch.js";
10
+ export { Select } from "./ui/primitives/select.js";
5
11
  export { operations } from "./extensions/types.js";
@@ -5,14 +5,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  * than passing silently through single sign-on. Only a POST signs anyone out.
6
6
  */
7
7
  import { redirect, useLoaderData } from "react-router";
8
- import { pageData, useConsoleContext } from "../context.js";
8
+ import { pageData, useConsoleContext, withShell } from "../context.js";
9
9
  import { ConsoleErrorBoundary } from "../ui/errors.js";
10
10
  import { ConsoleForm, ConsoleLink, Submit } from "../ui/console.js";
11
+ import { Page } from "../ui/shell.js";
12
+ import { button } from "../ui/primitives/button.js";
11
13
  export const meta = () => [{ title: "Sign out · ankka" }];
12
14
  export async function loader({ request, context }) {
13
15
  const ctx = useConsoleContext(context);
14
16
  const signedIn = (await ctx.runtime.store.read(request)) !== null;
15
- return { console: await pageData(ctx).catch(() => ({ mount: ctx.mount, principal: null, hidden: [] })), signedIn };
17
+ const page = await pageData(ctx)
18
+ .then((p) => withShell(p, { area: "organizations", crumbs: [{ label: "Organizations", to: "" }, { label: "Sign out" }] }))
19
+ .catch(() => ({ mount: ctx.mount, principal: null, hidden: [] }));
20
+ return { console: page, signedIn };
16
21
  }
17
22
  export async function action({ request, context }) {
18
23
  const ctx = useConsoleContext(context);
@@ -32,6 +37,6 @@ export async function action({ request, context }) {
32
37
  }
33
38
  export default function SignOut() {
34
39
  const { signedIn } = useLoaderData();
35
- return (_jsx("section", { className: "ac-page", children: signedIn ? (_jsxs(_Fragment, { children: [_jsx("h1", { children: "Sign out" }), _jsx(ConsoleForm, { to: "auth/sign-out", children: _jsx(Submit, { children: "Sign out" }) })] })) : (_jsxs(_Fragment, { children: [_jsx("h1", { children: "You are signed out" }), _jsx("p", { children: "Your session in this console and in the identity provider has ended." }), _jsx("p", { children: _jsx(ConsoleLink, { to: "auth/sign-in", className: "ac-button", children: "Sign in" }) })] })) }));
40
+ return (_jsx(Page, { children: signedIn ? (_jsxs(_Fragment, { children: [_jsx("h1", { children: "Sign out" }), _jsxs(ConsoleForm, { to: "auth/sign-out", className: "ac-card", children: [_jsx("p", { children: "Sign out of this console and of the identity provider." }), _jsx("div", { children: _jsx(Submit, { primary: true, children: "Sign out" }) })] })] })) : (_jsxs(_Fragment, { children: [_jsx("h1", { children: "You are signed out" }), _jsx("p", { children: "Your session in this console and in the identity provider has ended." }), _jsx("p", { children: _jsx(ConsoleLink, { to: "auth/sign-in", className: button({ variant: "primary" }), children: "Sign in" }) })] })) }));
36
41
  }
37
42
  export const ErrorBoundary = ConsoleErrorBoundary;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The front page: who you are, as the control plane sees you, and the organizations you can see —
3
- * yours with your role in each, or every one of them for a platform administrator.
3
+ * yours with your role in each, or every one of them for a platform administrator — and what the
4
+ * installation is: its version, and its cloud when it names one (feature 044).
4
5
  */
5
- import { type LoaderFunctionArgs, type MetaFunction } from "react-router";
6
+ import { type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "react-router";
6
7
  import { ConsoleErrorBoundary } from "../ui/errors.tsx";
7
8
  export declare const meta: MetaFunction;
8
9
  export declare function loader({ context }: LoaderFunctionArgs): Promise<{
@@ -36,6 +37,65 @@ export declare function loader({ context }: LoaderFunctionArgs): Promise<{
36
37
  instances: number;
37
38
  };
38
39
  }[];
40
+ installation: {
41
+ platformVersion: string;
42
+ cloud: {
43
+ provider: string;
44
+ account: string;
45
+ location: string;
46
+ kmsKey: string | undefined;
47
+ } | undefined;
48
+ backups: {
49
+ backupTarget: string;
50
+ retentionDays: number;
51
+ copyRequired: boolean;
52
+ sharesFailureDomain: boolean;
53
+ encryption: string;
54
+ controlPlane: {
55
+ line: string;
56
+ cluster: string;
57
+ phase: string;
58
+ lastBaseBackup: string | undefined;
59
+ firstRestorable: string | undefined;
60
+ lastRestorable: string | undefined;
61
+ archiveLagSeconds: number | undefined;
62
+ failing: string | undefined;
63
+ copiedAt: string | undefined;
64
+ } | undefined;
65
+ notBackedUp: string | undefined;
66
+ secondaryStore: {
67
+ lastCompleted: string | undefined;
68
+ lastFailed: string | undefined;
69
+ failure: string | undefined;
70
+ buckets: number | undefined;
71
+ deletedObjects: number | undefined;
72
+ } | undefined;
73
+ } | undefined;
74
+ secrets: {
75
+ backend: string;
76
+ recordRetention: string;
77
+ auditLog: string;
78
+ } | undefined;
79
+ };
80
+ hold: {
81
+ held: boolean;
82
+ restoredAt: string | undefined;
83
+ targetTime: string | undefined;
84
+ releasedAt: string | undefined;
85
+ releasedBy: string | undefined;
86
+ services: {
87
+ project: string;
88
+ service: string;
89
+ recordedGeneration: number | undefined;
90
+ clusterGeneration: number | undefined;
91
+ recordedImage: string | undefined;
92
+ clusterImage: string | undefined;
93
+ }[];
94
+ topics: string[];
95
+ unknownProjects: string[];
96
+ reconciled: string[];
97
+ } | undefined;
39
98
  }>;
99
+ export declare function action({ request, context }: ActionFunctionArgs): Promise<import("react-router").UNSAFE_DataWithResponseInit<import("../context.ts").ActionRefusal> | Response>;
40
100
  export default function Front(): import("react").JSX.Element;
41
101
  export declare const ErrorBoundary: typeof ConsoleErrorBoundary;
@@ -1,25 +1,60 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * The front page: who you are, as the control plane sees you, and the organizations you can see —
4
- * yours with your role in each, or every one of them for a platform administrator.
4
+ * yours with your role in each, or every one of them for a platform administrator — and what the
5
+ * installation is: its version, and its cloud when it names one (feature 044).
5
6
  */
6
- import { useLoaderData } from "react-router";
7
- import { guard, pageData, useConsoleContext } from "../context.js";
7
+ import { redirect, useLoaderData } from "react-router";
8
+ import { act, guard, pageData, text, useConsoleContext, withShell } from "../context.js";
9
+ import { ConsoleForm, Submit, useConsole, when } from "../ui/console.js";
10
+ import { Refused } from "../ui/refused.js";
8
11
  import { ConsoleErrorBoundary } from "../ui/errors.js";
9
- import { ConsoleLink, useConsole } from "../ui/console.js";
10
- import { HostActions } from "../extensions/render.js";
12
+ import { ConsoleLink } from "../ui/console.js";
13
+ import { Page } from "../ui/shell.js";
14
+ import { HostActions, useHostActions } from "../extensions/render.js";
11
15
  export const meta = () => [{ title: "Organizations · ankka" }];
12
16
  export async function loader({ context }) {
13
17
  const ctx = useConsoleContext(context);
14
18
  return guard(ctx, async () => {
15
- const [whoami, organizations] = await Promise.all([ctx.client.whoami(), ctx.client.listOrganizations()]);
16
- return { console: await pageData(ctx), whoami, organizations };
19
+ const [whoami, organizations, installation, hold] = await Promise.all([
20
+ ctx.client.whoami(),
21
+ ctx.client.listOrganizations(),
22
+ ctx.client.installation(),
23
+ // Whether the control plane is held (feature 041); one that cannot say leaves it out.
24
+ ctx.client.restoreHold().catch(() => undefined),
25
+ ]);
26
+ const shell = {
27
+ area: "organizations",
28
+ crumbs: [{ label: "Organizations" }],
29
+ primary: { label: "Create an organization", to: "organizations/new", operation: "organization.create" },
30
+ };
31
+ return { console: withShell(await pageData(ctx), shell), whoami, organizations, installation, hold };
32
+ });
33
+ }
34
+ export async function action({ request, context }) {
35
+ const ctx = useConsoleContext(context);
36
+ const form = await request.formData();
37
+ const intent = text(form, "intent");
38
+ return act(ctx, intent, form, async () => {
39
+ if (intent !== "release")
40
+ throw new Response(`unknown operation '${intent}'`, { status: 400 });
41
+ await ctx.client.releaseRestoreHold();
42
+ return redirect(ctx.href(""));
17
43
  });
18
44
  }
19
45
  const roleWords = { owner: "Owner", member: "Member" };
20
46
  export default function Front() {
21
- const { whoami, organizations } = useLoaderData();
47
+ const { whoami, organizations, installation, hold } = useLoaderData();
48
+ const cloud = installation.cloud ?? null;
49
+ const backups = installation.backups ?? null;
50
+ const hostActions = useHostActions("organization.create");
22
51
  const { shows } = useConsole();
23
- return (_jsxs("section", { className: "ac-page", children: [_jsx("h1", { children: "Organizations" }), _jsxs("p", { className: "ac-lede", children: ["Signed in as ", _jsx("strong", { children: whoami.name ?? whoami.email ?? whoami.subject }), whoami.email && whoami.name ? ` (${whoami.email})` : null, whoami.platformAdmin ? ", a platform administrator: you see every organization in this installation." : "."] }), organizations.length === 0 ? (_jsxs("p", { className: "ac-empty", children: ["You are not a member of any organization yet. Create one, or ask an owner to invite ", whoami.email ?? "you", "."] })) : (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Organizations you can see" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Id" }), _jsx("th", { scope: "col", children: "Your role" }), _jsx("th", { scope: "col", className: "ac-num", children: "Projects" }), _jsx("th", { scope: "col", children: "State" })] }) }), _jsx("tbody", { children: organizations.map((o) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(ConsoleLink, { to: `organizations/${encodeURIComponent(o.id)}`, children: o.name }) }), _jsx("td", { children: o.id }), _jsx("td", { children: o.role ? roleWords[o.role] : "Administrator (not a member)" }), _jsx("td", { className: "ac-num", children: o.projects }), _jsx("td", { children: o.disabled ? "Disabled" : "Active" })] }, o.id))) })] }) })), _jsxs("div", { className: "ac-actions", children: [shows("organization.create") ? (_jsx(ConsoleLink, { to: "organizations/new", className: "ac-button", children: "Create an organization" })) : null, _jsx(HostActions, { operation: "organization.create" })] })] }));
52
+ return (_jsxs(Page, { inspector: hostActions ? (_jsx("div", { className: "ac-ops", children: _jsx(HostActions, { operation: "organization.create" }) })) : null, children: [_jsx("h1", { children: "Organizations" }), _jsxs("p", { className: "ac-lede", children: ["Signed in as ", _jsx("strong", { children: whoami.name ?? whoami.email ?? whoami.subject }), whoami.email && whoami.name ? ` (${whoami.email})` : null, whoami.platformAdmin ? ", a platform administrator: you see every organization in this installation." : "."] }), hold?.held ? (_jsxs("section", { className: "ac-card", "aria-labelledby": "hold-title", "data-held": "yes", children: [_jsx("h2", { id: "hold-title", children: "The control plane is held" }), _jsxs("p", { children: ["Its database was restored to ", hold.targetTime ? when(hold.targetTime) : "an earlier moment", ", and it changes nothing in the cluster until a platform administrator releases it."] }), hold.services.length > 0 ? (_jsx("ul", { className: "ac-topics", "aria-label": "Services that differ", children: hold.services.map((d) => (_jsxs("li", { "data-differs": `${d.project}/${d.service}`, children: [d.project, "/", d.service, ": recorded ", d.recordedImage ?? "nothing", ", the cluster runs ", d.clusterImage ?? "nothing"] }, `${d.project}/${d.service}`))) })) : null, hold.unknownProjects.length > 0 ? _jsxs("p", { children: ["Projects the database does not know: ", hold.unknownProjects.join(", "), "."] }) : null, whoami.platformAdmin && shows("installation.release") ? (_jsx(ConsoleForm, { intent: "release", children: _jsx(Submit, { intent: "release", children: "Release the control plane" }) })) : null, _jsx(Refused, { intent: "release" })] })) : hold?.releasedAt ? (_jsxs("p", { "data-held": "released", children: ["The control plane was released after its database's restore, ", when(hold.releasedAt), hold.releasedBy ? ` by ${hold.releasedBy}` : "", "."] })) : null, backups ? (_jsx("p", { className: "ac-hint", "data-backups": backups.backupTarget, children: backups.notBackedUp ??
53
+ `Backups are kept at least ${backups.retentionDays} days${backups.sharesFailureDomain ? ", in the cluster they back up" : ", with a copy outside the cluster"}.` })) : null, organizations.length === 0 ? (_jsxs("p", { className: "ac-empty", children: ["You are not a member of any organization yet. Create one, or ask an owner to invite ", whoami.email ?? "you", "."] })) : (_jsx("div", { className: "ac-card ac-table-wrap", children: _jsxs("table", { className: "ac-table", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Organizations you can see" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Id" }), _jsx("th", { scope: "col", children: "Your role" }), _jsx("th", { scope: "col", className: "ac-num", children: "Projects" }), _jsx("th", { scope: "col", children: "State" })] }) }), _jsx("tbody", { children: organizations.map((o) => (_jsxs("tr", { children: [_jsx("td", { children: _jsx(ConsoleLink, { to: `organizations/${encodeURIComponent(o.id)}`, children: o.name }) }), _jsx("td", { children: o.id }), _jsx("td", { children: o.role ? roleWords[o.role] : "Administrator (not a member)" }), _jsx("td", { className: "ac-num", children: o.projects }), _jsx("td", { children: o.disabled ? "Disabled" : "Active" })] }, o.id))) })] }) })), _jsxs("section", { className: "ac-card", "aria-labelledby": "installation", children: [_jsx("h2", { id: "installation", children: "This installation" }), _jsxs("dl", { className: "ac-facts", children: [_jsx("dt", { children: "Platform version" }), _jsx("dd", { children: installation.platformVersion }), _jsx("dt", { children: "Cloud" }), _jsx("dd", { children: cloud ? `${cloud.provider}, account ${cloud.account}` : "None: everything is served by the installation itself" }), cloud ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Location" }), _jsx("dd", { children: cloud.location })] })) : null, cloud?.kmsKey ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Wrapping key" }), _jsx("dd", { children: cloud.kmsKey })] })) : null, installation.secrets ? (_jsxs(_Fragment, { children: [_jsx("dt", { children: "Secrets kept in" }), _jsx("dd", { children: secretBackendWords[installation.secrets.backend] ?? installation.secrets.backend }), _jsx("dt", { children: "Record of reads kept for" }), _jsx("dd", { children: installation.secrets.recordRetention }), _jsx("dt", { children: "Access log" }), _jsx("dd", { children: installation.secrets.auditLog })] })) : null] })] })] }));
24
54
  }
55
+ /** The installation's secret store (feature 038), in words; an unknown backend is shown as it is named. */
56
+ const secretBackendWords = {
57
+ postgres: "Postgres: each service's own database",
58
+ "secret-manager": "Google Secret Manager",
59
+ };
25
60
  export const ErrorBoundary = ConsoleErrorBoundary;
@@ -20,6 +20,97 @@ export declare function loader({ request, params, context }: LoaderFunctionArgs)
20
20
  paused: boolean;
21
21
  hosting: string;
22
22
  protocol: string | undefined;
23
+ mounts: {
24
+ path: string;
25
+ service: string;
26
+ state: string;
27
+ }[];
28
+ callers: string[];
29
+ processPort: number | undefined;
30
+ broker: string | undefined;
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;
45
+ topicChecks: {
46
+ topic: string | undefined;
47
+ service: string;
48
+ component: string;
49
+ direction: string;
50
+ stated: string | undefined;
51
+ state: string;
52
+ }[] | undefined;
53
+ topicSources: {
54
+ kind: string;
55
+ component: string;
56
+ topic: string;
57
+ group: string;
58
+ start: string;
59
+ version: number;
60
+ recordedVersion: number | undefined;
61
+ behind: boolean;
62
+ broker: string | undefined;
63
+ contract: string | undefined;
64
+ lag: number | undefined;
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;
82
+ }[] | undefined;
83
+ objectStorage: string | undefined;
84
+ bucket: string | undefined;
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;
23
114
  };
24
115
  project: {
25
116
  id: string;
@@ -58,6 +149,7 @@ export declare function loader({ request, params, context }: LoaderFunctionArgs)
58
149
  query: {
59
150
  instance: string | undefined;
60
151
  previous: boolean;
152
+ platform: boolean;
61
153
  tail: number;
62
154
  since: number | undefined;
63
155
  };