@things-factory/auth-ui 10.1.256 → 10.1.258
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/bootstrap.js +2 -2
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.d.ts +9 -8
- package/dist-client/components/access-decision-popup.js +46 -58
- package/dist-client/components/access-decision-popup.js.map +1 -1
- package/dist-client/components/access-request-popup.d.ts +13 -8
- package/dist-client/components/access-request-popup.js +78 -102
- package/dist-client/components/access-request-popup.js.map +1 -1
- package/dist-client/components/app-binding-detail.d.ts +7 -5
- package/dist-client/components/app-binding-detail.js +62 -82
- package/dist-client/components/app-binding-detail.js.map +1 -1
- package/dist-client/components/appliance-editor.d.ts +10 -9
- package/dist-client/components/appliance-editor.js +87 -138
- package/dist-client/components/appliance-editor.js.map +1 -1
- package/dist-client/components/application-editor.d.ts +7 -5
- package/dist-client/components/application-editor.js +58 -92
- package/dist-client/components/application-editor.js.map +1 -1
- package/dist-client/components/my-login-history.js +18 -40
- package/dist-client/components/my-login-history.js.map +1 -1
- package/dist-client/components/my-passkeys.d.ts +4 -3
- package/dist-client/components/my-passkeys.js +29 -51
- package/dist-client/components/my-passkeys.js.map +1 -1
- package/dist-client/components/my-roles.d.ts +3 -4
- package/dist-client/components/my-roles.js +36 -55
- package/dist-client/components/my-roles.js.map +1 -1
- package/dist-client/components/partner-role-editor.js +27 -50
- package/dist-client/components/partner-role-editor.js.map +1 -1
- package/dist-client/components/profile-component.d.ts +2 -2
- package/dist-client/components/profile-component.js +18 -26
- package/dist-client/components/profile-component.js.map +1 -1
- package/dist-client/components/role-template-picker.d.ts +3 -3
- package/dist-client/components/role-template-picker.js +28 -42
- package/dist-client/components/role-template-picker.js.map +1 -1
- package/dist-client/components/user-detail.d.ts +23 -22
- package/dist-client/components/user-detail.js +125 -179
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/components/user-invite.d.ts +8 -6
- package/dist-client/components/user-invite.js +106 -141
- package/dist-client/components/user-invite.js.map +1 -1
- package/dist-client/components/user-popups-standard.test.js +1 -1
- package/dist-client/components/user-popups-standard.test.js.map +1 -1
- package/dist-client/components/user-role-editor.js +72 -122
- package/dist-client/components/user-role-editor.js.map +1 -1
- package/dist-client/entity360/person-360.d.ts +8 -11
- package/dist-client/entity360/person-360.js +49 -68
- package/dist-client/entity360/person-360.js.map +1 -1
- package/dist-client/entries/oauth2/oauth2-decision-page.js +13 -25
- package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
- package/dist-client/identity-confirmation/identity-confirmation.d.ts +13 -14
- package/dist-client/identity-confirmation/identity-confirmation.js +39 -45
- package/dist-client/identity-confirmation/identity-confirmation.js.map +1 -1
- package/dist-client/pages/audit/audit-page.d.ts +4 -4
- package/dist-client/pages/audit/audit-page.js +28 -39
- package/dist-client/pages/audit/audit-page.js.map +1 -1
- package/dist-client/pages/domain-link/domain-link-management.js +1 -1
- package/dist-client/pages/domain-link/domain-link-management.js.map +1 -1
- package/dist-client/pages/partner/partner-management.js +30 -68
- package/dist-client/pages/partner/partner-management.js.map +1 -1
- package/dist-client/pages/role/role-extra-removal.test.d.mts +1 -0
- package/dist-client/pages/role/role-extra-removal.test.mjs +73 -0
- package/dist-client/pages/role/role-extra-removal.test.mjs.map +1 -0
- package/dist-client/pages/role/role-list-page.js +20 -22
- package/dist-client/pages/role/role-list-page.js.map +1 -1
- package/dist-client/pages/role/role-privilege-page.d.ts +36 -8
- package/dist-client/pages/role/role-privilege-page.js +216 -158
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/pages/role/role-task-review.test.js +20 -16
- package/dist-client/pages/role/role-task-review.test.js.map +1 -1
- package/dist-client/pages/role/role-template-gap.test.js +8 -3
- package/dist-client/pages/role/role-template-gap.test.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/helps/auth/roles.en.md +13 -49
- package/helps/auth/roles.ko.md +13 -32
- package/package.json +11 -10
- package/tests/page-state.test.ts +2 -1
- package/tests/password-rule-shown.test.ts +2 -2
- package/tests/person-360-sections.test.ts +9 -5
- package/translations/en.json +5 -1
- package/translations/es.json +5 -1
- package/translations/ja.json +5 -1
- package/translations/ko.json +5 -1
- package/translations/ms.json +5 -1
- package/translations/zh.json +5 -1
|
@@ -7,7 +7,7 @@ import gql from 'graphql-tag';
|
|
|
7
7
|
import { css, html, LitElement } from 'lit';
|
|
8
8
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
9
9
|
import { asyncReplace } from 'lit/directives/async-replace.js';
|
|
10
|
-
import {
|
|
10
|
+
import { MutationController, QueryController } from '@operato/graphql';
|
|
11
11
|
import { i18next, localize } from '@operato/i18n';
|
|
12
12
|
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
13
13
|
import { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles';
|
|
@@ -51,23 +51,50 @@ const APPLIANCE_FIELDS = `
|
|
|
51
51
|
let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement) {
|
|
52
52
|
constructor() {
|
|
53
53
|
super(...arguments);
|
|
54
|
-
/** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */
|
|
55
|
-
this.appliance = null;
|
|
56
54
|
/** Required fields still empty. Filled in when the person presses save, not while typing. */
|
|
57
55
|
this.missing = [];
|
|
58
|
-
/** The
|
|
59
|
-
this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
/** The record as a new secret or a revocation left it - the panel shows the result and stays open. */
|
|
57
|
+
this.changed = null;
|
|
58
|
+
this.read = new QueryController(this, gql `
|
|
59
|
+
query ($id: String!) {
|
|
60
|
+
appliance(id: $id) { ${APPLIANCE_FIELDS} }
|
|
61
|
+
}
|
|
62
|
+
`, { variables: () => (this.applianceId ? { id: this.applianceId } : undefined) });
|
|
63
|
+
this.revocation = new QueryController(this, gql `
|
|
64
|
+
query {
|
|
65
|
+
applianceCredentialRevocable
|
|
66
|
+
}
|
|
67
|
+
`, { variables: () => (this.applianceId ? {} : undefined) });
|
|
68
|
+
/* A refused save: the shared link says why, `failed` says the press landed. */
|
|
69
|
+
this.saves = {
|
|
70
|
+
create: new MutationController(this, gql `
|
|
71
|
+
mutation ($appliance: NewAppliance!) {
|
|
72
|
+
createAppliance(appliance: $appliance) {
|
|
73
|
+
id
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`),
|
|
77
|
+
update: new MutationController(this, gql `
|
|
78
|
+
mutation ($id: String!, $patch: AppliancePatch!) {
|
|
79
|
+
updateAppliance(id: $id, patch: $patch) { ${APPLIANCE_FIELDS} }
|
|
80
|
+
}
|
|
81
|
+
`),
|
|
82
|
+
secret: new MutationController(this, gql `
|
|
83
|
+
mutation ($id: String!) {
|
|
84
|
+
generateApplianceSecret(id: $id) { ${APPLIANCE_FIELDS} }
|
|
85
|
+
}
|
|
86
|
+
`),
|
|
87
|
+
revoke: new MutationController(this, gql `
|
|
88
|
+
mutation ($id: String!) {
|
|
89
|
+
revokeApplianceCredential(id: $id) { ${APPLIANCE_FIELDS} }
|
|
90
|
+
}
|
|
91
|
+
`),
|
|
92
|
+
delete: new MutationController(this, gql `
|
|
93
|
+
mutation ($id: String!) {
|
|
94
|
+
deleteAppliance(id: $id)
|
|
95
|
+
}
|
|
96
|
+
`)
|
|
97
|
+
};
|
|
71
98
|
}
|
|
72
99
|
/*
|
|
73
100
|
* ── Where the scrolling happens ─────────────────────────────────────────
|
|
@@ -187,6 +214,35 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
187
214
|
}
|
|
188
215
|
`
|
|
189
216
|
]; }
|
|
217
|
+
/** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */
|
|
218
|
+
get appliance() {
|
|
219
|
+
return this.changed ?? this.read.data?.appliance ?? null;
|
|
220
|
+
}
|
|
221
|
+
/** The mutation was refused. The shared link already said why; this says the press landed. */
|
|
222
|
+
get failed() {
|
|
223
|
+
return Object.values(this.saves).some(save => save.error);
|
|
224
|
+
}
|
|
225
|
+
/*
|
|
226
|
+
* The single-record read did not answer. Distinct from answering with nothing.
|
|
227
|
+
*
|
|
228
|
+
* A refusal arrives as `error` rather than throwing. Reading `data.appliance` without checking
|
|
229
|
+
* would show an empty form that looks like a record with every field blank — and saving it would
|
|
230
|
+
* then write those blanks.
|
|
231
|
+
*/
|
|
232
|
+
get loadFailed() {
|
|
233
|
+
return !!this.read.error || (!!this.read.data && !this.read.data.appliance);
|
|
234
|
+
}
|
|
235
|
+
/*
|
|
236
|
+
* Does revoking work on this installation, for this reader?
|
|
237
|
+
*
|
|
238
|
+
* Revocation is behind a configuration switch, and the server answers one question that folds
|
|
239
|
+
* the switch and the reader's privilege together — see `applianceCredentialRevocable`. False
|
|
240
|
+
* until that answer comes back, so a slow or refused read shows no button rather than one that
|
|
241
|
+
* refuses when pressed - a refusal and a `false` mean the same thing to this screen.
|
|
242
|
+
*/
|
|
243
|
+
get revocable() {
|
|
244
|
+
return readRevocable(this.revocation.error ? null : { data: this.revocation.data });
|
|
245
|
+
}
|
|
190
246
|
get editing() {
|
|
191
247
|
return !!this.applianceId;
|
|
192
248
|
}
|
|
@@ -335,50 +391,6 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
335
391
|
fieldError(name) {
|
|
336
392
|
return this.missing.includes(name) ? html `<div field-error>${i18next.t('text.this field is required')}</div>` : '';
|
|
337
393
|
}
|
|
338
|
-
async connectedCallback() {
|
|
339
|
-
super.connectedCallback();
|
|
340
|
-
if (this.editing) {
|
|
341
|
-
await Promise.all([this.fetchAppliance(), this.fetchRevocable()]);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
/*
|
|
345
|
-
* Whether the revoke button exists at all.
|
|
346
|
-
*
|
|
347
|
-
* A refusal arrives as `response.errors` rather than a throw, and both a refusal and a `false`
|
|
348
|
-
* mean the same thing to this screen: no button. So there is nothing to report here — the
|
|
349
|
-
* absence of the button is the whole answer.
|
|
350
|
-
*/
|
|
351
|
-
async fetchRevocable() {
|
|
352
|
-
const response = await client.query({
|
|
353
|
-
query: gql `
|
|
354
|
-
query {
|
|
355
|
-
applianceCredentialRevocable
|
|
356
|
-
}
|
|
357
|
-
`
|
|
358
|
-
});
|
|
359
|
-
this.revocable = readRevocable(response);
|
|
360
|
-
}
|
|
361
|
-
async fetchAppliance() {
|
|
362
|
-
const response = await client.query({
|
|
363
|
-
query: gql `
|
|
364
|
-
query ($id: String!) {
|
|
365
|
-
appliance(id: $id) { ${APPLIANCE_FIELDS} }
|
|
366
|
-
}
|
|
367
|
-
`,
|
|
368
|
-
variables: { id: this.applianceId }
|
|
369
|
-
});
|
|
370
|
-
/*
|
|
371
|
-
* `errorPolicy: 'all'` means a refusal arrives here rather than throwing. Reading
|
|
372
|
-
* `data.appliance` without checking would show an empty form that looks like a record
|
|
373
|
-
* with every field blank — and saving it would then write those blanks.
|
|
374
|
-
*/
|
|
375
|
-
if (response.errors || !response.data?.appliance) {
|
|
376
|
-
this.loadFailed = true;
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
this.loadFailed = false;
|
|
380
|
-
this.appliance = response.data.appliance;
|
|
381
|
-
}
|
|
382
394
|
cancel(e) {
|
|
383
395
|
e.preventDefault();
|
|
384
396
|
this.close();
|
|
@@ -400,21 +412,9 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
400
412
|
return this.editing ? this.updateAppliance(formData) : this.createAppliance(formData);
|
|
401
413
|
}
|
|
402
414
|
async createAppliance(formData) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
mutation ($appliance: NewAppliance!) {
|
|
406
|
-
createAppliance(appliance: $appliance) {
|
|
407
|
-
id
|
|
408
|
-
}
|
|
415
|
+
if (await this.saves.create.run({ appliance: filledValues(formData) })) {
|
|
416
|
+
this.saved();
|
|
409
417
|
}
|
|
410
|
-
`,
|
|
411
|
-
variables: { appliance: filledValues(formData) }
|
|
412
|
-
});
|
|
413
|
-
if (response.errors) {
|
|
414
|
-
this.failed = true;
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
this.saved();
|
|
418
418
|
}
|
|
419
419
|
async updateAppliance(formData) {
|
|
420
420
|
const patch = changedValues(formData, this.appliance);
|
|
@@ -426,19 +426,9 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
426
426
|
this.close();
|
|
427
427
|
return;
|
|
428
428
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
mutation ($id: String!, $patch: AppliancePatch!) {
|
|
432
|
-
updateAppliance(id: $id, patch: $patch) { ${APPLIANCE_FIELDS} }
|
|
433
|
-
}
|
|
434
|
-
`,
|
|
435
|
-
variables: { id: this.applianceId, patch }
|
|
436
|
-
});
|
|
437
|
-
if (response.errors) {
|
|
438
|
-
this.failed = true;
|
|
439
|
-
return;
|
|
429
|
+
if (await this.saves.update.run({ id: this.applianceId, patch })) {
|
|
430
|
+
this.saved();
|
|
440
431
|
}
|
|
441
|
-
this.saved();
|
|
442
432
|
}
|
|
443
433
|
async generateSecret(e) {
|
|
444
434
|
e.preventDefault();
|
|
@@ -452,24 +442,15 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
452
442
|
if (!confirmed) {
|
|
453
443
|
return;
|
|
454
444
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
mutation ($id: String!) {
|
|
458
|
-
generateApplianceSecret(id: $id) { ${APPLIANCE_FIELDS} }
|
|
459
|
-
}
|
|
460
|
-
`,
|
|
461
|
-
variables: { id: this.applianceId }
|
|
462
|
-
});
|
|
463
|
-
if (response.errors) {
|
|
464
|
-
this.failed = true;
|
|
445
|
+
const generated = await this.saves.secret.run({ id: this.applianceId });
|
|
446
|
+
if (!generated) {
|
|
465
447
|
return;
|
|
466
448
|
}
|
|
467
449
|
/*
|
|
468
450
|
* Stays open on purpose. A new token is the one value on this screen the reader has to
|
|
469
451
|
* copy before it is out of reach — closing here would hand it to them and take it away.
|
|
470
452
|
*/
|
|
471
|
-
this.
|
|
472
|
-
this.appliance = response.data.generateApplianceSecret;
|
|
453
|
+
this.changed = generated.generateApplianceSecret;
|
|
473
454
|
this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }));
|
|
474
455
|
}
|
|
475
456
|
/*
|
|
@@ -497,20 +478,11 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
497
478
|
if (!confirmed) {
|
|
498
479
|
return;
|
|
499
480
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
mutation ($id: String!) {
|
|
503
|
-
revokeApplianceCredential(id: $id) { ${APPLIANCE_FIELDS} }
|
|
504
|
-
}
|
|
505
|
-
`,
|
|
506
|
-
variables: { id: this.applianceId }
|
|
507
|
-
});
|
|
508
|
-
if (response.errors) {
|
|
509
|
-
this.failed = true;
|
|
481
|
+
const revoked = await this.saves.revoke.run({ id: this.applianceId });
|
|
482
|
+
if (!revoked) {
|
|
510
483
|
return;
|
|
511
484
|
}
|
|
512
|
-
this.
|
|
513
|
-
this.appliance = response.data.revokeApplianceCredential;
|
|
485
|
+
this.changed = revoked.revokeApplianceCredential;
|
|
514
486
|
this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }));
|
|
515
487
|
}
|
|
516
488
|
async deleteAppliance(e) {
|
|
@@ -525,23 +497,12 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
525
497
|
if (!confirmed) {
|
|
526
498
|
return;
|
|
527
499
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
mutation ($id: String!) {
|
|
531
|
-
deleteAppliance(id: $id)
|
|
532
|
-
}
|
|
533
|
-
`,
|
|
534
|
-
variables: { id: this.applianceId }
|
|
535
|
-
});
|
|
536
|
-
if (response.errors) {
|
|
537
|
-
this.failed = true;
|
|
538
|
-
return;
|
|
500
|
+
if (await this.saves.delete.run({ id: this.applianceId })) {
|
|
501
|
+
this.saved();
|
|
539
502
|
}
|
|
540
|
-
this.saved();
|
|
541
503
|
}
|
|
542
504
|
/** The list has to refetch, and the popup is done. */
|
|
543
505
|
saved() {
|
|
544
|
-
this.failed = false;
|
|
545
506
|
this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }));
|
|
546
507
|
this.close();
|
|
547
508
|
}
|
|
@@ -580,29 +541,17 @@ __decorate([
|
|
|
580
541
|
__decorate([
|
|
581
542
|
query('form'),
|
|
582
543
|
__metadata("design:type", HTMLFormElement
|
|
583
|
-
/**
|
|
544
|
+
/** Required fields still empty. Filled in when the person presses save, not while typing. */
|
|
584
545
|
)
|
|
585
546
|
], ApplianceEditor.prototype, "form", void 0);
|
|
586
|
-
__decorate([
|
|
587
|
-
state(),
|
|
588
|
-
__metadata("design:type", Object)
|
|
589
|
-
], ApplianceEditor.prototype, "appliance", void 0);
|
|
590
547
|
__decorate([
|
|
591
548
|
state(),
|
|
592
549
|
__metadata("design:type", Array)
|
|
593
550
|
], ApplianceEditor.prototype, "missing", void 0);
|
|
594
551
|
__decorate([
|
|
595
552
|
state(),
|
|
596
|
-
__metadata("design:type",
|
|
597
|
-
], ApplianceEditor.prototype, "
|
|
598
|
-
__decorate([
|
|
599
|
-
state(),
|
|
600
|
-
__metadata("design:type", Boolean)
|
|
601
|
-
], ApplianceEditor.prototype, "loadFailed", void 0);
|
|
602
|
-
__decorate([
|
|
603
|
-
state(),
|
|
604
|
-
__metadata("design:type", Boolean)
|
|
605
|
-
], ApplianceEditor.prototype, "revocable", void 0);
|
|
553
|
+
__metadata("design:type", Object)
|
|
554
|
+
], ApplianceEditor.prototype, "changed", void 0);
|
|
606
555
|
ApplianceEditor = __decorate([
|
|
607
556
|
customElement('appliance-editor')
|
|
608
557
|
], ApplianceEditor);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appliance-editor.js","sourceRoot":"","sources":["../../client/components/appliance-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@material/web/icon/icon.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles'\nimport { parseJwt, sleep } from '@operato/utils'\n\nimport './secret-field.js'\nimport { readRevocable, showsRevokeButton, showsRevokedNote } from '../utils/credential-panel-state.js'\nimport { changedValues, filledValues, missingRequired } from '../utils/form-values.js'\n\n/**\n * Registering and editing an appliance, in a popup.\n *\n * ── Why this replaced two pages ─────────────────────────────────────────────\n * `appliance-register` and `appliance/{id}` were full routes. Nothing outside auth-ui linked\n * to either, and every other resource in this package — role, user, domain, domain owner,\n * auth provider, attribute set — is a grist list with a popup over it. The two pages were not\n * a different kind of resource, only older code.\n *\n * ── The defect that came with them ──────────────────────────────────────────\n * Both pages sent the whole form. On the register page that meant `serialNo: ''`, and\n * `createAppliance` has no validation at all — it saves `{ domain, creator, updater,\n * ...appliance }` — so `''` passed GraphQL's non-null check (an empty string is a String) and\n * an appliance with no serial number, brand or model was created **and the screen navigated\n * away as if it had worked.** On the edit page the same habit cleared fields nobody touched.\n *\n * `NewAppliance` is the source of the required list below. `AppliancePatch` is spread over the\n * loaded row, so the update sends a diff — see `utils/form-values.ts` for why those are two\n * different rules rather than one.\n */\n\n/** Fields `NewAppliance` declares non-null. Not a house rule — the schema's own list. */\nconst REQUIRED = ['serialNo', 'name', 'brand', 'model']\n\nconst APPLIANCE_FIELDS = `\n id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n credentialRevoked\n`\n\n@customElement('appliance-editor')\nexport class ApplianceEditor extends localize(i18next)(LitElement) {\n /*\n * ── Where the scrolling happens ─────────────────────────────────────────\n * The button row stays put and only the middle scrolls. That is the house shape for a\n * popup, and this had it wrong: the host itself scrolled, so the row you press to finish\n * walked off the bottom of the card along with everything else.\n *\n * So the host clips, and the content area between the title and the buttons is the one\n * thing with overflow. Two details carry it:\n *\n * min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below\n * its own content. Without this the overflow rule sits there doing\n * nothing and the card grows instead — it reads as a working rule.\n * no padding CommonPopupStyles already pads the host. Padding here again doubles the\n * inset and pushes the scrollbar in off the card edge.\n *\n * ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.\n *\n * ── Why the in-content buttons need a rule of their own ─────────────────\n * CommonPopupStyles makes controls pill-shaped because a popup is a rounded card and the\n * things inside it follow the card. But it says that through `.buttons`, so only the commit\n * row got it: the actions sitting inside a fieldset — generate a new token, generate a new\n * secret — kept the page radius and read as a different kind of button on the same card.\n * Measured on a live popup: 5px inside the content, 13986px in the row below it.\n */\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n /* in-content actions follow the card too, not just the commit row. see the note on styles. */\n fieldset md-text-button,\n fieldset md-filled-button {\n --md-text-button-container-shape: var(--popup-control-radius);\n --md-filled-button-container-shape: var(--popup-control-radius);\n }\n\n /* the host does not scroll; the content area below does. see the note on styles. */\n :host {\n overflow: hidden;\n }\n\n /* min-height:0 is load-bearing — see the note on styles. */\n form {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n\n display: flex;\n flex-direction: column;\n gap: var(--popup-gap);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 0;\n padding: var(--popup-padding);\n }\n\n legend {\n padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n /* one labelled box. .row puts two of them side by side; alone it spans the card. */\n [field] {\n display: flex;\n flex-direction: column;\n flex: 1;\n }\n\n label {\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n }\n\n input:focus {\n outline: none;\n }\n\n input[invalid] {\n border-color: var(--md-sys-color-error);\n }\n\n [field-error] {\n color: var(--md-sys-color-error);\n font-size: var(--popup-font-size-small);\n margin-top: var(--spacing-tiny, 2px);\n }\n\n /* taking a credential away is not the same kind of act as issuing one. */\n md-text-button.danger {\n --md-text-button-label-text-color: var(--md-sys-color-error);\n }\n\n .note.revoked {\n color: var(--md-sys-color-error);\n }\n\n @media screen and (max-width: 480px) {\n [field] {\n grid-column: span 2;\n }\n }\n `\n ]\n\n /** The appliance to edit. Absent means this popup is registering a new one. */\n @property({ type: String }) applianceId?: string\n\n @query('form') form!: HTMLFormElement\n\n /** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */\n @state() private appliance: any = null\n\n /** Required fields still empty. Filled in when the person presses save, not while typing. */\n @state() private missing: string[] = []\n\n /** The mutation was refused. The shared link already said why; this says the press landed. */\n @state() private failed: boolean = false\n\n /** The single-record read did not answer. Distinct from answering with nothing. */\n @state() private loadFailed: boolean = false\n\n /*\n * Does revoking work on this installation, for this reader?\n *\n * Revocation is behind a configuration switch, and the server answers one question that folds\n * the switch and the reader's privilege together — see `applianceCredentialRevocable`. False\n * until that answer comes back, so a slow or refused read shows no button rather than one that\n * refuses when pressed.\n */\n @state() private revocable: boolean = false\n\n private get editing(): boolean {\n return !!this.applianceId\n }\n\n render() {\n const appliance = this.appliance || {}\n const exp = appliance.accessToken ? parseJwt(appliance.accessToken)?.exp : undefined\n\n if (this.editing && this.loadFailed) {\n return html`<div class=\"failure\">${i18next.t('text.the appliance could not be loaded')}</div>`\n }\n\n return html`\n <form @submit=${(e: Event) => e.preventDefault()}>\n <fieldset>\n <legend>${i18next.t('text.appliance')}</legend>\n <div field>\n <label for=\"name\">${i18next.t('field.name')}</label>\n <input\n id=\"name\"\n type=\"text\"\n name=\"name\"\n .value=${appliance.name || ''}\n required\n autofocus\n ?invalid=${this.missing.includes('name')}\n />\n ${this.fieldError('name')}\n </div>\n\n <div field>\n <label for=\"description\">${i18next.t('field.description')}</label>\n <input id=\"description\" type=\"text\" name=\"description\" .value=${appliance.description || ''} />\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"serial-no\">${i18next.t('field.serial-no')}</label>\n <input\n id=\"serial-no\"\n type=\"text\"\n name=\"serialNo\"\n .value=${appliance.serialNo || ''}\n required\n ?invalid=${this.missing.includes('serialNo')}\n />\n ${this.fieldError('serialNo')}\n </div>\n\n <div field>\n <label for=\"brand\">${i18next.t('field.brand')}</label>\n <input\n id=\"brand\"\n type=\"text\"\n name=\"brand\"\n .value=${appliance.brand || ''}\n required\n ?invalid=${this.missing.includes('brand')}\n />\n ${this.fieldError('brand')}\n </div>\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"model\">${i18next.t('field.model')}</label>\n <input\n id=\"model\"\n type=\"text\"\n name=\"model\"\n .value=${appliance.model || ''}\n required\n ?invalid=${this.missing.includes('model')}\n />\n ${this.fieldError('model')}\n </div>\n\n <div field>\n <label for=\"netmask\">${i18next.t('field.netmask')}</label>\n <input id=\"netmask\" type=\"text\" name=\"netmask\" .value=${appliance.netmask || ''} />\n </div>\n </div>\n </fieldset>\n\n ${this.editing ? this.renderCredential(appliance, exp) : ''}\n ${this.failed ? html`<div class=\"failure\">${i18next.t('text.could not be saved')}</div>` : ''}\n </form>\n\n <ox-popup-actions>\n ${\n this.editing\n ? html`\n <md-text-button slot=\"start\" destructive @click=${this.deleteAppliance.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button slot=\"cancel\" @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n /*\n * The credential panel only exists once the appliance does — there is no token to show\n * before it is created, and the list query deliberately never asks for one.\n *\n * ── Why a revoked appliance says so ─────────────────────────────────────\n * Revoking clears the token, so without a word for it the panel looks exactly like one whose\n * credential was never issued. Those are opposite situations: one is waiting to be set up,\n * the other has been deliberately shut off, and only one of them wants somebody to act.\n *\n * ── Why the revoke button is not always here ────────────────────────────\n * It appears only where the installation turned revocation on and the reader may use it. See\n * the note on `revocable`.\n */\n private renderCredential(appliance: any, exp?: number) {\n return html`\n <fieldset>\n <legend>${i18next.t('text.appliance credential')}</legend>\n <div field>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${appliance.accessToken || ''}></secret-field>\n ${\n showsRevokedNote(appliance)\n ? html`<div class=\"note revoked\">${i18next.t('text.this credential has been revoked')}</div>`\n : ''\n }\n ${\n exp\n ? html`<div class=\"note\">\n ${i18next.t('text.token expiry time')} ${new Date(exp * 1000).toLocaleString()} :\n ${asyncReplace(this.expTimer(exp))}\n </div>`\n : ''\n }\n <div class=\"row\">\n <md-text-button @click=${this.generateSecret.bind(this)}>\n ${i18next.t('button.generate new access token')}\n </md-text-button>\n ${\n showsRevokeButton(this.revocable, appliance)\n ? html`\n <md-text-button class=\"danger\" @click=${this.revokeCredential.bind(this)}>\n ${i18next.t('button.revoke credential')}\n </md-text-button>\n `\n : ''\n }\n </div>\n </div>\n </fieldset>\n `\n }\n\n private fieldError(name: string) {\n return this.missing.includes(name) ? html`<div field-error>${i18next.t('text.this field is required')}</div>` : ''\n }\n\n async connectedCallback() {\n super.connectedCallback()\n\n if (this.editing) {\n await Promise.all([this.fetchAppliance(), this.fetchRevocable()])\n }\n }\n\n /*\n * Whether the revoke button exists at all.\n *\n * A refusal arrives as `response.errors` rather than a throw, and both a refusal and a `false`\n * mean the same thing to this screen: no button. So there is nothing to report here — the\n * absence of the button is the whole answer.\n */\n private async fetchRevocable() {\n const response = await client.query({\n query: gql`\n query {\n applianceCredentialRevocable\n }\n `\n })\n\n this.revocable = readRevocable(response as any)\n }\n\n async fetchAppliance() {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n appliance(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n /*\n * `errorPolicy: 'all'` means a refusal arrives here rather than throwing. Reading\n * `data.appliance` without checking would show an empty form that looks like a record\n * with every field blank — and saving it would then write those blanks.\n */\n if (response.errors || !response.data?.appliance) {\n this.loadFailed = true\n return\n }\n\n this.loadFailed = false\n this.appliance = response.data.appliance\n }\n\n private cancel(e: Event) {\n e.preventDefault()\n this.close()\n }\n\n async save(e: Event) {\n e.preventDefault()\n\n const formData = new FormData(this.form)\n\n /*\n * The same four fields are required on both paths. They come from `NewAppliance`, and an\n * update is held to them too: the patch is spread over the row, so `name: ''` would clear\n * a field the product will not let you create without.\n */\n const missing = missingRequired(formData, REQUIRED)\n\n if (missing.length) {\n this.missing = missing\n return\n }\n\n this.missing = []\n\n return this.editing ? this.updateAppliance(formData) : this.createAppliance(formData)\n }\n\n private async createAppliance(formData: FormData) {\n const response = await client.mutate({\n mutation: gql`\n mutation ($appliance: NewAppliance!) {\n createAppliance(appliance: $appliance) {\n id\n }\n }\n `,\n variables: { appliance: filledValues(formData) }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async updateAppliance(formData: FormData) {\n const patch = changedValues(formData, this.appliance)\n\n /*\n * Nothing moved. Sending an empty patch would still stamp `updater` and `updatedAt`,\n * which makes an untouched record look edited to whoever reads the list next.\n */\n if (!Object.keys(patch).length) {\n this.close()\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: AppliancePatch!) {\n updateAppliance(id: $id, patch: $patch) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId, patch }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n private async generateSecret(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.generate new access token'),\n text: i18next.t('text.the current access token stops working immediately'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n generateApplianceSecret(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n /*\n * Stays open on purpose. A new token is the one value on this screen the reader has to\n * copy before it is out of reach — closing here would hand it to them and take it away.\n */\n this.failed = false\n this.appliance = response.data.generateApplianceSecret\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n }\n\n /*\n * Take the credential away and leave the appliance where it is.\n *\n * ── Why this is not the delete button ───────────────────────────────────\n * Deleting takes the appliance and its history with it. This is for a device that is lost,\n * decommissioned or suspect and whose record still has to stand — the row keeps pointing at\n * the same identity, and issuing a new secret later picks that identity back up.\n *\n * ── Why the popup stays open ────────────────────────────────────────────\n * The panel has to show the result: the token is gone and the appliance now says it was\n * revoked. Closing would leave the reader guessing whether the press landed, and the list\n * behind it is refetched either way by the event below.\n */\n private async revokeCredential(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.revoke credential'),\n text: i18next.t('text.revoking stops every token this appliance holds'),\n confirmButton: { text: i18next.t('button.revoke credential') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n revokeApplianceCredential(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.failed = false\n this.appliance = response.data.revokeApplianceCredential\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n }\n\n private async deleteAppliance(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.delete'),\n text: i18next.t('text.are you sure you want to delete x', { x: this.appliance?.name || '' }),\n confirmButton: { text: i18next.t('button.delete') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteAppliance(id: $id)\n }\n `,\n variables: { id: this.applianceId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.saved()\n }\n\n /** The list has to refetch, and the popup is done. */\n private saved() {\n this.failed = false\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n this.close()\n }\n\n private close() {\n this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n }\n\n private async *expTimer(exp: number) {\n const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.isConnected) {\n let remain = Math.floor(Number(exp) - Date.now() / 1000)\n\n /*\n * Past the expiry the arithmetic below produces \"-5 days -3 hours\", which reads as a\n * duration rather than as the token being dead. Say it plainly and stop counting.\n */\n if (remain <= 0) {\n yield i18next.t('text.expired')\n return\n }\n\n const days = Math.floor(remain / DAY)\n remain -= days * DAY\n const hours = Math.floor(remain / HOUR)\n remain -= hours * HOUR\n const mins = Math.floor(remain / MIN)\n const secs = remain - mins * MIN\n\n yield i18next.t('text.remaining time', { days, hours, mins, secs })\n\n await sleep(1000)\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"appliance-editor.js","sourceRoot":"","sources":["../../client/components/appliance-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/popup/ox-popup-actions.js'\nimport '@material/web/icon/icon.js'\nimport '@material/web/button/text-button.js'\nimport '@material/web/button/filled-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, query, state } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\n\nimport { MutationController, QueryController } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { OxPrompt } from '@operato/popup/ox-prompt.js'\nimport { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles'\nimport { parseJwt, sleep } from '@operato/utils'\n\nimport './secret-field.js'\nimport { readRevocable, showsRevokeButton, showsRevokedNote } from '../utils/credential-panel-state.js'\nimport { changedValues, filledValues, missingRequired } from '../utils/form-values.js'\n\n/**\n * Registering and editing an appliance, in a popup.\n *\n * ── Why this replaced two pages ─────────────────────────────────────────────\n * `appliance-register` and `appliance/{id}` were full routes. Nothing outside auth-ui linked\n * to either, and every other resource in this package — role, user, domain, domain owner,\n * auth provider, attribute set — is a grist list with a popup over it. The two pages were not\n * a different kind of resource, only older code.\n *\n * ── The defect that came with them ──────────────────────────────────────────\n * Both pages sent the whole form. On the register page that meant `serialNo: ''`, and\n * `createAppliance` has no validation at all — it saves `{ domain, creator, updater,\n * ...appliance }` — so `''` passed GraphQL's non-null check (an empty string is a String) and\n * an appliance with no serial number, brand or model was created **and the screen navigated\n * away as if it had worked.** On the edit page the same habit cleared fields nobody touched.\n *\n * `NewAppliance` is the source of the required list below. `AppliancePatch` is spread over the\n * loaded row, so the update sends a diff — see `utils/form-values.ts` for why those are two\n * different rules rather than one.\n */\n\n/** Fields `NewAppliance` declares non-null. Not a house rule — the schema's own list. */\nconst REQUIRED = ['serialNo', 'name', 'brand', 'model']\n\nconst APPLIANCE_FIELDS = `\n id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n credentialRevoked\n`\n\n@customElement('appliance-editor')\nexport class ApplianceEditor extends localize(i18next)(LitElement) {\n /*\n * ── Where the scrolling happens ─────────────────────────────────────────\n * The button row stays put and only the middle scrolls. That is the house shape for a\n * popup, and this had it wrong: the host itself scrolled, so the row you press to finish\n * walked off the bottom of the card along with everything else.\n *\n * So the host clips, and the content area between the title and the buttons is the one\n * thing with overflow. Two details carry it:\n *\n * min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below\n * its own content. Without this the overflow rule sits there doing\n * nothing and the card grows instead — it reads as a working rule.\n * no padding CommonPopupStyles already pads the host. Padding here again doubles the\n * inset and pushes the scrollbar in off the card edge.\n *\n * ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.\n *\n * ── Why the in-content buttons need a rule of their own ─────────────────\n * CommonPopupStyles makes controls pill-shaped because a popup is a rounded card and the\n * things inside it follow the card. But it says that through `.buttons`, so only the commit\n * row got it: the actions sitting inside a fieldset — generate a new token, generate a new\n * secret — kept the page radius and read as a different kind of button on the same card.\n * Measured on a live popup: 5px inside the content, 13986px in the row below it.\n */\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n ScrollbarStyles,\n css`\n /* in-content actions follow the card too, not just the commit row. see the note on styles. */\n fieldset md-text-button,\n fieldset md-filled-button {\n --md-text-button-container-shape: var(--popup-control-radius);\n --md-filled-button-container-shape: var(--popup-control-radius);\n }\n\n /* the host does not scroll; the content area below does. see the note on styles. */\n :host {\n overflow: hidden;\n }\n\n /* min-height:0 is load-bearing — see the note on styles. */\n form {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n\n display: flex;\n flex-direction: column;\n gap: var(--popup-gap);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 0;\n padding: var(--popup-padding);\n }\n\n legend {\n padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n /* one labelled box. .row puts two of them side by side; alone it spans the card. */\n [field] {\n display: flex;\n flex-direction: column;\n flex: 1;\n }\n\n label {\n font: var(--label-font);\n color: var(--label-color, var(--md-sys-color-on-surface));\n text-transform: var(--label-text-transform);\n }\n\n input {\n background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));\n color: var(--input-field-color, var(--md-sys-color-on-surface));\n border: var(--border-dim-color);\n border-radius: var(--border-radius);\n margin: var(--input-margin);\n padding: var(--input-padding);\n font: var(--input-font);\n }\n\n input:focus {\n outline: none;\n }\n\n input[invalid] {\n border-color: var(--md-sys-color-error);\n }\n\n [field-error] {\n color: var(--md-sys-color-error);\n font-size: var(--popup-font-size-small);\n margin-top: var(--spacing-tiny, 2px);\n }\n\n /* taking a credential away is not the same kind of act as issuing one. */\n md-text-button.danger {\n --md-text-button-label-text-color: var(--md-sys-color-error);\n }\n\n .note.revoked {\n color: var(--md-sys-color-error);\n }\n\n @media screen and (max-width: 480px) {\n [field] {\n grid-column: span 2;\n }\n }\n `\n ]\n\n /** The appliance to edit. Absent means this popup is registering a new one. */\n @property({ type: String }) applianceId?: string\n\n @query('form') form!: HTMLFormElement\n\n /** Required fields still empty. Filled in when the person presses save, not while typing. */\n @state() private missing: string[] = []\n\n /** The record as a new secret or a revocation left it - the panel shows the result and stays open. */\n @state() private changed: any = null\n\n private read = new QueryController(\n this,\n gql`\n query ($id: String!) {\n appliance(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `,\n { variables: () => (this.applianceId ? { id: this.applianceId } : undefined) }\n )\n\n private revocation = new QueryController(\n this,\n gql`\n query {\n applianceCredentialRevocable\n }\n `,\n { variables: () => (this.applianceId ? {} : undefined) }\n )\n\n /* A refused save: the shared link says why, `failed` says the press landed. */\n private saves = {\n create: new MutationController(\n this,\n gql`\n mutation ($appliance: NewAppliance!) {\n createAppliance(appliance: $appliance) {\n id\n }\n }\n `\n ),\n update: new MutationController(\n this,\n gql`\n mutation ($id: String!, $patch: AppliancePatch!) {\n updateAppliance(id: $id, patch: $patch) { ${APPLIANCE_FIELDS} }\n }\n `\n ),\n secret: new MutationController(\n this,\n gql`\n mutation ($id: String!) {\n generateApplianceSecret(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `\n ),\n revoke: new MutationController(\n this,\n gql`\n mutation ($id: String!) {\n revokeApplianceCredential(id: $id) { ${APPLIANCE_FIELDS} }\n }\n `\n ),\n delete: new MutationController(\n this,\n gql`\n mutation ($id: String!) {\n deleteAppliance(id: $id)\n }\n `\n )\n }\n\n /** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */\n private get appliance(): any {\n return this.changed ?? this.read.data?.appliance ?? null\n }\n\n /** The mutation was refused. The shared link already said why; this says the press landed. */\n private get failed(): boolean {\n return Object.values(this.saves).some(save => save.error)\n }\n\n /*\n * The single-record read did not answer. Distinct from answering with nothing.\n *\n * A refusal arrives as `error` rather than throwing. Reading `data.appliance` without checking\n * would show an empty form that looks like a record with every field blank — and saving it would\n * then write those blanks.\n */\n private get loadFailed(): boolean {\n return !!this.read.error || (!!this.read.data && !this.read.data.appliance)\n }\n\n /*\n * Does revoking work on this installation, for this reader?\n *\n * Revocation is behind a configuration switch, and the server answers one question that folds\n * the switch and the reader's privilege together — see `applianceCredentialRevocable`. False\n * until that answer comes back, so a slow or refused read shows no button rather than one that\n * refuses when pressed - a refusal and a `false` mean the same thing to this screen.\n */\n private get revocable(): boolean {\n return readRevocable(this.revocation.error ? null : { data: this.revocation.data })\n }\n\n private get editing(): boolean {\n return !!this.applianceId\n }\n\n render() {\n const appliance = this.appliance || {}\n const exp = appliance.accessToken ? parseJwt(appliance.accessToken)?.exp : undefined\n\n if (this.editing && this.loadFailed) {\n return html`<div class=\"failure\">${i18next.t('text.the appliance could not be loaded')}</div>`\n }\n\n return html`\n <form @submit=${(e: Event) => e.preventDefault()}>\n <fieldset>\n <legend>${i18next.t('text.appliance')}</legend>\n <div field>\n <label for=\"name\">${i18next.t('field.name')}</label>\n <input\n id=\"name\"\n type=\"text\"\n name=\"name\"\n .value=${appliance.name || ''}\n required\n autofocus\n ?invalid=${this.missing.includes('name')}\n />\n ${this.fieldError('name')}\n </div>\n\n <div field>\n <label for=\"description\">${i18next.t('field.description')}</label>\n <input id=\"description\" type=\"text\" name=\"description\" .value=${appliance.description || ''} />\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"serial-no\">${i18next.t('field.serial-no')}</label>\n <input\n id=\"serial-no\"\n type=\"text\"\n name=\"serialNo\"\n .value=${appliance.serialNo || ''}\n required\n ?invalid=${this.missing.includes('serialNo')}\n />\n ${this.fieldError('serialNo')}\n </div>\n\n <div field>\n <label for=\"brand\">${i18next.t('field.brand')}</label>\n <input\n id=\"brand\"\n type=\"text\"\n name=\"brand\"\n .value=${appliance.brand || ''}\n required\n ?invalid=${this.missing.includes('brand')}\n />\n ${this.fieldError('brand')}\n </div>\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"model\">${i18next.t('field.model')}</label>\n <input\n id=\"model\"\n type=\"text\"\n name=\"model\"\n .value=${appliance.model || ''}\n required\n ?invalid=${this.missing.includes('model')}\n />\n ${this.fieldError('model')}\n </div>\n\n <div field>\n <label for=\"netmask\">${i18next.t('field.netmask')}</label>\n <input id=\"netmask\" type=\"text\" name=\"netmask\" .value=${appliance.netmask || ''} />\n </div>\n </div>\n </fieldset>\n\n ${this.editing ? this.renderCredential(appliance, exp) : ''}\n ${this.failed ? html`<div class=\"failure\">${i18next.t('text.could not be saved')}</div>` : ''}\n </form>\n\n <ox-popup-actions>\n ${\n this.editing\n ? html`\n <md-text-button slot=\"start\" destructive @click=${this.deleteAppliance.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button slot=\"cancel\" @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </ox-popup-actions>\n `\n }\n\n /*\n * The credential panel only exists once the appliance does — there is no token to show\n * before it is created, and the list query deliberately never asks for one.\n *\n * ── Why a revoked appliance says so ─────────────────────────────────────\n * Revoking clears the token, so without a word for it the panel looks exactly like one whose\n * credential was never issued. Those are opposite situations: one is waiting to be set up,\n * the other has been deliberately shut off, and only one of them wants somebody to act.\n *\n * ── Why the revoke button is not always here ────────────────────────────\n * It appears only where the installation turned revocation on and the reader may use it. See\n * the note on `revocable`.\n */\n private renderCredential(appliance: any, exp?: number) {\n return html`\n <fieldset>\n <legend>${i18next.t('text.appliance credential')}</legend>\n <div field>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${appliance.accessToken || ''}></secret-field>\n ${\n showsRevokedNote(appliance)\n ? html`<div class=\"note revoked\">${i18next.t('text.this credential has been revoked')}</div>`\n : ''\n }\n ${\n exp\n ? html`<div class=\"note\">\n ${i18next.t('text.token expiry time')} ${new Date(exp * 1000).toLocaleString()} :\n ${asyncReplace(this.expTimer(exp))}\n </div>`\n : ''\n }\n <div class=\"row\">\n <md-text-button @click=${this.generateSecret.bind(this)}>\n ${i18next.t('button.generate new access token')}\n </md-text-button>\n ${\n showsRevokeButton(this.revocable, appliance)\n ? html`\n <md-text-button class=\"danger\" @click=${this.revokeCredential.bind(this)}>\n ${i18next.t('button.revoke credential')}\n </md-text-button>\n `\n : ''\n }\n </div>\n </div>\n </fieldset>\n `\n }\n\n private fieldError(name: string) {\n return this.missing.includes(name) ? html`<div field-error>${i18next.t('text.this field is required')}</div>` : ''\n }\n\n private cancel(e: Event) {\n e.preventDefault()\n this.close()\n }\n\n async save(e: Event) {\n e.preventDefault()\n\n const formData = new FormData(this.form)\n\n /*\n * The same four fields are required on both paths. They come from `NewAppliance`, and an\n * update is held to them too: the patch is spread over the row, so `name: ''` would clear\n * a field the product will not let you create without.\n */\n const missing = missingRequired(formData, REQUIRED)\n\n if (missing.length) {\n this.missing = missing\n return\n }\n\n this.missing = []\n\n return this.editing ? this.updateAppliance(formData) : this.createAppliance(formData)\n }\n\n private async createAppliance(formData: FormData) {\n if (await this.saves.create.run({ appliance: filledValues(formData) })) {\n this.saved()\n }\n }\n\n private async updateAppliance(formData: FormData) {\n const patch = changedValues(formData, this.appliance)\n\n /*\n * Nothing moved. Sending an empty patch would still stamp `updater` and `updatedAt`,\n * which makes an untouched record look edited to whoever reads the list next.\n */\n if (!Object.keys(patch).length) {\n this.close()\n return\n }\n\n if (await this.saves.update.run({ id: this.applianceId, patch })) {\n this.saved()\n }\n }\n\n private async generateSecret(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.generate new access token'),\n text: i18next.t('text.the current access token stops working immediately'),\n confirmButton: { text: i18next.t('button.confirm') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const generated = await this.saves.secret.run({ id: this.applianceId })\n\n if (!generated) {\n return\n }\n\n /*\n * Stays open on purpose. A new token is the one value on this screen the reader has to\n * copy before it is out of reach — closing here would hand it to them and take it away.\n */\n this.changed = generated.generateApplianceSecret\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n }\n\n /*\n * Take the credential away and leave the appliance where it is.\n *\n * ── Why this is not the delete button ───────────────────────────────────\n * Deleting takes the appliance and its history with it. This is for a device that is lost,\n * decommissioned or suspect and whose record still has to stand — the row keeps pointing at\n * the same identity, and issuing a new secret later picks that identity back up.\n *\n * ── Why the popup stays open ────────────────────────────────────────────\n * The panel has to show the result: the token is gone and the appliance now says it was\n * revoked. Closing would leave the reader guessing whether the press landed, and the list\n * behind it is refetched either way by the event below.\n */\n private async revokeCredential(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.revoke credential'),\n text: i18next.t('text.revoking stops every token this appliance holds'),\n confirmButton: { text: i18next.t('button.revoke credential') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n const revoked = await this.saves.revoke.run({ id: this.applianceId })\n\n if (!revoked) {\n return\n }\n\n this.changed = revoked.revokeApplianceCredential\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n }\n\n private async deleteAppliance(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.delete'),\n text: i18next.t('text.are you sure you want to delete x', { x: this.appliance?.name || '' }),\n confirmButton: { text: i18next.t('button.delete') },\n cancelButton: { text: i18next.t('button.cancel') }\n })\n\n if (!confirmed) {\n return\n }\n\n if (await this.saves.delete.run({ id: this.applianceId })) {\n this.saved()\n }\n }\n\n /** The list has to refetch, and the popup is done. */\n private saved() {\n this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }))\n this.close()\n }\n\n private close() {\n this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }))\n }\n\n private async *expTimer(exp: number) {\n const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.isConnected) {\n let remain = Math.floor(Number(exp) - Date.now() / 1000)\n\n /*\n * Past the expiry the arithmetic below produces \"-5 days -3 hours\", which reads as a\n * duration rather than as the token being dead. Say it plainly and stop counting.\n */\n if (remain <= 0) {\n yield i18next.t('text.expired')\n return\n }\n\n const days = Math.floor(remain / DAY)\n remain -= days * DAY\n const hours = Math.floor(remain / HOUR)\n remain -= hours * HOUR\n const mins = Math.floor(remain / MIN)\n const secs = remain - mins * MIN\n\n yield i18next.t('text.remaining time', { days, hours, mins, secs })\n\n await sleep(1000)\n }\n }\n}\n"]}
|
|
@@ -10,16 +10,18 @@ export declare class ApplicationEditor extends ApplicationEditor_base {
|
|
|
10
10
|
/** The application to edit. Absent means this popup is registering a new one. */
|
|
11
11
|
applicationId?: string;
|
|
12
12
|
form: HTMLFormElement;
|
|
13
|
-
private application;
|
|
14
13
|
private missing;
|
|
15
|
-
|
|
16
|
-
private
|
|
14
|
+
/** The application as a new secret left it - shown once, so it has to be copied before the popup closes. */
|
|
15
|
+
private regenerated;
|
|
16
|
+
private read;
|
|
17
|
+
private saves;
|
|
18
|
+
private get application();
|
|
19
|
+
private get failed();
|
|
20
|
+
private get loadFailed();
|
|
17
21
|
private get editing();
|
|
18
22
|
render(): import("lit-html").TemplateResult<1>;
|
|
19
23
|
private renderEndpoints;
|
|
20
24
|
private renderCredential;
|
|
21
|
-
connectedCallback(): Promise<void>;
|
|
22
|
-
fetchApplication(): Promise<void>;
|
|
23
25
|
private cancel;
|
|
24
26
|
save(e: Event): Promise<void>;
|
|
25
27
|
private createApplication;
|