ankka-console 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/client/control-plane.d.ts +25 -1
  2. package/dist/client/control-plane.js +60 -1
  3. package/dist/client/schemas.d.ts +525 -0
  4. package/dist/client/schemas.js +174 -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 +7 -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.js +12 -6
  17. package/dist/routes/logs.d.ts +35 -0
  18. package/dist/routes/logs.js +15 -14
  19. package/dist/routes/members.js +11 -8
  20. package/dist/routes/organization-new.js +7 -4
  21. package/dist/routes/organization.js +13 -6
  22. package/dist/routes/project-new.js +8 -4
  23. package/dist/routes/project.d.ts +73 -2
  24. package/dist/routes/project.js +81 -9
  25. package/dist/routes/service-apply.d.ts +5 -2
  26. package/dist/routes/service-apply.js +17 -14
  27. package/dist/routes/service-history.d.ts +30 -0
  28. package/dist/routes/service-history.js +79 -0
  29. package/dist/routes/service-topology.d.ts +153 -0
  30. package/dist/routes/service-topology.js +54 -0
  31. package/dist/routes/service.d.ts +34 -10
  32. package/dist/routes/service.js +27 -22
  33. package/dist/routes/stream.service.js +18 -1
  34. package/dist/routes/tokens.js +8 -6
  35. package/dist/routes.js +2 -0
  36. package/dist/styles.css +2 -481
  37. package/dist/testing/fake-control-plane.d.ts +155 -0
  38. package/dist/testing/fake-control-plane.js +379 -12
  39. package/dist/testing/index.d.ts +1 -1
  40. package/dist/testing/scenarios.js +6 -0
  41. package/dist/theme.css +66 -0
  42. package/dist/ui/console.d.ts +19 -6
  43. package/dist/ui/console.js +9 -2
  44. package/dist/ui/errors.js +7 -5
  45. package/dist/ui/hosting.d.ts +11 -0
  46. package/dist/ui/hosting.js +20 -0
  47. package/dist/ui/primitives/button.d.ts +11 -0
  48. package/dist/ui/primitives/button.js +21 -0
  49. package/dist/ui/primitives/select.d.ts +10 -0
  50. package/dist/ui/primitives/select.js +4 -0
  51. package/dist/ui/primitives/switch.d.ts +9 -0
  52. package/dist/ui/primitives/switch.js +4 -0
  53. package/dist/ui/shape.d.ts +4 -0
  54. package/dist/ui/shape.js +36 -0
  55. package/dist/ui/shell.d.ts +76 -0
  56. package/dist/ui/shell.js +107 -0
  57. package/dist/ui/tokens.d.ts +82 -0
  58. package/dist/ui/tokens.js +172 -0
  59. package/dist/ui/topology/Graph.d.ts +13 -0
  60. package/dist/ui/topology/Graph.js +56 -0
  61. package/dist/ui/topology/Table.d.ts +10 -0
  62. package/dist/ui/topology/Table.js +14 -0
  63. package/dist/ui/topology/layout.d.ts +113 -0
  64. package/dist/ui/topology/layout.js +177 -0
  65. package/dist/ui/use-stream.d.ts +137 -1
  66. package/dist/ui/use-stream.js +11 -1
  67. package/package.json +11 -3
@@ -90,6 +90,11 @@ export const projectSummarySchema = z.object({
90
90
  services: z.number().int(),
91
91
  registry: optional(registrySummarySchema),
92
92
  });
