@things-factory/auth-ui 10.1.256 → 10.1.257

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist-client/bootstrap.js +2 -2
  2. package/dist-client/bootstrap.js.map +1 -1
  3. package/dist-client/components/access-decision-popup.d.ts +9 -8
  4. package/dist-client/components/access-decision-popup.js +46 -58
  5. package/dist-client/components/access-decision-popup.js.map +1 -1
  6. package/dist-client/components/access-request-popup.d.ts +13 -8
  7. package/dist-client/components/access-request-popup.js +78 -102
  8. package/dist-client/components/access-request-popup.js.map +1 -1
  9. package/dist-client/components/app-binding-detail.d.ts +7 -5
  10. package/dist-client/components/app-binding-detail.js +62 -82
  11. package/dist-client/components/app-binding-detail.js.map +1 -1
  12. package/dist-client/components/appliance-editor.d.ts +10 -9
  13. package/dist-client/components/appliance-editor.js +87 -138
  14. package/dist-client/components/appliance-editor.js.map +1 -1
  15. package/dist-client/components/application-editor.d.ts +7 -5
  16. package/dist-client/components/application-editor.js +58 -92
  17. package/dist-client/components/application-editor.js.map +1 -1
  18. package/dist-client/components/my-login-history.js +18 -40
  19. package/dist-client/components/my-login-history.js.map +1 -1
  20. package/dist-client/components/my-passkeys.d.ts +4 -3
  21. package/dist-client/components/my-passkeys.js +29 -51
  22. package/dist-client/components/my-passkeys.js.map +1 -1
  23. package/dist-client/components/my-roles.d.ts +3 -4
  24. package/dist-client/components/my-roles.js +36 -55
  25. package/dist-client/components/my-roles.js.map +1 -1
  26. package/dist-client/components/partner-role-editor.js +27 -50
  27. package/dist-client/components/partner-role-editor.js.map +1 -1
  28. package/dist-client/components/profile-component.d.ts +2 -2
  29. package/dist-client/components/profile-component.js +18 -26
  30. package/dist-client/components/profile-component.js.map +1 -1
  31. package/dist-client/components/role-template-picker.d.ts +3 -3
  32. package/dist-client/components/role-template-picker.js +28 -42
  33. package/dist-client/components/role-template-picker.js.map +1 -1
  34. package/dist-client/components/user-detail.d.ts +23 -22
  35. package/dist-client/components/user-detail.js +125 -179
  36. package/dist-client/components/user-detail.js.map +1 -1
  37. package/dist-client/components/user-invite.d.ts +8 -6
  38. package/dist-client/components/user-invite.js +106 -141
  39. package/dist-client/components/user-invite.js.map +1 -1
  40. package/dist-client/components/user-popups-standard.test.js +1 -1
  41. package/dist-client/components/user-popups-standard.test.js.map +1 -1
  42. package/dist-client/components/user-role-editor.js +72 -122
  43. package/dist-client/components/user-role-editor.js.map +1 -1
  44. package/dist-client/entity360/person-360.d.ts +8 -11
  45. package/dist-client/entity360/person-360.js +49 -68
  46. package/dist-client/entity360/person-360.js.map +1 -1
  47. package/dist-client/entries/oauth2/oauth2-decision-page.js +13 -25
  48. package/dist-client/entries/oauth2/oauth2-decision-page.js.map +1 -1
  49. package/dist-client/identity-confirmation/identity-confirmation.d.ts +13 -14
  50. package/dist-client/identity-confirmation/identity-confirmation.js +39 -45
  51. package/dist-client/identity-confirmation/identity-confirmation.js.map +1 -1
  52. package/dist-client/pages/audit/audit-page.d.ts +4 -4
  53. package/dist-client/pages/audit/audit-page.js +28 -39
  54. package/dist-client/pages/audit/audit-page.js.map +1 -1
  55. package/dist-client/pages/domain-link/domain-link-management.js +1 -1
  56. package/dist-client/pages/domain-link/domain-link-management.js.map +1 -1
  57. package/dist-client/pages/partner/partner-management.js +30 -68
  58. package/dist-client/pages/partner/partner-management.js.map +1 -1
  59. package/dist-client/pages/role/role-extra-removal.test.d.mts +1 -0
  60. package/dist-client/pages/role/role-extra-removal.test.mjs +73 -0
  61. package/dist-client/pages/role/role-extra-removal.test.mjs.map +1 -0
  62. package/dist-client/pages/role/role-list-page.js +20 -22
  63. package/dist-client/pages/role/role-list-page.js.map +1 -1
  64. package/dist-client/pages/role/role-privilege-page.d.ts +36 -8
  65. package/dist-client/pages/role/role-privilege-page.js +216 -158
  66. package/dist-client/pages/role/role-privilege-page.js.map +1 -1
  67. package/dist-client/pages/role/role-task-review.test.js +20 -16
  68. package/dist-client/pages/role/role-task-review.test.js.map +1 -1
  69. package/dist-client/pages/role/role-template-gap.test.js +8 -3
  70. package/dist-client/pages/role/role-template-gap.test.js.map +1 -1
  71. package/dist-client/tsconfig.tsbuildinfo +1 -1
  72. package/helps/auth/roles.en.md +13 -49
  73. package/helps/auth/roles.ko.md +13 -32
  74. package/package.json +9 -8
  75. package/tests/page-state.test.ts +2 -1
  76. package/tests/password-rule-shown.test.ts +2 -2
  77. package/tests/person-360-sections.test.ts +9 -5
  78. package/translations/en.json +5 -1
  79. package/translations/es.json +5 -1
  80. package/translations/ja.json +5 -1
  81. package/translations/ko.json +5 -1
  82. package/translations/ms.json +5 -1
  83. 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 { client } from '@operato/graphql';
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 mutation was refused. The shared link already said why; this says the press landed. */
59
- this.failed = false;
60
- /** The single-record read did not answer. Distinct from answering with nothing. */
61
- this.loadFailed = false;
62
- /*
63
- * Does revoking work on this installation, for this reader?
64
- *
65
- * Revocation is behind a configuration switch, and the server answers one question that folds
66
- * the switch and the reader's privilege together — see `applianceCredentialRevocable`. False
67
- * until that answer comes back, so a slow or refused read shows no button rather than one that
68
- * refuses when pressed.
69
- */
70
- this.revocable = false;
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
- const response = await client.mutate({
404
- mutation: gql `
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
- const response = await client.mutate({
430
- mutation: gql `
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 response = await client.mutate({
456
- mutation: gql `
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.failed = false;
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 response = await client.mutate({
501
- mutation: gql `
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.failed = false;
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
- const response = await client.mutate({
529
- mutation: gql `
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
- /** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */
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", Boolean)
597
- ], ApplianceEditor.prototype, "failed", void 0);
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
- private failed;
16
- private loadFailed;
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;