@camunda/ccma-shared-types 2.0.0 → 3.0.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 (34) hide show
  1. package/dist/_tests/permissions/session-permissions.spec.d.ts +1 -0
  2. package/dist/approvals/policy.d.ts +94 -5
  3. package/dist/approvals/policy.js +64 -14
  4. package/dist/approvals/policy.js.map +2 -2
  5. package/dist/approvals/use-cases.const.d.ts +41 -0
  6. package/dist/approvals/use-cases.const.js +12 -0
  7. package/dist/approvals/use-cases.const.js.map +7 -0
  8. package/dist/dtos/AlertSubscription.dto.d.ts +6 -0
  9. package/dist/dtos/AlertSubscription.dto.js.map +2 -2
  10. package/dist/dtos/ApprovalJob.dto.d.ts +258 -4
  11. package/dist/dtos/ApprovalJob.dto.js +42 -1
  12. package/dist/dtos/ApprovalJob.dto.js.map +2 -2
  13. package/dist/dtos/ClusterOwnerMove.dto.d.ts +81 -0
  14. package/dist/dtos/ClusterOwnerMove.dto.js +1 -0
  15. package/dist/dtos/ClusterOwnerMove.dto.js.map +7 -0
  16. package/dist/dtos/FeatureFlags.dto.d.ts +1 -1
  17. package/dist/dtos/Generations.dto.d.ts +17 -0
  18. package/dist/dtos/Generations.dto.js +7 -0
  19. package/dist/dtos/Generations.dto.js.map +2 -2
  20. package/dist/dtos/Generations.dto.spec.d.ts +1 -0
  21. package/dist/dtos/ReleaseTrainSanity.dto.d.ts +6 -0
  22. package/dist/enums/SystemAuditEntity.enum.d.ts +1 -0
  23. package/dist/enums/SystemAuditEntity.enum.js +1 -0
  24. package/dist/enums/SystemAuditEntity.enum.js.map +2 -2
  25. package/dist/index.d.ts +9 -6
  26. package/dist/index.js +45 -3
  27. package/dist/index.js.map +2 -2
  28. package/dist/permissions/combo.d.ts +9 -0
  29. package/dist/permissions/combo.js +54 -1
  30. package/dist/permissions/combo.js.map +2 -2
  31. package/dist/utils/versionCheck.utils.d.ts +9 -0
  32. package/dist/utils/versionCheck.utils.js +7 -2
  33. package/dist/utils/versionCheck.utils.js.map +2 -2
  34. package/package.json +1 -1
@@ -19,6 +19,44 @@
19
19
  */
20
20
  export declare const APPROVAL_JOB_STATUSES: readonly ["pending", "approved", "executing", "executed", "failed", "rejected", "withdrawn", "expired"];
21
21
  export type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number];
22
+ /**
23
+ * The states in which the action of a request is still COMING.
24
+ *
25
+ * `pending` waits for approvals, `approved` waits for a person to press the
26
+ * button, and `executing` is running now. Each of them can still change the
27
+ * world, so two requests for one target must not be in these states together.
28
+ * `dedupeKey` reads this set for that reason.
29
+ *
30
+ * `failed` is NOT here, although a person can run a failed request again. A
31
+ * failed request runs only when a person presses the button, and that person
32
+ * sees the row. Blocking a new request on a failed one would also trap the
33
+ * key: `canWithdraw` accepts `pending` alone, so nobody could close it.
34
+ *
35
+ * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.
36
+ */
37
+ export declare const APPROVAL_LIVE_STATUSES: readonly ["pending", "approved", "executing"];
38
+ /**
39
+ * How many requests one page of the list holds when the caller asks for no
40
+ * number.
41
+ *
42
+ * The list is a work queue that a Camundi reads, so the page is large: a
43
+ * person wants the whole queue and not ten rows at a time. What the number is
44
+ * for is the ceiling below.
45
+ */
46
+ export declare const APPROVAL_PAGE_SIZE_DEFAULT = 500;
47
+ /**
48
+ * The largest page that the service builds, and the largest that a caller may
49
+ * ask for. The API answers 400 above it.
50
+ *
51
+ * The list had no limit at all. One authenticated caller could then make the
52
+ * service read the whole table into memory, serialize it, and do it again, so
53
+ * the cost of a request grew with the age of the service and with nothing that
54
+ * the caller had to pay for. A ceiling turns that into a fixed cost.
55
+ *
56
+ * It is HERE and not only in the service, because the view must know it: a
57
+ * page that asked for more would get a 400 instead of rows.
58
+ */
59
+ export declare const APPROVAL_PAGE_SIZE_MAX = 2000;
22
60
  /**
23
61
  * What happens when a request reaches its threshold.
24
62
  *
@@ -26,6 +64,22 @@ export type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number];
26
64
  * `approved` and waits for a person to run it.
27
65
  */
28
66
  export type ApprovalExecutionMode = 'auto' | 'explicit';
67
+ /**
68
+ * WHERE the action runs. It is a separate question from who starts it.
69
+ *
70
+ * - `inline` runs the action inside the call that started it, and the answer
71
+ * already carries the outcome. Right for one call to another service.
72
+ * - `background` answers as soon as the request is claimed, and the action
73
+ * runs on. It reports into `ApprovalJobDto.progress`, and the row is the
74
+ * only place that holds the progress, so a reload and a second person read
75
+ * the same numbers. A pod that stops loses the run, and the reconciler of
76
+ * control-backend recovers the row.
77
+ *
78
+ * An action over thousands of targets MUST be `background`. `inline` would
79
+ * hold one HTTP request for the whole run, and the ingress, the load balancer
80
+ * and the browser each have their own deadline before it finishes.
81
+ */
82
+ export type ApprovalActionMode = 'inline' | 'background';
29
83
  /**
30
84
  * A person, as an approval request records them.
31
85
  *
@@ -41,24 +95,168 @@ export interface ApprovalAck extends ApprovalActor {
41
95
  /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */
42
96
  date: string;
43
97
  }
