@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.
- package/dist/_tests/permissions/session-permissions.spec.d.ts +1 -0
- package/dist/approvals/policy.d.ts +94 -5
- package/dist/approvals/policy.js +64 -14
- package/dist/approvals/policy.js.map +2 -2
- package/dist/approvals/use-cases.const.d.ts +41 -0
- package/dist/approvals/use-cases.const.js +12 -0
- package/dist/approvals/use-cases.const.js.map +7 -0
- package/dist/dtos/AlertSubscription.dto.d.ts +6 -0
- package/dist/dtos/AlertSubscription.dto.js.map +2 -2
- package/dist/dtos/ApprovalJob.dto.d.ts +258 -4
- package/dist/dtos/ApprovalJob.dto.js +42 -1
- package/dist/dtos/ApprovalJob.dto.js.map +2 -2
- package/dist/dtos/ClusterOwnerMove.dto.d.ts +81 -0
- package/dist/dtos/ClusterOwnerMove.dto.js +1 -0
- package/dist/dtos/ClusterOwnerMove.dto.js.map +7 -0
- package/dist/dtos/FeatureFlags.dto.d.ts +1 -1
- package/dist/dtos/Generations.dto.d.ts +17 -0
- package/dist/dtos/Generations.dto.js +7 -0
- package/dist/dtos/Generations.dto.js.map +2 -2
- package/dist/dtos/Generations.dto.spec.d.ts +1 -0
- package/dist/dtos/ReleaseTrainSanity.dto.d.ts +6 -0
- package/dist/enums/SystemAuditEntity.enum.d.ts +1 -0
- package/dist/enums/SystemAuditEntity.enum.js +1 -0
- package/dist/enums/SystemAuditEntity.enum.js.map +2 -2
- package/dist/index.d.ts +9 -6
- package/dist/index.js +45 -3
- package/dist/index.js.map +2 -2
- package/dist/permissions/combo.d.ts +9 -0
- package/dist/permissions/combo.js +54 -1
- package/dist/permissions/combo.js.map +2 -2
- package/dist/utils/versionCheck.utils.d.ts +9 -0
- package/dist/utils/versionCheck.utils.js +7 -2
- package/dist/utils/versionCheck.utils.js.map +2 -2
- 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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
@@ -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;
|
|
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";
|