93
+ export const mountStatusSchema = z.object({
94
+ path: z.string(),
95
+ service: z.string(),
96
+ state: z.string().default(""),
97
+ });
93
98
  export const serviceStatusSchema = z.object({
94
99
  name: z.string(),
95
100
  projectId: z.string(),
@@ -107,6 +112,19 @@ export const serviceStatusSchema = z.object({
107
112
  paused: z.boolean().default(false),
108
113
  hosting: z.string().default("embedded"),
109
114
  protocol: optional(z.string()),
115
+ mounts: z.array(mountStatusSchema).default([]),
116
+ callers: z.array(z.string()).default([]),
117
+ processPort: optional(z.number().int()),
118
+ broker: optional(z.string()),
119
+ /** Topics the service's components use that its project does not declare; absent when not read. */
120
+ undeclaredTopics: optional(z.array(z.string())),
121
+ /** Each side the service takes on a declared topic with a contract, checked against it; absent when not read. */
122
+ topicChecks: optional(z.array(z.lazy(() => topicCheckSchema))),
123
+ /** Each topic source of the service with how far behind it is, from its instances; absent when not read. */
124
+ topicSources: optional(z.array(z.lazy(() => topicSourceReportSchema))),
125
+ objectStorage: optional(z.string()),
126
+ bucket: optional(z.string()),
127
+ bucketAddress: optional(z.string()),
110
128
  });
111
129
  export const historyActorSchema = z.object({
112
130
  subject: z.string(),
@@ -118,7 +136,14 @@ export const historyEntrySchema = z.object({
118
136
  generation: z.number().int(),
119
137
  actor: optional(historyActorSchema),
120
138
  at: optional(z.string()),
139
+ /** On an entry that recorded a descriptor: its image, and a digest two such entries share exactly when the descriptors are the same. */
140
+ image: optional(z.string()),
141
+ digest: optional(z.string()),
142
+ /** On a rollback: the generation whose descriptor was applied again. */
143
+ rolledBackTo: optional(z.number().int()),
121
144
  });
145
+ export const rollbackRequestSchema = z.object({ generation: optional(z.number().int()) });
146
+ export const rolledBackSchema = z.object({ rolledBackTo: z.number().int(), status: serviceStatusSchema });
122
147
  export const instanceLogsSchema = z.object({
123
148
  instance: z.string(),
124
149
  output: z.string(),
@@ -127,6 +152,65 @@ export const instanceLogsSchema = z.object({
127
152
  export const logsResponseSchema = z.object({
128
153
  instances: z.array(instanceLogsSchema),
129
154
  });
155
+ // ── Topology ──────────────────────────────────────────────────────────────
156
+ export const topologyHandlerSchema = z.object({
157
+ name: z.string(),
158
+ type: z.string(),
159
+ streaming: optional(z.boolean()),
160
+ });
161
+ export const topologyNodeSchema = z.object({
162
+ id: z.string(),
163
+ kind: z.string(),
164
+ layer: z.number().int(),
165
+ platform: z.boolean(),
166
+ handlers: z.array(topologyHandlerSchema),
167
+ });
168
+ export const declaredEdgeSchema = z.object({ from: z.string(), to: z.string(), kind: z.string() });
169
+ export const callPairSchema = z.object({
170
+ caller: z.string(),
171
+ callee: z.string(),
172
+ handled: z.object({ ok: z.number(), refused: z.number(), failed: z.number() }),
173
+ unanswered: z.object({ timedOut: z.number(), undelivered: z.number() }),
174
+ durationMillis: z.object({ p50: z.number(), p99: z.number(), max: z.number(), bucketed: z.boolean().default(true) }),
175
+ streaming: z.boolean().default(false),
176
+ histogram: z.array(z.number()).default([]),
177
+ });
178
+ export const callEdgeSchema = z.object({ from: z.string(), to: z.string(), pairs: z.array(callPairSchema) });
179
+ export const topologyWindowSchema = z.object({
180
+ seconds: z.number(),
181
+ since: z.string(),
182
+ calls: z.number(),
183
+ unanswered: z.number().default(0),
184
+ });
185
+ export const instanceTopologyDocumentSchema = z.object({
186
+ service: z.object({ name: z.string(), runtime: z.string(), instance: z.string(), startedAt: z.string() }),
187
+ window: topologyWindowSchema,
188
+ nodes: z.array(topologyNodeSchema),
189
+ declared: z.array(declaredEdgeSchema),
190
+ calls: z.array(callEdgeSchema),
191
+ });
192
+ export const instanceStatusSchema = z.enum(["ok", "unreachable", "unsupported", "failed"]);
193
+ export const instanceTopologySchema = z.object({
194
+ pod: z.string(),
195
+ status: instanceStatusSchema,
196
+ problem: optional(z.string()),
197
+ runtime: optional(z.string()),
198
+ readAt: optional(z.string()),
199
+ });
200
+ export const topologyDifferenceSchema = z.object({ node: z.string(), presentOn: z.array(z.string()) });
201
+ /** `GET /services/{projectId}/{name}/topology`: every instance's topology, merged. */
202
+ export const serviceTopologySchema = z.object({
203
+ service: z.string(),
204
+ running: z.number().int(),
205
+ contributing: z.number().int(),
206
+ partial: z.boolean(),
207
+ instances: z.array(instanceTopologySchema),
208
+ window: topologyWindowSchema,
209
+ nodes: z.array(topologyNodeSchema),
210
+ declared: z.array(declaredEdgeSchema),
211
+ calls: z.array(callEdgeSchema),
212
+ differences: z.array(topologyDifferenceSchema),
213
+ });
130
214
  export const memberSummarySchema = z.object({
131
215
  subject: z.string(),
132
216
  role: roleSchema,
@@ -186,6 +270,83 @@ export const setRegistrySchema = z.object({
186
270
  username: z.string(),
187
271
  password: z.string(),
188
272
  });
273
+ /** `PUT /projects/{id}/secrets/{name}`: entries merged into a project secret. */
274
+ export const setProjectSecretSchema = z.object({
275
+ entries: z.record(z.string(), z.string()),
276
+ });
277
+ /** A contract as declared on a topic: its name and the fingerprint of its schema document. */
278
+ export const contractSchema = z.object({
279
+ name: z.string(),
280
+ fingerprint: z.string(),
281
+ });
282
+ /** One side of a declared topic as a running service states it, against the declared contract. */
283
+ export const topicCheckSchema = z.object({
284
+ topic: optional(z.string()),
285
+ service: z.string(),
286
+ component: z.string(),
287
+ direction: z.string(),
288
+ stated: optional(z.string()),
289
+ state: z.string(),
290
+ });
291
+ /**
292
+ * `PUT /projects/{id}/topics/{name}`: the partitions a declared topic has, whether the broker keeps
293
+ * only the last message under each key, and the contract it carries with its schema document.
294
+ */
295
+ export const topicDeclarationRequestSchema = z.object({
296
+ partitions: z.number().int(),
297
+ compacted: z.boolean().default(false),
298
+ contract: optional(z.object({ name: z.string(), schema: z.unknown() })),
299
+ });
300
+ /** A topic a project declares, and how far the platform has got with it. */
301
+ export const projectTopicSchema = z.object({
302
+ name: z.string(),
303
+ partitions: z.number().int(),
304
+ phase: optional(z.string()),
305
+ detail: optional(z.string()),
306
+ compacted: z.boolean().default(false),
307
+ contract: optional(contractSchema),
308
+ checks: z.array(topicCheckSchema).default([]),
309
+ });
310
+ /**
311
+ * A topic source as a service's instances report it: what reads which topic under which group, the
312
+ * declared broker and contract it states, `lag` (messages past the last one handled, as of the last
313
+ * poll) and `failing` (the reason of the change being delivered again, until one succeeds).
314
+ */
315
+ export const topicSourceReportSchema = z.object({
316
+ kind: z.string(),
317
+ component: z.string(),
318
+ topic: z.string(),
319
+ group: z.string(),
320
+ start: z.string(),
321
+ version: z.number().int(),
322
+ recordedVersion: optional(z.number().int()),
323
+ behind: z.boolean().default(false),
324
+ broker: optional(z.string()),
325
+ contract: optional(z.string()),
326
+ lag: optional(z.number().int()),
327
+ failing: optional(z.string()),
328
+ });
329
+ /** `PUT /projects/{id}/brokers/{name}`: a broker declared beside the installation's, and how it is reached. */
330
+ export const brokerDeclarationRequestSchema = z.object({
331
+ bootstrap: z.string(),
332
+ shape: z.string(),
333
+ secret: z.string(),
334
+ });
335
+ /** A broker a project declares, as listed: where it is, the shape of its credential, and the project secret holding it. */
336
+ export const projectBrokerSchema = z.object({
337
+ name: z.string(),
338
+ bootstrap: z.string(),
339
+ shape: z.string(),
340
+ secret: z.string(),
341
+ declaredAt: optional(z.string()),
342
+ });
343
+ /** A project secret as the control plane lists it: entries' names, never a value. */
344
+ export const projectSecretSummarySchema = z.object({
345
+ name: z.string(),
346
+ entries: z.array(z.string()),
347
+ setAt: optional(z.string()),
348
+ setBy: optional(z.string()),
349
+ });
189
350
  /** Every schema by the Scala type's name, as the fixture files name them. */
190
351
  export const schemasByType = {
191
352
  AuthDiscovery: authDiscoverySchema,
@@ -198,11 +359,17 @@ export const schemasByType = {
198
359
  RegistrySummary: registrySummarySchema,
199
360
  ProjectDetail: projectDetailSchema,
200
361
  ProjectSummary: projectSummarySchema,
362
+ MountStatus: mountStatusSchema,
201
363
  ServiceStatus: serviceStatusSchema,
202
364
  HistoryActor: historyActorSchema,
203
365
  HistoryEntry: historyEntrySchema,
366
+ RollbackRequest: rollbackRequestSchema,
367
+ RolledBack: rolledBackSchema,
204
368
  InstanceLogs: instanceLogsSchema,
205
369
  LogsResponse: logsResponseSchema,
370
+ InstanceTopologyDocument: instanceTopologyDocumentSchema,
371
+ InstanceTopology: instanceTopologySchema,
372
+ ServiceTopology: serviceTopologySchema,
206
373
  MemberSummary: memberSummarySchema,
207
374
  InvitationSummary: invitationSummarySchema,
208
375
  MembersResponse: membersResponseSchema,
@@ -218,4 +385,11 @@ export const schemasByType = {
218
385
  Repair: repairSchema,
219
386
  CreateDeployToken: createDeployTokenSchema,
220
387
  SetRegistry: setRegistrySchema,
388
+ SetProjectSecret: setProjectSecretSchema,
389
+ ProjectSecretSummary: projectSecretSummarySchema,
390
+ TopicDeclarationRequest: topicDeclarationRequestSchema,
391
+ ProjectTopic: projectTopicSchema,
392
+ Contract: contractSchema,
393
+ BrokerDeclarationRequest: brokerDeclarationRequestSchema,
394
+ ProjectBroker: projectBrokerSchema,
221
395
  };
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"];
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"];
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", "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", "service.apply", "service.pause", "service.resume", "service.restart", "service.rollback", "service.expose", "service.unexpose", "service.delete", "service.logs"];
5
5
  export type Operation = (typeof operations)[number];
6
6
  export interface PanelEntities {
7
7
  organization: OrganizationSummary;
@@ -19,10 +19,17 @@ export const operations = [
19
19
  "project.delete",
20
20
  "registry.set",
21
21
  "registry.clear",
22
+ "project-secret.set",
23
+ "project-secret.unset",
24
+ "project-topic.set",
25
+ "project-topic.unset",
26
+ "project-broker.set",
27
+ "project-broker.unset",
22
28
  "service.apply",
23
29
  "service.pause",
24
30
  "service.resume",
25
31
  "service.restart",
32
+ "service.rollback",
26
33
  "service.expose",
27
34
  "service.unexpose",
28
35
  "service.delete",
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;
@@ -4,22 +4,28 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * yours with your role in each, or every one of them for a platform administrator.
5
5
  */
6
6
  import { useLoaderData } from "react-router";
7
- import { guard, pageData, useConsoleContext } from "../context.js";
7
+ import { guard, pageData, useConsoleContext, withShell } from "../context.js";
8
8
  import { ConsoleErrorBoundary } from "../ui/errors.js";
9
- import { ConsoleLink, useConsole } from "../ui/console.js";
10
- import { HostActions } from "../extensions/render.js";
9
+ import { ConsoleLink } from "../ui/console.js";
10
+ import { Page } from "../ui/shell.js";
11
+ import { HostActions, useHostActions } from "../extensions/render.js";
11
12
  export const meta = () => [{ title: "Organizations · ankka" }];
12
13
  export async function loader({ context }) {
13
14
  const ctx = useConsoleContext(context);
14
15
  return guard(ctx, async () => {
15
16
  const [whoami, organizations] = await Promise.all([ctx.client.whoami(), ctx.client.listOrganizations()]);
16
- return { console: await pageData(ctx), whoami, organizations };
17
+ const shell = {
18
+ area: "organizations",
19
+ crumbs: [{ label: "Organizations" }],
20
+ primary: { label: "Create an organization", to: "organizations/new", operation: "organization.create" },
21
+ };
22
+ return { console: withShell(await pageData(ctx), shell), whoami, organizations };
17
23
  });
18
24
  }
19
25
  const roleWords = { owner: "Owner", member: "Member" };
20
26
  export default function Front() {
21
27
  const { whoami, organizations } = useLoaderData();
22
- 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" })] })] }));
28
+ const hostActions = useHostActions("organization.create");
29
+ 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." : "."] }), 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))) })] }) }))] }));
24
30
  }
25
31
  export const ErrorBoundary = ConsoleErrorBoundary;
@@ -20,6 +20,40 @@ 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
+ topicChecks: {
33
+ topic: string | undefined;
34
+ service: string;
35
+ component: string;
36
+ direction: string;
37
+ stated: string | undefined;
38
+ state: string;
39
+ }[] | undefined;
40
+ topicSources: {
41
+ kind: string;
42
+ component: string;
43
+ topic: string;
44
+ group: string;
45
+ start: string;
46
+ version: number;
47
+ recordedVersion: number | undefined;
48
+ behind: boolean;
49
+ broker: string | undefined;
50
+ contract: string | undefined;
51
+ lag: number | undefined;
52
+ failing: string | undefined;
53
+ }[] | undefined;
54
+ objectStorage: string | undefined;
55
+ bucket: string | undefined;
56
+ bucketAddress: string | undefined;
23
57
  };
24
58
  project: {
25
59
  id: string;
@@ -58,6 +92,7 @@ export declare function loader({ request, params, context }: LoaderFunctionArgs)
58
92
  query: {
59
93
  instance: string | undefined;
60
94
  previous: boolean;
95
+ platform: boolean;
61
96
  tail: number;
62
97
  since: number | undefined;
63
98
  };