98
+ /**
99
+ * What one value of a summary field is.
100
+ *
101
+ * `text` is what a person reads. `id` is what a link points at, and it is
102
+ * absent when the kind names no entity. The two are separate because the text
103
+ * is a COPY that the request keeps: an organization that gets a new name after
104
+ * the request was opened must still read as the organization that the person
105
+ * chose.
106
+ */
107
+ export interface ApprovalSummaryValue {
108
+ text: string;
109
+ id?: string;
110
+ }
111
+ /**
112
+ * How a reader must render one field of a summary.
113
+ *
114
+ * `org`, `cluster` and `user` name an entity, so a reader can link to it and
115
+ * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,
116
+ * and `code` is a value that must keep its characters, such as a json fragment.
117
+ */
118
+ export declare const APPROVAL_SUMMARY_FIELD_KINDS: readonly ["text", "number", "boolean", "date", "org", "cluster", "user", "code"];
119
+ export type ApprovalSummaryFieldKind = (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number];
120
+ /**
121
+ * One labelled field of the payload of a request.
122
+ *
123
+ * This is what makes ONE view able to show EVERY use case. The payload of a
124
+ * request is an object of anything, so a generic view can only print it as
125
+ * json. The use case turns it into labelled fields here, and the view renders
126
+ * a field by its kind: it links an `org`, it formats a `date`, and it lists
127
+ * 300 clusters as 300 links.
128
+ *
129
+ * `values` is always an array. A field that holds one value holds an array of
130
+ * one, so a reader needs no branch for "one or many".
131
+ */
132
+ export interface ApprovalSummaryField {
133
+ label: string;
134
+ kind: ApprovalSummaryFieldKind;
135
+ values: ApprovalSummaryValue[];
136
+ }
137
+ /**
138
+ * How long a run may go quiet before a reader calls it stalled.
139
+ *
140
+ * The pod that runs an action writes its progress and renews its lease on one
141
+ * timer, so a healthy run touches the row well inside this window. A row that
142
+ * is `executing` and older than this held a pod that stopped, and the
143
+ * reconciler of control-backend is on its way to it.
144
+ *
145
+ * It lives here, and not in the config of the service, because a READER needs
146
+ * it: the browser must decide what to show without a second call, and a number
147
+ * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in
148
+ * `config/index.ts` is held below this on purpose.
149
+ */
150
+ export declare const APPROVAL_RUN_STALE_MS = 120000;
151
+ /**
152
+ * What an action reports while it runs.
153
+ *
154
+ * This is what makes a long action watchable. Every field is in the DATABASE
155
+ * and not in the answer to the call that started the run, so three readers see
156
+ * the same thing: the person who pressed the button, the same person after a
157
+ * reload, and a second person who opens the request.
158
+ *
159
+ * `total` can be 0 while an action counts its targets, and it stays 0 for an
160
+ * action that cannot count them. A reader must then show that it runs, and no
161
+ * percentage.
162
+ */
163
+ export interface ApprovalProgress {
164
+ /** Targets in total. 0 means the action did not say. */
165
+ total: number;
166
+ /** Targets finished, whatever the outcome of each one. */
167
+ done: number;
168
+ /** Targets that finished with an error. Part of `done`. */
169
+ failed: number;
170
+ /** One short line for a person, such as the step that runs now. */
171
+ note?: string;
172
+ /** An ISO timestamp: when the run started. */
173
+ startedAt: string;
174
+ /** An ISO timestamp: when the run last wrote anything. */
175
+ updatedAt: string;
176
+ }
177
+ /** The share of the work that is finished, or `undefined` with no total. */
178
+ export declare function progressFraction(progress: ApprovalProgress | undefined): number | undefined;
179
+ /**
180
+ * Reports whether a running request went quiet.
181
+ *
182
+ * True means that the pod which held the run stopped, or lost the row. It does
183
+ * NOT mean that the action failed: the reconciler either runs it again, or it
184
+ * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.
185
+ */
186
+ export declare function isRunStalled(job: ApprovalJobDto, now: Date): boolean;
44
187
  /** One approval request, as the API returns it. */
45
188
  export interface ApprovalJobDto {
46
189
  id: string;
47
190
  type: string;
48
191
  dedupeKey?: string;
49
192
  payload: Record<string, unknown>;
193
+ /**
194
+ * The payload as labelled fields. See `ApprovalSummaryField`.
195
+ *
196
+ * The use case builds it when the request is opened AND every time the
197
+ * request is read, so a field that a later version of the use case adds
198
+ * appears on a request that is already open.
199
+ */
200
+ summary: ApprovalSummaryField[];
201
+ /** Why the person opened the request. Some use cases demand one. */
202
+ reason?: string;
50
203
  createdBy: ApprovalActor;
51
204
  acks: ApprovalAck[];
52
- /** Copied from the use case, so a reader can render "1 of 2". */
205
+ /**
206
+ * How many approvals THIS request needs.
207
+ *
208
+ * It is on the request and not only on the use case, because the person who
209
+ * opens one can ask for more than the floor of the use case. A bulk
210
+ * operation over 3000 clusters is the same action as one over 3, and it can
211
+ * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the
212
+ * floor that a caller cannot go below.
213
+ */
53
214
  requiredAcks: number;
215
+ /**
216
+ * What a person must hold to approve, reject or run THIS request.
217
+ *
218
+ * It is on the request and not only on the use case, because a payload can
219
+ * raise the guard: a generation update with `yoloForceUpdate` needs the yolo
220
+ * permission on top of the base one, and the same use case without the flag
221
+ * does not. The set is computed ONE time, when the request is opened, so it
222
+ * is also the record of what the request was guarded by.
223
+ *
224
+ * A person needs EVERY permission in the set, not one of them.
225
+ */
226
+ requiredPermissions: string[];
54
227
  executionMode: ApprovalExecutionMode;
228
+ actionMode: ApprovalActionMode;
55
229
  status: ApprovalJobStatus;
56
230
  closedBy?: ApprovalActor;
57
231
  closedReason?: string;
58
232
  /** An ISO timestamp. */
59
233
  expiresAt: string;
234
+ /**
235
+ * An ISO timestamp: when the current run took the request.
236
+ *
237
+ * It is set every time a run starts, so a run that the reconciler starts
238
+ * again moves it. `attempts` says how many runs there were.
239
+ */
240
+ executingSince?: string;
241
+ /** How many times a run started. A recovery raises it. */
242
+ attempts: number;
243
+ /**
244
+ * What the action reports while it runs. See `ApprovalProgress`.
245
+ *
246
+ * It stays on the row after the run, so the record of a finished run says
247
+ * how many targets it touched and how many refused.
248
+ */
249
+ progress?: ApprovalProgress;
60
250
  executedAt?: string;
61
251
  executionError?: string;
252
+ /**
253
+ * What the action reported when it ran.
254
+ *
255
+ * The shape belongs to the use case. An action over many targets puts the
256
+ * result of each target here, which is the difference between "it failed"
257
+ * and "it did 2998 of 3000, and these two refused".
258
+ */
259
+ executionResult?: Record<string, unknown>;
62
260
  createdAt: string;
63
261
  updatedAt: string;
64
262
  }
@@ -73,11 +271,67 @@ export interface ApprovalUseCaseDto {
73
271
  type: string;
74
272
  /** A short name for a person to read. */
75
273
  label: string;
76
- requiredAcks: number;
274
+ /** One sentence: what the action does, and what it cannot undo. */
275
+ description: string;
276
+ /** The approvals a new request asks for when the caller names no number. */
277
+ defaultAcks: number;
278
+ /**
279
+ * The floor. A caller can ask for more approvals, never for fewer.
280
+ *
281
+ * It is a separate number from `defaultAcks` because the two answer
282
+ * different questions: the default is what the form offers, and the floor is
283
+ * what the service refuses to go below.
284
+ */
285
+ minAcks: number;
77
286
  ttlDays: number;
78
287
  executionMode: ApprovalExecutionMode;
79
- /** The permission that acknowledge, reject and run all need. */
288
+ /** See `ApprovalActionMode`. An action over many targets is `background`. */
289
+ actionMode: ApprovalActionMode;
290
+ /**
291
+ * True when a run that was interrupted can start again by itself.
292
+ *
293
+ * It is a promise about the ACTION, and the reconciler reads it. Set it only
294
+ * when the action can run a second time over the same payload and do no harm
295
+ * twice: it must read what the first run already finished, out of
296
+ * `ApprovalJobDto.executionResult`, and skip it.
297
+ *
298
+ * False, the default, is the safe answer. An interrupted run then lands in
299
+ * `failed`, which a person can run again from the detail page, and a person
300
+ * decides whether running it twice is safe.
301
+ */
302
+ resumable: boolean;
303
+ /**
304
+ * The permission that every request of this use case needs to approve,
305
+ * reject or run. A payload can add more. See
306
+ * `ApprovalJobDto.requiredPermissions`, which is the effective set.
307
+ *
308
+ * It is also the permission to SEE a request of this use case, because a
309
+ * person who cannot act on one has no reason to read it. The person who
310
+ * opened a request always sees that request.
311
+ */
80
312
  requiredPermission: string;
81
- /** The permission to open a request. Absent means any signed-in user. */
313
+ /**
314
+ * The permission to open a request. Absent means any signed-in Camundi.
315
+ *
316
+ * Set it when the pool that can ASK must be as small as the pool that can
317
+ * approve. Leave it absent when the approvals alone are the guard, and the
318
+ * proposal is not.
319
+ */
82
320
  createPermission?: string;
321
+ /** True when a request cannot be opened without a reason. */
322
+ reasonRequired: boolean;
323
+ /**
324
+ * True when the ACTION needs the email address of the person who asked.
325
+ *
326
+ * A token can carry no address, and the row then stores
327
+ * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another
328
+ * service asks it to find a user that does not exist, so the request can
329
+ * only fail — and it fails at the END, after other people spent their
330
+ * approvals on it. A use case that carries the creator into its call sets
331
+ * this, and `canCreate` then refuses such a caller at the start.
332
+ *
333
+ * `extend-trial` is one: accounts makes the creator the actor of the
334
+ * extension, through `onBehalfOf`.
335
+ */
336
+ creatorEmailRequired?: boolean;
83
337
  }
@@ -8,7 +8,48 @@ const APPROVAL_JOB_STATUSES = [
8
8
  "withdrawn",
9
9
  "expired"
10
10
  ];
11
+ const APPROVAL_LIVE_STATUSES = [
12
+ "pending",
13
+ "approved",
14
+ "executing"
15
+ ];
16
+ const APPROVAL_PAGE_SIZE_DEFAULT = 500;
17
+ const APPROVAL_PAGE_SIZE_MAX = 2e3;
18
+ const APPROVAL_SUMMARY_FIELD_KINDS = [
19
+ "text",
20
+ "number",
21
+ "boolean",
22
+ "date",
23
+ "org",
24
+ "cluster",
25
+ "user",
26
+ "code"
27
+ ];
28
+ const APPROVAL_RUN_STALE_MS = 12e4;
29
+ function progressFraction(progress) {
30
+ if (!progress || progress.total <= 0) {
31
+ return void 0;
32
+ }
33
+ return Math.min(1, progress.done / progress.total);
34
+ }
35
+ function isRunStalled(job, now) {
36
+ if (job.status !== "executing") {
37
+ return false;
38
+ }
39
+ const lastSign = job.progress?.updatedAt ?? job.executingSince;
40
+ if (!lastSign) {
41
+ return false;
42
+ }
43
+ return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS;
44
+ }
11
45
  export {
12
- APPROVAL_JOB_STATUSES
46
+ APPROVAL_JOB_STATUSES,
47
+ APPROVAL_LIVE_STATUSES,
48
+ APPROVAL_PAGE_SIZE_DEFAULT,
49
+ APPROVAL_PAGE_SIZE_MAX,
50
+ APPROVAL_RUN_STALE_MS,
51
+ APPROVAL_SUMMARY_FIELD_KINDS,
52
+ isRunStalled,
53
+ progressFraction
13
54
  };
14
55
  //# sourceMappingURL=ApprovalJob.dto.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/dtos/ApprovalJob.dto.ts"],
4
- "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /** Copied from the use case, so a reader can render \"1 of 2\". */\n requiredAcks: number\n executionMode: ApprovalExecutionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n executedAt?: string\n executionError?: string\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n requiredAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** The permission that acknowledge, reject and run all need. */\n requiredPermission: string\n /** The permission to open a request. Absent means any signed-in user. */\n createPermission?: string\n}\n"],
5
- "mappings": "AA0BO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\n/**\n * The wire types of the generic approval mechanism.\n *\n * This file holds no logic and no dependency, because three consumers read it:\n * control-backend, control-frontend and cloudbot. The rules that read these\n * types are in `../approvals/policy.js`, and they are pure for the same reason.\n */\n\n/**\n * Every state that an approval request can hold.\n *\n * A `const` tuple, not an enum, so it is both the union type and the list that\n * validates a status filter at run time.\n *\n * The path of a request: `pending` until it reaches the threshold. Then\n * `executing` (automatic mode) or `approved` (explicit mode, which waits for a\n * person to press the button). `executed` or `failed` after the action ran. A\n * request that never reaches the threshold ends `rejected`, `withdrawn` or\n * `expired`.\n */\nexport const APPROVAL_JOB_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n 'executed',\n 'failed',\n 'rejected',\n 'withdrawn',\n 'expired',\n] as const\n\nexport type ApprovalJobStatus = (typeof APPROVAL_JOB_STATUSES)[number]\n\n/**\n * The states in which the action of a request is still COMING.\n *\n * `pending` waits for approvals, `approved` waits for a person to press the\n * button, and `executing` is running now. Each of them can still change the\n * world, so two requests for one target must not be in these states together.\n * `dedupeKey` reads this set for that reason.\n *\n * `failed` is NOT here, although a person can run a failed request again. A\n * failed request runs only when a person presses the button, and that person\n * sees the row. Blocking a new request on a failed one would also trap the\n * key: `canWithdraw` accepts `pending` alone, so nobody could close it.\n *\n * `executed`, `rejected`, `withdrawn` and `expired` are the end of a request.\n */\nexport const APPROVAL_LIVE_STATUSES = [\n 'pending',\n 'approved',\n 'executing',\n] as const satisfies readonly ApprovalJobStatus[]\n\n/**\n * How many requests one page of the list holds when the caller asks for no\n * number.\n *\n * The list is a work queue that a Camundi reads, so the page is large: a\n * person wants the whole queue and not ten rows at a time. What the number is\n * for is the ceiling below.\n */\nexport const APPROVAL_PAGE_SIZE_DEFAULT = 500\n\n/**\n * The largest page that the service builds, and the largest that a caller may\n * ask for. The API answers 400 above it.\n *\n * The list had no limit at all. One authenticated caller could then make the\n * service read the whole table into memory, serialize it, and do it again, so\n * the cost of a request grew with the age of the service and with nothing that\n * the caller had to pay for. A ceiling turns that into a fixed cost.\n *\n * It is HERE and not only in the service, because the view must know it: a\n * page that asked for more would get a 400 instead of rows.\n */\nexport const APPROVAL_PAGE_SIZE_MAX = 2000\n\n/**\n * What happens when a request reaches its threshold.\n *\n * `auto` runs the action immediately. `explicit` holds the request at\n * `approved` and waits for a person to run it.\n */\nexport type ApprovalExecutionMode = 'auto' | 'explicit'\n\n/**\n * WHERE the action runs. It is a separate question from who starts it.\n *\n * - `inline` runs the action inside the call that started it, and the answer\n * already carries the outcome. Right for one call to another service.\n * - `background` answers as soon as the request is claimed, and the action\n * runs on. It reports into `ApprovalJobDto.progress`, and the row is the\n * only place that holds the progress, so a reload and a second person read\n * the same numbers. A pod that stops loses the run, and the reconciler of\n * control-backend recovers the row.\n *\n * An action over thousands of targets MUST be `background`. `inline` would\n * hold one HTTP request for the whole run, and the ingress, the load balancer\n * and the browser each have their own deadline before it finishes.\n */\nexport type ApprovalActionMode = 'inline' | 'background'\n\n/**\n * A person, as an approval request records them.\n *\n * Both keys are kept. The machine then never has to choose one identity key,\n * which is what split the earlier acknowledgement flows: some held an email and\n * some held a user id, and the two could not be compared.\n */\nexport interface ApprovalActor {\n userId: string\n email: string\n}\n\nexport interface ApprovalAck extends ApprovalActor {\n /** An ISO timestamp. It is in a json column, so it is a string, not a Date. */\n date: string\n}\n\n/**\n * What one value of a summary field is.\n *\n * `text` is what a person reads. `id` is what a link points at, and it is\n * absent when the kind names no entity. The two are separate because the text\n * is a COPY that the request keeps: an organization that gets a new name after\n * the request was opened must still read as the organization that the person\n * chose.\n */\nexport interface ApprovalSummaryValue {\n text: string\n id?: string\n}\n\n/**\n * How a reader must render one field of a summary.\n *\n * `org`, `cluster` and `user` name an entity, so a reader can link to it and\n * can resolve a fresh name. The others are plain: `date` is an ISO timestamp,\n * and `code` is a value that must keep its characters, such as a json fragment.\n */\nexport const APPROVAL_SUMMARY_FIELD_KINDS = [\n 'text',\n 'number',\n 'boolean',\n 'date',\n 'org',\n 'cluster',\n 'user',\n 'code',\n] as const\n\nexport type ApprovalSummaryFieldKind =\n (typeof APPROVAL_SUMMARY_FIELD_KINDS)[number]\n\n/**\n * One labelled field of the payload of a request.\n *\n * This is what makes ONE view able to show EVERY use case. The payload of a\n * request is an object of anything, so a generic view can only print it as\n * json. The use case turns it into labelled fields here, and the view renders\n * a field by its kind: it links an `org`, it formats a `date`, and it lists\n * 300 clusters as 300 links.\n *\n * `values` is always an array. A field that holds one value holds an array of\n * one, so a reader needs no branch for \"one or many\".\n */\nexport interface ApprovalSummaryField {\n label: string\n kind: ApprovalSummaryFieldKind\n values: ApprovalSummaryValue[]\n}\n\n/**\n * How long a run may go quiet before a reader calls it stalled.\n *\n * The pod that runs an action writes its progress and renews its lease on one\n * timer, so a healthy run touches the row well inside this window. A row that\n * is `executing` and older than this held a pod that stopped, and the\n * reconciler of control-backend is on its way to it.\n *\n * It lives here, and not in the config of the service, because a READER needs\n * it: the browser must decide what to show without a second call, and a number\n * that only the backend knew would make the two disagree. `ACTION_LEASE_MS` in\n * `config/index.ts` is held below this on purpose.\n */\nexport const APPROVAL_RUN_STALE_MS = 120_000\n\n/**\n * What an action reports while it runs.\n *\n * This is what makes a long action watchable. Every field is in the DATABASE\n * and not in the answer to the call that started the run, so three readers see\n * the same thing: the person who pressed the button, the same person after a\n * reload, and a second person who opens the request.\n *\n * `total` can be 0 while an action counts its targets, and it stays 0 for an\n * action that cannot count them. A reader must then show that it runs, and no\n * percentage.\n */\nexport interface ApprovalProgress {\n /** Targets in total. 0 means the action did not say. */\n total: number\n /** Targets finished, whatever the outcome of each one. */\n done: number\n /** Targets that finished with an error. Part of `done`. */\n failed: number\n /** One short line for a person, such as the step that runs now. */\n note?: string\n /** An ISO timestamp: when the run started. */\n startedAt: string\n /** An ISO timestamp: when the run last wrote anything. */\n updatedAt: string\n}\n\n/** The share of the work that is finished, or `undefined` with no total. */\nexport function progressFraction(\n progress: ApprovalProgress | undefined,\n): number | undefined {\n if (!progress || progress.total <= 0) {\n return undefined\n }\n return Math.min(1, progress.done / progress.total)\n}\n\n/**\n * Reports whether a running request went quiet.\n *\n * True means that the pod which held the run stopped, or lost the row. It does\n * NOT mean that the action failed: the reconciler either runs it again, or it\n * writes `failed` so that a person can. See `APPROVAL_RUN_STALE_MS`.\n */\nexport function isRunStalled(job: ApprovalJobDto, now: Date): boolean {\n if (job.status !== 'executing') {\n return false\n }\n const lastSign = job.progress?.updatedAt ?? job.executingSince\n if (!lastSign) {\n return false\n }\n return now.getTime() - new Date(lastSign).getTime() > APPROVAL_RUN_STALE_MS\n}\n\n/** One approval request, as the API returns it. */\nexport interface ApprovalJobDto {\n id: string\n type: string\n dedupeKey?: string\n payload: Record<string, unknown>\n /**\n * The payload as labelled fields. See `ApprovalSummaryField`.\n *\n * The use case builds it when the request is opened AND every time the\n * request is read, so a field that a later version of the use case adds\n * appears on a request that is already open.\n */\n summary: ApprovalSummaryField[]\n /** Why the person opened the request. Some use cases demand one. */\n reason?: string\n createdBy: ApprovalActor\n acks: ApprovalAck[]\n /**\n * How many approvals THIS request needs.\n *\n * It is on the request and not only on the use case, because the person who\n * opens one can ask for more than the floor of the use case. A bulk\n * operation over 3000 clusters is the same action as one over 3, and it can\n * still deserve a third pair of eyes. `ApprovalUseCaseDto.minAcks` is the\n * floor that a caller cannot go below.\n */\n requiredAcks: number\n /**\n * What a person must hold to approve, reject or run THIS request.\n *\n * It is on the request and not only on the use case, because a payload can\n * raise the guard: a generation update with `yoloForceUpdate` needs the yolo\n * permission on top of the base one, and the same use case without the flag\n * does not. The set is computed ONE time, when the request is opened, so it\n * is also the record of what the request was guarded by.\n *\n * A person needs EVERY permission in the set, not one of them.\n */\n requiredPermissions: string[]\n executionMode: ApprovalExecutionMode\n actionMode: ApprovalActionMode\n status: ApprovalJobStatus\n closedBy?: ApprovalActor\n closedReason?: string\n /** An ISO timestamp. */\n expiresAt: string\n /**\n * An ISO timestamp: when the current run took the request.\n *\n * It is set every time a run starts, so a run that the reconciler starts\n * again moves it. `attempts` says how many runs there were.\n */\n executingSince?: string\n /** How many times a run started. A recovery raises it. */\n attempts: number\n /**\n * What the action reports while it runs. See `ApprovalProgress`.\n *\n * It stays on the row after the run, so the record of a finished run says\n * how many targets it touched and how many refused.\n */\n progress?: ApprovalProgress\n executedAt?: string\n executionError?: string\n /**\n * What the action reported when it ran.\n *\n * The shape belongs to the use case. An action over many targets puts the\n * result of each target here, which is the difference between \"it failed\"\n * and \"it did 2998 of 3000, and these two refused\".\n */\n executionResult?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/**\n * What a use case is, as the API reports it.\n *\n * The frontend needs `requiredPermission` to run the same rule as the backend.\n * PR #9301 kept this registry a second time, by hand, in the frontend. A\n * registry that the server owns means a new use case needs no frontend deploy.\n */\nexport interface ApprovalUseCaseDto {\n type: string\n /** A short name for a person to read. */\n label: string\n /** One sentence: what the action does, and what it cannot undo. */\n description: string\n /** The approvals a new request asks for when the caller names no number. */\n defaultAcks: number\n /**\n * The floor. A caller can ask for more approvals, never for fewer.\n *\n * It is a separate number from `defaultAcks` because the two answer\n * different questions: the default is what the form offers, and the floor is\n * what the service refuses to go below.\n */\n minAcks: number\n ttlDays: number\n executionMode: ApprovalExecutionMode\n /** See `ApprovalActionMode`. An action over many targets is `background`. */\n actionMode: ApprovalActionMode\n /**\n * True when a run that was interrupted can start again by itself.\n *\n * It is a promise about the ACTION, and the reconciler reads it. Set it only\n * when the action can run a second time over the same payload and do no harm\n * twice: it must read what the first run already finished, out of\n * `ApprovalJobDto.executionResult`, and skip it.\n *\n * False, the default, is the safe answer. An interrupted run then lands in\n * `failed`, which a person can run again from the detail page, and a person\n * decides whether running it twice is safe.\n */\n resumable: boolean\n /**\n * The permission that every request of this use case needs to approve,\n * reject or run. A payload can add more. See\n * `ApprovalJobDto.requiredPermissions`, which is the effective set.\n *\n * It is also the permission to SEE a request of this use case, because a\n * person who cannot act on one has no reason to read it. The person who\n * opened a request always sees that request.\n */\n requiredPermission: string\n /**\n * The permission to open a request. Absent means any signed-in Camundi.\n *\n * Set it when the pool that can ASK must be as small as the pool that can\n * approve. Leave it absent when the approvals alone are the guard, and the\n * proposal is not.\n */\n createPermission?: string\n /** True when a request cannot be opened without a reason. */\n reasonRequired: boolean\n /**\n * True when the ACTION needs the email address of the person who asked.\n *\n * A token can carry no address, and the row then stores\n * `UNKNOWN_ACTOR_EMAIL`. An action that sends that placeholder to another\n * service asks it to find a user that does not exist, so the request can\n * only fail \u2014 and it fails at the END, after other people spent their\n * approvals on it. A use case that carries the creator into its call sets\n * this, and `canCreate` then refuses such a caller at the start.\n *\n * `extend-trial` is one: accounts makes the creator the actor of the\n * extension, through `onBehalfOf`.\n */\n creatorEmailRequired?: boolean\n}\n"],
5
+ "mappings": "AA0BO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAmBO,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF;AAUO,MAAM,6BAA6B;AAcnC,MAAM,yBAAyB;AAiE/B,MAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAoCO,MAAM,wBAAwB;AA8B9B,SAAS,iBACd,UACoB;AACpB,MAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO,SAAS,KAAK;AACnD;AASO,SAAS,aAAa,KAAqB,KAAoB;AACpE,MAAI,IAAI,WAAW,aAAa;AAC9B,WAAO;AAAA,EACT;AACA,QAAM,WAAW,IAAI,UAAU,aAAa,IAAI;AAChD,MAAI,CAAC,UAAU;AACb,WAAO;AAAA,EACT;AACA,SAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,QAAQ,EAAE,QAAQ,IAAI;AACxD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * What a move of a cluster to another organization did not repair. The move
3
+ * keeps going when a warning occurs. A Camundi reads the warnings and decides.
4
+ */
5
+ export interface MoveClusterWarnings {
6
+ /**
7
+ * The plan type of the cluster, when the sales plan of the target
8
+ * organization does not configure it. The cluster keeps running and stays
9
+ * manageable. That organization cannot create another cluster of this plan
10
+ * type and cannot duplicate this one, because only a POST route mounts
11
+ * `validPlanTypeValidator`. `id` supports a search of a support case.
12
+ * `name` is what the dialog shows.
13
+ */
14
+ planTypeNotConfigured?: {
15
+ id: string;
16
+ name: string;
17
+ };
18
+ /** True when the cluster holds an external IdP configuration. */
19
+ externalIdp: boolean;
20
+ /**
21
+ * The k8s objects of the cluster that carry the organization in a label:
22
+ * Backup, BackupSchedule, ExternalMonitoring, and PrivateConnectivity
23
+ * custom resources. The move patches each one's `orgId` label before it
24
+ * writes the database row for a backup. `failed` lists each object whose
25
+ * label patch failed, with its kind and name. For a failed Backup, a watch
26
+ * reconnect can write the old organization into its database row again. A
27
+ * Camundi must patch a failed label by hand.
28
+ *
29
+ * A cluster does not always have every kind of object. A cluster with no
30
+ * monitoring, or no private connectivity, counts as nothing for that kind,
31
+ * the same way a cluster with no backup counts as nothing.
32
+ */
33
+ orgLabels: {
34
+ patched: number;
35
+ failed: {
36
+ kind: string;
37
+ name: string;
38
+ error: string;
39
+ }[];
40
+ };
41
+ alertSubscriptions: {
42
+ kept: {
43
+ report: string;
44
+ target: string;
45
+ }[];
46
+ deleted: {
47
+ target: string;
48
+ }[];
49
+ failed: {
50
+ target: string;
51
+ error: string;
52
+ }[];
53
+ /**
54
+ * True when console could not read the subscriptions of the cluster. The
55
+ * three lists are then empty, and empty is not proof that the cluster holds
56
+ * no subscription. A Camundi must look at the subscriptions by hand.
57
+ */
58
+ readFailed: boolean;
59
+ /**
60
+ * True when console read the subscriptions but could not read the members
61
+ * of the target organization, thus it could not check who still belongs to
62
+ * it. Every subscription is then kept. A person outside the target
63
+ * organization can still get alerts for this cluster. A Camundi must check
64
+ * the membership by hand.
65
+ */
66
+ membershipCheckFailed: boolean;
67
+ };
68
+ }
69
+ export interface MoveClusterOwnerResult {
70
+ clusterId: string;
71
+ fromOrgId: string;
72
+ toOrgId: string;
73
+ warnings: MoveClusterWarnings;
74
+ /**
75
+ * True when the cluster already belonged to `toOrgId` before this request.
76
+ * The request repaired the k8s org labels and the `Backup.orgId` rows, and
77
+ * it moved nothing else. It ran no guard, no owner write, no audit entry,
78
+ * and no alert-subscription prune.
79
+ */
80
+ ownerUnchanged: boolean;
81
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=ClusterOwnerMove.dto.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -36,7 +36,7 @@ export interface Features {
36
36
  };
37
37
  control: {
38
38
  frontendTelemetry: boolean;
39
- sendToExpiredTrialApproval: boolean;
39
+ approvals: boolean;
40
40
  };
41
41
  accounts: {
42
42
  moveClusterOrg: boolean;
@@ -10,8 +10,25 @@ export interface GenerationDto {
10
10
  updatedBy: string;
11
11
  updated: Date;
12
12
  isUnsupportedStarting?: Date;
13
+ /**
14
+ * Archived generations stay in the database for the cluster audit history,
15
+ * but nothing offers them: pickers, filter facets, cluster creation and the
16
+ * upgrade readers all leave them out. Do not treat an archived generation
17
+ * as a normal upgrade option. The stored upgrade edges are unchanged, so an
18
+ * explicit upgrade by uuid over an existing edge still runs.
19
+ */
20
+ isArchived?: boolean;
13
21
  isBespoke?: boolean;
14
22
  }
23
+ /**
24
+ * Drop archived generations from a picker list. Uuids in `keep` stay in the
25
+ * result even when archived, so a selection made before archiving does not
26
+ * vanish from the control that shows it.
27
+ */
28
+ export declare function activeGenerations<T extends {
29
+ uuid: string;
30
+ isArchived?: boolean;
31
+ }>(generations: T[], keep?: Iterable<string>): T[];
15
32
  export interface GenerationWithClusterCountDto extends GenerationDto {
16
33
  clusterCount: number;
17
34
  }
@@ -1,5 +1,11 @@
1
1
  import semver from "semver";
2
2
  import { stripSecfixPostfix } from "../utils/versionCheck.utils.js";
3
+ function activeGenerations(generations, keep = []) {
4
+ const keepSet = new Set(keep);
5
+ return generations.filter(
6
+ (generation) => generation.isArchived !== true || keepSet.has(generation.uuid)
7
+ );
8
+ }
3
9
  const secfixRank = (version) => version === stripSecfixPostfix(version) ? 0 : 1;
4
10
  function sortGenerationBySemverOfZeebe(generationA, generationB) {
5
11
  const versionAvalid = generationA.versions?.zeebe !== void 0 && semver.valid(generationA.versions.zeebe.split(":")[1]) !== null;
@@ -57,6 +63,7 @@ const compareGenerationNamesNewestFirst = (nameA, nameB) => {
57
63
  return suffixB.localeCompare(suffixA, void 0, collation);
58
64
  };
59
65
  export {
66
+ activeGenerations,
60
67
  compareGenerationNamesNewestFirst,
61
68
  sortGenerationBySemverOfZeebe
62
69
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/dtos/Generations.dto.ts"],
4
- "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport semver from 'semver'\nimport type { ZeebeClusterVersionsDto } from '../types/C8ClusterComponentVersions.type.js'\nimport { stripSecfixPostfix } from '../utils/versionCheck.utils.js'\n\nexport interface GenerationDto {\n name: string\n uuid: string\n versions: ZeebeClusterVersionsDto\n upgradeableFrom: GenerationDto[]\n createdBy: string\n createdByName?: string\n created: Date\n updatedBy: string\n updated: Date\n isUnsupportedStarting?: Date\n isBespoke?: boolean\n}\n\nexport interface GenerationWithClusterCountDto extends GenerationDto {\n clusterCount: number\n}\n\nexport interface UpgradableGenerationsDto {\n preferred: GenerationDto | null\n options: GenerationDto[]\n}\n\nconst secfixRank = (version: string): number =>\n version === stripSecfixPostfix(version) ? 0 : 1\n\nexport function sortGenerationBySemverOfZeebe(\n generationA: { name: string; versions?: { zeebe: string } },\n generationB: { name: string; versions?: { zeebe: string } },\n) {\n // magic sorting algorithm\n // uses the generation version of zeebe for comparison\n // if version invalid: use localeCompare\n\n const versionAvalid: boolean =\n generationA.versions?.zeebe !== undefined &&\n semver.valid(generationA.versions.zeebe.split(':')[1]) !== null\n const versionBvalid: boolean =\n generationB.versions?.zeebe !== undefined &&\n semver.valid(generationB.versions.zeebe.split(':')[1]) !== null\n\n if (versionAvalid && versionBvalid) {\n const zeebeA = generationA.versions!.zeebe.split(':')[1]\n const zeebeB = generationB.versions!.zeebe.split(':')[1]\n const byVersion = semver.compare(\n stripSecfixPostfix(zeebeB),\n stripSecfixPostfix(zeebeA),\n )\n if (byVersion !== 0) {\n return byVersion\n }\n // Both builds carry the same Camunda version here, so only the postfix\n // separates them. A secfix build holds that version plus a security patch,\n // thus it is the newer of the two and takes the higher rank.\n const bySecfix = secfixRank(zeebeB) - secfixRank(zeebeA)\n if (bySecfix !== 0) {\n return bySecfix\n }\n // Same image on both (8.9+gen5 and 8.9+gen6 both run 8.9.5). The name is\n // the only order left, so a picker does not show them in input order.\n return compareGenerationNamesNewestFirst(generationA.name, generationB.name)\n } else if (versionAvalid) {\n return -1\n } else if (versionBvalid) {\n return 1\n } else {\n return compareGenerationNamesNewestFirst(generationA.name, generationB.name)\n }\n}\n\n/** The text before the first digit: \"Camunda \" for \"Camunda 8.9+gen20\". */\nconst productPrefix = (name: string): string => {\n const firstDigit = name.search(/\\d/)\n return firstDigit === -1 ? name : name.slice(0, firstDigit)\n}\n\n/**\n * Splits a name at the first dash after the version: \"Camunda 8.10.0-alpha1\"\n * gives [\"Camunda 8.10.0\", \"alpha1\"]. A name without a dash gives an empty\n * suffix.\n */\nconst splitPrerelease = (name: string): [base: string, suffix: string] => {\n const dash = name.indexOf('-', name.search(/\\d/))\n return dash === -1 ? [name, ''] : [name.slice(0, dash), name.slice(dash + 1)]\n}\n\n/**\n * Orders generation names by product, then newest first within a product, by\n * numeric collation: `8.10` comes before `8.9`, and `gen10` before `gen9`.\n * Products stay in alphabetic order, so the Camunda generations come before\n * the older Zeebe ones. A release comes before its prereleases, so\n * `8.10.0` is above `8.10.0-alpha1`. For a list that has no zeebe image to\n * compare, such as a filter facet. When the image is known, use\n * `sortGenerationBySemverOfZeebe`.\n */\nexport const compareGenerationNamesNewestFirst = (\n nameA: string,\n nameB: string,\n): number => {\n const collation = { numeric: true, sensitivity: 'base' } as const\n const byProduct = productPrefix(nameA).localeCompare(\n productPrefix(nameB),\n undefined,\n collation,\n )\n if (byProduct !== 0) {\n return byProduct\n }\n const [baseA, suffixA] = splitPrerelease(nameA)\n const [baseB, suffixB] = splitPrerelease(nameB)\n const byBase = baseB.localeCompare(baseA, undefined, collation)\n if (byBase !== 0) {\n return byBase\n }\n if (suffixA === '' || suffixB === '') {\n return suffixA === suffixB ? 0 : suffixA === '' ? -1 : 1\n }\n return suffixB.localeCompare(suffixA, undefined, collation)\n}\n"],
5
- "mappings": "AAMA,OAAO,YAAY;AAEnB,SAAS,0BAA0B;AAyBnC,MAAM,aAAa,CAAC,YAClB,YAAY,mBAAmB,OAAO,IAAI,IAAI;AAEzC,SAAS,8BACd,aACA,aACA;AAKA,QAAM,gBACJ,YAAY,UAAU,UAAU,UAChC,OAAO,MAAM,YAAY,SAAS,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,MAAM;AAC7D,QAAM,gBACJ,YAAY,UAAU,UAAU,UAChC,OAAO,MAAM,YAAY,SAAS,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,MAAM;AAE7D,MAAI,iBAAiB,eAAe;AAClC,UAAM,SAAS,YAAY,SAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AACvD,UAAM,SAAS,YAAY,SAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AACvD,UAAM,YAAY,OAAO;AAAA,MACvB,mBAAmB,MAAM;AAAA,MACzB,mBAAmB,MAAM;AAAA,IAC3B;AACA,QAAI,cAAc,GAAG;AACnB,aAAO;AAAA,IACT;AAIA,UAAM,WAAW,WAAW,MAAM,IAAI,WAAW,MAAM;AACvD,QAAI,aAAa,GAAG;AAClB,aAAO;AAAA,IACT;AAGA,WAAO,kCAAkC,YAAY,MAAM,YAAY,IAAI;AAAA,EAC7E,WAAW,eAAe;AACxB,WAAO;AAAA,EACT,WAAW,eAAe;AACxB,WAAO;AAAA,EACT,OAAO;AACL,WAAO,kCAAkC,YAAY,MAAM,YAAY,IAAI;AAAA,EAC7E;AACF;AAGA,MAAM,gBAAgB,CAAC,SAAyB;AAC9C,QAAM,aAAa,KAAK,OAAO,IAAI;AACnC,SAAO,eAAe,KAAK,OAAO,KAAK,MAAM,GAAG,UAAU;AAC5D;AAOA,MAAM,kBAAkB,CAAC,SAAiD;AACxE,QAAM,OAAO,KAAK,QAAQ,KAAK,KAAK,OAAO,IAAI,CAAC;AAChD,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,MAAM,GAAG,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,CAAC;AAC9E;AAWO,MAAM,oCAAoC,CAC/C,OACA,UACW;AACX,QAAM,YAAY,EAAE,SAAS,MAAM,aAAa,OAAO;AACvD,QAAM,YAAY,cAAc,KAAK,EAAE;AAAA,IACrC,cAAc,KAAK;AAAA,IACnB;AAAA,IACA;AAAA,EACF;AACA,MAAI,cAAc,GAAG;AACnB,WAAO;AAAA,EACT;AACA,QAAM,CAAC,OAAO,OAAO,IAAI,gBAAgB,KAAK;AAC9C,QAAM,CAAC,OAAO,OAAO,IAAI,gBAAgB,KAAK;AAC9C,QAAM,SAAS,MAAM,cAAc,OAAO,QAAW,SAAS;AAC9D,MAAI,WAAW,GAAG;AAChB,WAAO;AAAA,EACT;AACA,MAAI,YAAY,MAAM,YAAY,IAAI;AACpC,WAAO,YAAY,UAAU,IAAI,YAAY,KAAK,KAAK;AAAA,EACzD;AACA,SAAO,QAAQ,cAAc,SAAS,QAAW,SAAS;AAC5D;",
4
+ "sourcesContent": ["/*\n * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH\n * under one or more contributor license agreements. Licensed under a commercial license.\n * You may not use this file except in compliance with the commercial license.\n */\n\nimport semver from 'semver'\nimport type { ZeebeClusterVersionsDto } from '../types/C8ClusterComponentVersions.type.js'\nimport { stripSecfixPostfix } from '../utils/versionCheck.utils.js'\n\nexport interface GenerationDto {\n name: string\n uuid: string\n versions: ZeebeClusterVersionsDto\n upgradeableFrom: GenerationDto[]\n createdBy: string\n createdByName?: string\n created: Date\n updatedBy: string\n updated: Date\n isUnsupportedStarting?: Date\n /**\n * Archived generations stay in the database for the cluster audit history,\n * but nothing offers them: pickers, filter facets, cluster creation and the\n * upgrade readers all leave them out. Do not treat an archived generation\n * as a normal upgrade option. The stored upgrade edges are unchanged, so an\n * explicit upgrade by uuid over an existing edge still runs.\n */\n isArchived?: boolean\n isBespoke?: boolean\n}\n\n/**\n * Drop archived generations from a picker list. Uuids in `keep` stay in the\n * result even when archived, so a selection made before archiving does not\n * vanish from the control that shows it.\n */\nexport function activeGenerations<\n T extends { uuid: string; isArchived?: boolean },\n>(generations: T[], keep: Iterable<string> = []): T[] {\n const keepSet = new Set(keep)\n return generations.filter(\n (generation) =>\n generation.isArchived !== true || keepSet.has(generation.uuid),\n )\n}\n\nexport interface GenerationWithClusterCountDto extends GenerationDto {\n clusterCount: number\n}\n\nexport interface UpgradableGenerationsDto {\n preferred: GenerationDto | null\n options: GenerationDto[]\n}\n\nconst secfixRank = (version: string): number =>\n version === stripSecfixPostfix(version) ? 0 : 1\n\nexport function sortGenerationBySemverOfZeebe(\n generationA: { name: string; versions?: { zeebe: string } },\n generationB: { name: string; versions?: { zeebe: string } },\n) {\n // magic sorting algorithm\n // uses the generation version of zeebe for comparison\n // if version invalid: use localeCompare\n\n const versionAvalid: boolean =\n generationA.versions?.zeebe !== undefined &&\n semver.valid(generationA.versions.zeebe.split(':')[1]) !== null\n const versionBvalid: boolean =\n generationB.versions?.zeebe !== undefined &&\n semver.valid(generationB.versions.zeebe.split(':')[1]) !== null\n\n if (versionAvalid && versionBvalid) {\n const zeebeA = generationA.versions!.zeebe.split(':')[1]\n const zeebeB = generationB.versions!.zeebe.split(':')[1]\n const byVersion = semver.compare(\n stripSecfixPostfix(zeebeB),\n stripSecfixPostfix(zeebeA),\n )\n if (byVersion !== 0) {\n return byVersion\n }\n // Both builds carry the same Camunda version here, so only the postfix\n // separates them. A secfix build holds that version plus a security patch,\n // thus it is the newer of the two and takes the higher rank.\n const bySecfix = secfixRank(zeebeB) - secfixRank(zeebeA)\n if (bySecfix !== 0) {\n return bySecfix\n }\n // Same image on both (8.9+gen5 and 8.9+gen6 both run 8.9.5). The name is\n // the only order left, so a picker does not show them in input order.\n return compareGenerationNamesNewestFirst(generationA.name, generationB.name)\n } else if (versionAvalid) {\n return -1\n } else if (versionBvalid) {\n return 1\n } else {\n return compareGenerationNamesNewestFirst(generationA.name, generationB.name)\n }\n}\n\n/** The text before the first digit: \"Camunda \" for \"Camunda 8.9+gen20\". */\nconst productPrefix = (name: string): string => {\n const firstDigit = name.search(/\\d/)\n return firstDigit === -1 ? name : name.slice(0, firstDigit)\n}\n\n/**\n * Splits a name at the first dash after the version: \"Camunda 8.10.0-alpha1\"\n * gives [\"Camunda 8.10.0\", \"alpha1\"]. A name without a dash gives an empty\n * suffix.\n */\nconst splitPrerelease = (name: string): [base: string, suffix: string] => {\n const dash = name.indexOf('-', name.search(/\\d/))\n return dash === -1 ? [name, ''] : [name.slice(0, dash), name.slice(dash + 1)]\n}\n\n/**\n * Orders generation names by product, then newest first within a product, by\n * numeric collation: `8.10` comes before `8.9`, and `gen10` before `gen9`.\n * Products stay in alphabetic order, so the Camunda generations come before\n * the older Zeebe ones. A release comes before its prereleases, so\n * `8.10.0` is above `8.10.0-alpha1`. For a list that has no zeebe image to\n * compare, such as a filter facet. When the image is known, use\n * `sortGenerationBySemverOfZeebe`.\n */\nexport const compareGenerationNamesNewestFirst = (\n nameA: string,\n nameB: string,\n): number => {\n const collation = { numeric: true, sensitivity: 'base' } as const\n const byProduct = productPrefix(nameA).localeCompare(\n productPrefix(nameB),\n undefined,\n collation,\n )\n if (byProduct !== 0) {\n return byProduct\n }\n const [baseA, suffixA] = splitPrerelease(nameA)\n const [baseB, suffixB] = splitPrerelease(nameB)\n const byBase = baseB.localeCompare(baseA, undefined, collation)\n if (byBase !== 0) {\n return byBase\n }\n if (suffixA === '' || suffixB === '') {\n return suffixA === suffixB ? 0 : suffixA === '' ? -1 : 1\n }\n return suffixB.localeCompare(suffixA, undefined, collation)\n}\n"],
5
+ "mappings": "AAMA,OAAO,YAAY;AAEnB,SAAS,0BAA0B;AA6B5B,SAAS,kBAEd,aAAkB,OAAyB,CAAC,GAAQ;AACpD,QAAM,UAAU,IAAI,IAAI,IAAI;AAC5B,SAAO,YAAY;AAAA,IACjB,CAAC,eACC,WAAW,eAAe,QAAQ,QAAQ,IAAI,WAAW,IAAI;AAAA,EACjE;AACF;AAWA,MAAM,aAAa,CAAC,YAClB,YAAY,mBAAmB,OAAO,IAAI,IAAI;AAEzC,SAAS,8BACd,aACA,aACA;AAKA,QAAM,gBACJ,YAAY,UAAU,UAAU,UAChC,OAAO,MAAM,YAAY,SAAS,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,MAAM;AAC7D,QAAM,gBACJ,YAAY,UAAU,UAAU,UAChC,OAAO,MAAM,YAAY,SAAS,MAAM,MAAM,GAAG,EAAE,CAAC,CAAC,MAAM;AAE7D,MAAI,iBAAiB,eAAe;AAClC,UAAM,SAAS,YAAY,SAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AACvD,UAAM,SAAS,YAAY,SAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AACvD,UAAM,YAAY,OAAO;AAAA,MACvB,mBAAmB,MAAM;AAAA,MACzB,mBAAmB,MAAM;AAAA,IAC3B;AACA,QAAI,cAAc,GAAG;AACnB,aAAO;AAAA,IACT;AAIA,UAAM,WAAW,WAAW,MAAM,IAAI,WAAW,MAAM;AACvD,QAAI,aAAa,GAAG;AAClB,aAAO;AAAA,IACT;AAGA,WAAO,kCAAkC,YAAY,MAAM,YAAY,IAAI;AAAA,EAC7E,WAAW,eAAe;AACxB,WAAO;AAAA,EACT,WAAW,eAAe;AACxB,WAAO;AAAA,EACT,OAAO;AACL,WAAO,kCAAkC,YAAY,MAAM,YAAY,IAAI;AAAA,EAC7E;AACF;AAGA,MAAM,gBAAgB,CAAC,SAAyB;AAC9C,QAAM,aAAa,KAAK,OAAO,IAAI;AACnC,SAAO,eAAe,KAAK,OAAO,KAAK,MAAM,GAAG,UAAU;AAC5D;AAOA,MAAM,kBAAkB,CAAC,SAAiD;AACxE,QAAM,OAAO,KAAK,QAAQ,KAAK,KAAK,OAAO,IAAI,CAAC;AAChD,SAAO,SAAS,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,MAAM,GAAG,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,CAAC;AAC9E;AAWO,MAAM,oCAAoC,CAC/C,OACA,UACW;AACX,QAAM,YAAY,EAAE,SAAS,MAAM,aAAa,OAAO;AACvD,QAAM,YAAY,cAAc,KAAK,EAAE;AAAA,IACrC,cAAc,KAAK;AAAA,IACnB;AAAA,IACA;AAAA,EACF;AACA,MAAI,cAAc,GAAG;AACnB,WAAO;AAAA,EACT;AACA,QAAM,CAAC,OAAO,OAAO,IAAI,gBAAgB,KAAK;AAC9C,QAAM,CAAC,OAAO,OAAO,IAAI,gBAAgB,KAAK;AAC9C,QAAM,SAAS,MAAM,cAAc,OAAO,QAAW,SAAS;AAC9D,MAAI,WAAW,GAAG;AAChB,WAAO;AAAA,EACT;AACA,MAAI,YAAY,MAAM,YAAY,IAAI;AACpC,WAAO,YAAY,UAAU,IAAI,YAAY,KAAK,KAAK;AAAA,EACzD;AACA,SAAO,QAAQ,cAAc,SAAS,QAAW,SAAS;AAC5D;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -22,6 +22,12 @@ export type SanityFix = {
22
22
  generationName: string;
23
23
  /** When set, the delete must not be offered as selectable (e.g. clusters still run it). */
24
24
  blockedReason?: string;
25
+ } | {
26
+ type: 'archive-generation';
27
+ generationUuid: string;
28
+ generationName: string;
29
+ /** When set, the archive must not be offered as selectable (e.g. it is still an upgrade target). */
30
+ blockedReason?: string;
25
31
  };
26
32
  export interface SanityViolation {
27
33
  ruleId: string;
@@ -27,6 +27,7 @@ export declare enum SystemAuditEntityAttribute {
27
27
  GENERATION_UPGRADEABLE_TO = "Upgradeable To",
28
28
  GENERATION_SUPPORTED_UNTIL = "Supported Until",
29
29
  GENERATION_BESPOKE = "Bespoke",
30
+ GENERATION_ARCHIVED = "Archived",
30
31
  CHANNEL_NAME = "Name",
31
32
  CHANNEL_DESCRIPTION = "Description",
32
33
  CHANNEL_DEFAULT_GENERATION = "Default Generation",
@@ -21,6 +21,7 @@ var SystemAuditEntityAttribute = /* @__PURE__ */ ((SystemAuditEntityAttribute2)
21
21
  SystemAuditEntityAttribute2["GENERATION_UPGRADEABLE_TO"] = "Upgradeable To";
22
22
  SystemAuditEntityAttribute2["GENERATION_SUPPORTED_UNTIL"] = "Supported Until";
23
23
  SystemAuditEntityAttribute2["GENERATION_BESPOKE"] = "Bespoke";
24
+ SystemAuditEntityAttribute2["GENERATION_ARCHIVED"] = "Archived";
24
25
  SystemAuditEntityAttribute2["CHANNEL_NAME"] = "Name";
25
26
  SystemAuditEntityAttribute2["CHANNEL_DESCRIPTION"] = "Description";
26
27
  SystemAuditEntityAttribute2["CHANNEL_DEFAULT_GENERATION"] = "Default Generation";