@things-factory/auth-ui 10.1.17 → 10.1.18
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/components/app-binding-detail.d.ts +46 -0
- package/dist-client/components/app-binding-detail.js +384 -0
- package/dist-client/components/app-binding-detail.js.map +1 -0
- package/dist-client/components/appliance-editor.d.ts +37 -0
- package/dist-client/components/appliance-editor.js +489 -0
- package/dist-client/components/appliance-editor.js.map +1 -0
- package/dist-client/components/application-editor.d.ts +31 -0
- package/dist-client/components/application-editor.js +491 -0
- package/dist-client/components/application-editor.js.map +1 -0
- package/dist-client/components/profile-component.js +28 -11
- package/dist-client/components/profile-component.js.map +1 -1
- package/dist-client/constants/application-type-parity.test.js +62 -0
- package/dist-client/constants/application-type-parity.test.js.map +1 -0
- package/dist-client/constants/application.d.ts +17 -7
- package/dist-client/constants/application.js +18 -8
- package/dist-client/constants/application.js.map +1 -1
- package/dist-client/pages/app-binding/app-bindings.d.ts +2 -0
- package/dist-client/pages/app-binding/app-bindings.js +19 -1
- package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
- package/dist-client/pages/appliance/home.d.ts +2 -0
- package/dist-client/pages/appliance/home.js +21 -3
- package/dist-client/pages/appliance/home.js.map +1 -1
- package/dist-client/pages/application/applications.d.ts +2 -0
- package/dist-client/pages/application/applications.js +21 -3
- package/dist-client/pages/application/applications.js.map +1 -1
- package/dist-client/pages/secrets-are-not-printed.test.js +21 -3
- package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -1
- package/dist-client/route.js +0 -15
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/form-values.d.ts +29 -0
- package/dist-client/utils/form-values.js +45 -0
- package/dist-client/utils/form-values.js.map +1 -1
- package/dist-client/utils/form-values.test.js +59 -1
- package/dist-client/utils/form-values.test.js.map +1 -1
- package/dist-client/utils/webauthn-verification.d.ts +56 -0
- package/dist-client/utils/webauthn-verification.js +79 -0
- package/dist-client/utils/webauthn-verification.js.map +1 -0
- package/dist-client/utils/webauthn-verification.test.js +79 -0
- package/dist-client/utils/webauthn-verification.test.js.map +1 -0
- package/package.json +3 -3
- package/things-factory.config.js +0 -5
- package/translations/en.json +25 -0
- package/translations/ja.json +26 -0
- package/translations/ko.json +25 -0
- package/translations/ms.json +26 -0
- package/translations/zh.json +26 -0
- package/dist-client/pages/app-binding/app-binding.d.ts +0 -2
- package/dist-client/pages/app-binding/app-binding.js +0 -371
- package/dist-client/pages/app-binding/app-binding.js.map +0 -1
- package/dist-client/pages/appliance/appliance.d.ts +0 -2
- package/dist-client/pages/appliance/appliance.js +0 -390
- package/dist-client/pages/appliance/appliance.js.map +0 -1
- package/dist-client/pages/appliance/register.js +0 -166
- package/dist-client/pages/appliance/register.js.map +0 -1
- package/dist-client/pages/application/application.d.ts +0 -2
- package/dist-client/pages/application/application.js +0 -384
- package/dist-client/pages/application/application.js.map +0 -1
- package/dist-client/pages/application/register.js +0 -246
- package/dist-client/pages/application/register.js.map +0 -1
- /package/dist-client/{pages/appliance/register.d.ts → constants/application-type-parity.test.d.ts} +0 -0
- /package/dist-client/{pages/application/register.d.ts → utils/webauthn-verification.test.d.ts} +0 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import '@material/web/button/text-button.js';
|
|
4
|
+
import '@material/web/button/filled-button.js';
|
|
5
|
+
import gql from 'graphql-tag';
|
|
6
|
+
import { css, html, LitElement } from 'lit';
|
|
7
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
8
|
+
import { asyncReplace } from 'lit/directives/async-replace.js';
|
|
9
|
+
import { client } from '@operato/graphql';
|
|
10
|
+
import { i18next, localize } from '@operato/i18n';
|
|
11
|
+
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
12
|
+
import { CommonHeaderStyles, CommonPopupStyles } from '@operato/styles';
|
|
13
|
+
import { parseJwt, sleep } from '@operato/utils';
|
|
14
|
+
import './secret-field.js';
|
|
15
|
+
import { changedValues, filledValues, missingRequired } from '../utils/form-values.js';
|
|
16
|
+
/**
|
|
17
|
+
* Registering and editing an appliance, in a popup.
|
|
18
|
+
*
|
|
19
|
+
* ── Why this replaced two pages ─────────────────────────────────────────────
|
|
20
|
+
* `appliance-register` and `appliance/{id}` were full routes. Nothing outside auth-ui linked
|
|
21
|
+
* to either, and every other resource in this package — role, user, domain, domain owner,
|
|
22
|
+
* auth provider, attribute set — is a grist list with a popup over it. The two pages were not
|
|
23
|
+
* a different kind of resource, only older code.
|
|
24
|
+
*
|
|
25
|
+
* ── The defect that came with them ──────────────────────────────────────────
|
|
26
|
+
* Both pages sent the whole form. On the register page that meant `serialNo: ''`, and
|
|
27
|
+
* `createAppliance` has no validation at all — it saves `{ domain, creator, updater,
|
|
28
|
+
* ...appliance }` — so `''` passed GraphQL's non-null check (an empty string is a String) and
|
|
29
|
+
* an appliance with no serial number, brand or model was created **and the screen navigated
|
|
30
|
+
* away as if it had worked.** On the edit page the same habit cleared fields nobody touched.
|
|
31
|
+
*
|
|
32
|
+
* `NewAppliance` is the source of the required list below. `AppliancePatch` is spread over the
|
|
33
|
+
* loaded row, so the update sends a diff — see `utils/form-values.ts` for why those are two
|
|
34
|
+
* different rules rather than one.
|
|
35
|
+
*/
|
|
36
|
+
/** Fields `NewAppliance` declares non-null. Not a house rule — the schema's own list. */
|
|
37
|
+
const REQUIRED = ['serialNo', 'name', 'brand', 'model'];
|
|
38
|
+
const APPLIANCE_FIELDS = `
|
|
39
|
+
id
|
|
40
|
+
name
|
|
41
|
+
description
|
|
42
|
+
serialNo
|
|
43
|
+
brand
|
|
44
|
+
model
|
|
45
|
+
netmask
|
|
46
|
+
accessToken
|
|
47
|
+
`;
|
|
48
|
+
let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement) {
|
|
49
|
+
constructor() {
|
|
50
|
+
super(...arguments);
|
|
51
|
+
/** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */
|
|
52
|
+
this.appliance = null;
|
|
53
|
+
/** Required fields still empty. Filled in when the person presses save, not while typing. */
|
|
54
|
+
this.missing = [];
|
|
55
|
+
/** The mutation was refused. The shared link already said why; this says the press landed. */
|
|
56
|
+
this.failed = false;
|
|
57
|
+
/** The single-record read did not answer. Distinct from answering with nothing. */
|
|
58
|
+
this.loadFailed = false;
|
|
59
|
+
}
|
|
60
|
+
static { this.styles = [
|
|
61
|
+
CommonHeaderStyles,
|
|
62
|
+
CommonPopupStyles,
|
|
63
|
+
css `
|
|
64
|
+
:host {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
|
|
68
|
+
background-color: var(--md-sys-color-surface);
|
|
69
|
+
overflow: auto;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
form {
|
|
73
|
+
flex: 1;
|
|
74
|
+
padding: var(--popup-padding);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fieldset {
|
|
78
|
+
border-radius: var(--border-radius);
|
|
79
|
+
border: var(--border-dim-color);
|
|
80
|
+
margin: 0 0 var(--popup-gap) 0;
|
|
81
|
+
padding: var(--popup-padding);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
legend {
|
|
85
|
+
padding: var(--legend-padding);
|
|
86
|
+
font-weight: bold;
|
|
87
|
+
color: var(--legend-color);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[field-2column] {
|
|
91
|
+
display: grid;
|
|
92
|
+
grid-template-columns: 1fr 1fr;
|
|
93
|
+
grid-gap: var(--popup-gap);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[field] {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
[grid-span] {
|
|
102
|
+
grid-column: span 2;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
label {
|
|
106
|
+
font: var(--label-font);
|
|
107
|
+
color: var(--label-color, var(--md-sys-color-on-surface));
|
|
108
|
+
text-transform: var(--label-text-transform);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
input {
|
|
112
|
+
background-color: var(--input-field-background, var(--md-sys-color-surface-container-highest));
|
|
113
|
+
color: var(--input-field-color, var(--md-sys-color-on-surface));
|
|
114
|
+
border: var(--border-dim-color);
|
|
115
|
+
border-radius: var(--border-radius);
|
|
116
|
+
margin: var(--input-margin);
|
|
117
|
+
padding: var(--input-padding);
|
|
118
|
+
font: var(--input-font);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
input:focus {
|
|
122
|
+
outline: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
input[invalid] {
|
|
126
|
+
border-color: var(--md-sys-color-error);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[field-error] {
|
|
130
|
+
color: var(--md-sys-color-error);
|
|
131
|
+
font-size: var(--popup-font-size-small);
|
|
132
|
+
margin-top: var(--spacing-tiny, 2px);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[input-hint] {
|
|
136
|
+
font-size: var(--popup-font-size-small);
|
|
137
|
+
color: var(--input-hint-color);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* The one reverting action goes left, away from the ones that commit. */
|
|
141
|
+
.buttons .left {
|
|
142
|
+
margin-right: auto;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@media screen and (max-width: 480px) {
|
|
146
|
+
[field] {
|
|
147
|
+
grid-column: span 2;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
`
|
|
151
|
+
]; }
|
|
152
|
+
get editing() {
|
|
153
|
+
return !!this.applianceId;
|
|
154
|
+
}
|
|
155
|
+
render() {
|
|
156
|
+
const appliance = this.appliance || {};
|
|
157
|
+
const exp = appliance.accessToken ? parseJwt(appliance.accessToken)?.exp : undefined;
|
|
158
|
+
if (this.editing && this.loadFailed) {
|
|
159
|
+
return html `<div class="failure">${i18next.t('text.the appliance could not be loaded')}</div>`;
|
|
160
|
+
}
|
|
161
|
+
return html `
|
|
162
|
+
<form @submit=${(e) => e.preventDefault()}>
|
|
163
|
+
<fieldset>
|
|
164
|
+
<legend>${i18next.t('text.appliance')}</legend>
|
|
165
|
+
<div field-2column>
|
|
166
|
+
<div field grid-span>
|
|
167
|
+
<label for="name">${i18next.t('field.name')}</label>
|
|
168
|
+
<input
|
|
169
|
+
id="name"
|
|
170
|
+
type="text"
|
|
171
|
+
name="name"
|
|
172
|
+
.value=${appliance.name || ''}
|
|
173
|
+
required
|
|
174
|
+
autofocus
|
|
175
|
+
?invalid=${this.missing.includes('name')}
|
|
176
|
+
/>
|
|
177
|
+
${this.fieldError('name')}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div field grid-span>
|
|
181
|
+
<label for="description">${i18next.t('field.description')}</label>
|
|
182
|
+
<input id="description" type="text" name="description" .value=${appliance.description || ''} />
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div field>
|
|
186
|
+
<label for="serial-no">${i18next.t('field.serial-no')}</label>
|
|
187
|
+
<input
|
|
188
|
+
id="serial-no"
|
|
189
|
+
type="text"
|
|
190
|
+
name="serialNo"
|
|
191
|
+
.value=${appliance.serialNo || ''}
|
|
192
|
+
required
|
|
193
|
+
?invalid=${this.missing.includes('serialNo')}
|
|
194
|
+
/>
|
|
195
|
+
${this.fieldError('serialNo')}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div field>
|
|
199
|
+
<label for="brand">${i18next.t('field.brand')}</label>
|
|
200
|
+
<input
|
|
201
|
+
id="brand"
|
|
202
|
+
type="text"
|
|
203
|
+
name="brand"
|
|
204
|
+
.value=${appliance.brand || ''}
|
|
205
|
+
required
|
|
206
|
+
?invalid=${this.missing.includes('brand')}
|
|
207
|
+
/>
|
|
208
|
+
${this.fieldError('brand')}
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div field>
|
|
212
|
+
<label for="model">${i18next.t('field.model')}</label>
|
|
213
|
+
<input
|
|
214
|
+
id="model"
|
|
215
|
+
type="text"
|
|
216
|
+
name="model"
|
|
217
|
+
.value=${appliance.model || ''}
|
|
218
|
+
required
|
|
219
|
+
?invalid=${this.missing.includes('model')}
|
|
220
|
+
/>
|
|
221
|
+
${this.fieldError('model')}
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div field>
|
|
225
|
+
<label for="netmask">${i18next.t('field.netmask')}</label>
|
|
226
|
+
<input id="netmask" type="text" name="netmask" .value=${appliance.netmask || ''} />
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</fieldset>
|
|
230
|
+
|
|
231
|
+
${this.editing ? this.renderCredential(appliance, exp) : ''}
|
|
232
|
+
${this.failed ? html `<div class="failure">${i18next.t('text.could not be saved')}</div>` : ''}
|
|
233
|
+
</form>
|
|
234
|
+
|
|
235
|
+
<div class="buttons">
|
|
236
|
+
${this.editing
|
|
237
|
+
? html `
|
|
238
|
+
<md-text-button class="left" @click=${this.deleteAppliance.bind(this)}>
|
|
239
|
+
${i18next.t('button.delete')}
|
|
240
|
+
</md-text-button>
|
|
241
|
+
`
|
|
242
|
+
: ''}
|
|
243
|
+
<md-text-button @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>
|
|
244
|
+
<md-filled-button @click=${this.save.bind(this)}>
|
|
245
|
+
${this.editing ? i18next.t('button.update') : i18next.t('button.register')}
|
|
246
|
+
</md-filled-button>
|
|
247
|
+
</div>
|
|
248
|
+
`;
|
|
249
|
+
}
|
|
250
|
+
/*
|
|
251
|
+
* The credential panel only exists once the appliance does — there is no token to show
|
|
252
|
+
* before it is created, and the list query deliberately never asks for one.
|
|
253
|
+
*/
|
|
254
|
+
renderCredential(appliance, exp) {
|
|
255
|
+
return html `
|
|
256
|
+
<fieldset>
|
|
257
|
+
<legend>${i18next.t('text.appliance credential')}</legend>
|
|
258
|
+
<div field grid-span>
|
|
259
|
+
<label for="access-token">${i18next.t('label.access token')}</label>
|
|
260
|
+
<secret-field id="access-token" .value=${appliance.accessToken || ''}></secret-field>
|
|
261
|
+
${exp
|
|
262
|
+
? html `<div input-hint>
|
|
263
|
+
${i18next.t('text.token expiry time')} ${new Date(exp * 1000).toLocaleString()} :
|
|
264
|
+
${asyncReplace(this.expTimer(exp))}
|
|
265
|
+
</div>`
|
|
266
|
+
: ''}
|
|
267
|
+
<div class="row">
|
|
268
|
+
<md-text-button @click=${this.generateSecret.bind(this)}>
|
|
269
|
+
${i18next.t('button.generate new access token')}
|
|
270
|
+
</md-text-button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</fieldset>
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
fieldError(name) {
|
|
277
|
+
return this.missing.includes(name) ? html `<div field-error>${i18next.t('text.this field is required')}</div>` : '';
|
|
278
|
+
}
|
|
279
|
+
async connectedCallback() {
|
|
280
|
+
super.connectedCallback();
|
|
281
|
+
if (this.editing) {
|
|
282
|
+
await this.fetchAppliance();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async fetchAppliance() {
|
|
286
|
+
const response = await client.query({
|
|
287
|
+
query: gql `
|
|
288
|
+
query ($id: String!) {
|
|
289
|
+
appliance(id: $id) { ${APPLIANCE_FIELDS} }
|
|
290
|
+
}
|
|
291
|
+
`,
|
|
292
|
+
variables: { id: this.applianceId }
|
|
293
|
+
});
|
|
294
|
+
/*
|
|
295
|
+
* `errorPolicy: 'all'` means a refusal arrives here rather than throwing. Reading
|
|
296
|
+
* `data.appliance` without checking would show an empty form that looks like a record
|
|
297
|
+
* with every field blank — and saving it would then write those blanks.
|
|
298
|
+
*/
|
|
299
|
+
if (response.errors || !response.data?.appliance) {
|
|
300
|
+
this.loadFailed = true;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.loadFailed = false;
|
|
304
|
+
this.appliance = response.data.appliance;
|
|
305
|
+
}
|
|
306
|
+
cancel(e) {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
this.close();
|
|
309
|
+
}
|
|
310
|
+
async save(e) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
const formData = new FormData(this.form);
|
|
313
|
+
/*
|
|
314
|
+
* The same four fields are required on both paths. They come from `NewAppliance`, and an
|
|
315
|
+
* update is held to them too: the patch is spread over the row, so `name: ''` would clear
|
|
316
|
+
* a field the product will not let you create without.
|
|
317
|
+
*/
|
|
318
|
+
const missing = missingRequired(formData, REQUIRED);
|
|
319
|
+
if (missing.length) {
|
|
320
|
+
this.missing = missing;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.missing = [];
|
|
324
|
+
return this.editing ? this.updateAppliance(formData) : this.createAppliance(formData);
|
|
325
|
+
}
|
|
326
|
+
async createAppliance(formData) {
|
|
327
|
+
const response = await client.mutate({
|
|
328
|
+
mutation: gql `
|
|
329
|
+
mutation ($appliance: NewAppliance!) {
|
|
330
|
+
createAppliance(appliance: $appliance) {
|
|
331
|
+
id
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
`,
|
|
335
|
+
variables: { appliance: filledValues(formData) }
|
|
336
|
+
});
|
|
337
|
+
if (response.errors) {
|
|
338
|
+
this.failed = true;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
this.saved();
|
|
342
|
+
}
|
|
343
|
+
async updateAppliance(formData) {
|
|
344
|
+
const patch = changedValues(formData, this.appliance);
|
|
345
|
+
/*
|
|
346
|
+
* Nothing moved. Sending an empty patch would still stamp `updater` and `updatedAt`,
|
|
347
|
+
* which makes an untouched record look edited to whoever reads the list next.
|
|
348
|
+
*/
|
|
349
|
+
if (!Object.keys(patch).length) {
|
|
350
|
+
this.close();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const response = await client.mutate({
|
|
354
|
+
mutation: gql `
|
|
355
|
+
mutation ($id: String!, $patch: AppliancePatch!) {
|
|
356
|
+
updateAppliance(id: $id, patch: $patch) { ${APPLIANCE_FIELDS} }
|
|
357
|
+
}
|
|
358
|
+
`,
|
|
359
|
+
variables: { id: this.applianceId, patch }
|
|
360
|
+
});
|
|
361
|
+
if (response.errors) {
|
|
362
|
+
this.failed = true;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.saved();
|
|
366
|
+
}
|
|
367
|
+
async generateSecret(e) {
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
const confirmed = await OxPrompt.open({
|
|
370
|
+
type: 'warning',
|
|
371
|
+
title: i18next.t('button.generate new access token'),
|
|
372
|
+
text: i18next.t('text.the current access token stops working immediately'),
|
|
373
|
+
confirmButton: { text: i18next.t('button.confirm') },
|
|
374
|
+
cancelButton: { text: i18next.t('button.cancel') }
|
|
375
|
+
});
|
|
376
|
+
if (!confirmed) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const response = await client.mutate({
|
|
380
|
+
mutation: gql `
|
|
381
|
+
mutation ($id: String!) {
|
|
382
|
+
generateApplianceSecret(id: $id) { ${APPLIANCE_FIELDS} }
|
|
383
|
+
}
|
|
384
|
+
`,
|
|
385
|
+
variables: { id: this.applianceId }
|
|
386
|
+
});
|
|
387
|
+
if (response.errors) {
|
|
388
|
+
this.failed = true;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
/*
|
|
392
|
+
* Stays open on purpose. A new token is the one value on this screen the reader has to
|
|
393
|
+
* copy before it is out of reach — closing here would hand it to them and take it away.
|
|
394
|
+
*/
|
|
395
|
+
this.failed = false;
|
|
396
|
+
this.appliance = response.data.generateApplianceSecret;
|
|
397
|
+
this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }));
|
|
398
|
+
}
|
|
399
|
+
async deleteAppliance(e) {
|
|
400
|
+
e.preventDefault();
|
|
401
|
+
const confirmed = await OxPrompt.open({
|
|
402
|
+
type: 'warning',
|
|
403
|
+
title: i18next.t('button.delete'),
|
|
404
|
+
text: i18next.t('text.are you sure you want to delete x', { x: this.appliance?.name || '' }),
|
|
405
|
+
confirmButton: { text: i18next.t('button.delete') },
|
|
406
|
+
cancelButton: { text: i18next.t('button.cancel') }
|
|
407
|
+
});
|
|
408
|
+
if (!confirmed) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const response = await client.mutate({
|
|
412
|
+
mutation: gql `
|
|
413
|
+
mutation ($id: String!) {
|
|
414
|
+
deleteAppliance(id: $id)
|
|
415
|
+
}
|
|
416
|
+
`,
|
|
417
|
+
variables: { id: this.applianceId }
|
|
418
|
+
});
|
|
419
|
+
if (response.errors) {
|
|
420
|
+
this.failed = true;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.saved();
|
|
424
|
+
}
|
|
425
|
+
/** The list has to refetch, and the popup is done. */
|
|
426
|
+
saved() {
|
|
427
|
+
this.failed = false;
|
|
428
|
+
this.dispatchEvent(new CustomEvent('appliance-changed', { bubbles: true, composed: true }));
|
|
429
|
+
this.close();
|
|
430
|
+
}
|
|
431
|
+
close() {
|
|
432
|
+
this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
|
|
433
|
+
}
|
|
434
|
+
async *expTimer(exp) {
|
|
435
|
+
const DAY = 24 * 60 * 60;
|
|
436
|
+
const HOUR = 60 * 60;
|
|
437
|
+
const MIN = 60;
|
|
438
|
+
while (this.isConnected) {
|
|
439
|
+
let remain = Math.floor(Number(exp) - Date.now() / 1000);
|
|
440
|
+
/*
|
|
441
|
+
* Past the expiry the arithmetic below produces "-5 days -3 hours", which reads as a
|
|
442
|
+
* duration rather than as the token being dead. Say it plainly and stop counting.
|
|
443
|
+
*/
|
|
444
|
+
if (remain <= 0) {
|
|
445
|
+
yield i18next.t('text.expired');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const days = Math.floor(remain / DAY);
|
|
449
|
+
remain -= days * DAY;
|
|
450
|
+
const hours = Math.floor(remain / HOUR);
|
|
451
|
+
remain -= hours * HOUR;
|
|
452
|
+
const mins = Math.floor(remain / MIN);
|
|
453
|
+
const secs = remain - mins * MIN;
|
|
454
|
+
yield i18next.t('text.remaining time', { days, hours, mins, secs });
|
|
455
|
+
await sleep(1000);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
__decorate([
|
|
460
|
+
property({ type: String }),
|
|
461
|
+
__metadata("design:type", String)
|
|
462
|
+
], ApplianceEditor.prototype, "applianceId", void 0);
|
|
463
|
+
__decorate([
|
|
464
|
+
query('form'),
|
|
465
|
+
__metadata("design:type", HTMLFormElement
|
|
466
|
+
/** The loaded record. The patch is a diff against this, so it is never rebuilt from the form. */
|
|
467
|
+
)
|
|
468
|
+
], ApplianceEditor.prototype, "form", void 0);
|
|
469
|
+
__decorate([
|
|
470
|
+
state(),
|
|
471
|
+
__metadata("design:type", Object)
|
|
472
|
+
], ApplianceEditor.prototype, "appliance", void 0);
|
|
473
|
+
__decorate([
|
|
474
|
+
state(),
|
|
475
|
+
__metadata("design:type", Array)
|
|
476
|
+
], ApplianceEditor.prototype, "missing", void 0);
|
|
477
|
+
__decorate([
|
|
478
|
+
state(),
|
|
479
|
+
__metadata("design:type", Boolean)
|
|
480
|
+
], ApplianceEditor.prototype, "failed", void 0);
|
|
481
|
+
__decorate([
|
|
482
|
+
state(),
|
|
483
|
+
__metadata("design:type", Boolean)
|
|
484
|
+
], ApplianceEditor.prototype, "loadFailed", void 0);
|
|
485
|
+
ApplianceEditor = __decorate([
|
|
486
|
+
customElement('appliance-editor')
|
|
487
|
+
], ApplianceEditor);
|
|
488
|
+
export { ApplianceEditor };
|
|
489
|
+
//# sourceMappingURL=appliance-editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appliance-editor.js","sourceRoot":"","sources":["../../client/components/appliance-editor.ts"],"names":[],"mappings":"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","sourcesContent":["import '@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 } from '@operato/styles'\nimport { parseJwt, sleep } from '@operato/utils'\n\nimport './secret-field.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`\n\n@customElement('appliance-editor')\nexport class ApplianceEditor extends localize(i18next)(LitElement) {\n static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n\n background-color: var(--md-sys-color-surface);\n overflow: auto;\n }\n\n form {\n flex: 1;\n padding: var(--popup-padding);\n }\n\n fieldset {\n border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: 0 0 var(--popup-gap) 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 [field-2column] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: var(--popup-gap);\n }\n\n [field] {\n display: flex;\n flex-direction: column;\n }\n\n [grid-span] {\n grid-column: span 2;\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 [input-hint] {\n font-size: var(--popup-font-size-small);\n color: var(--input-hint-color);\n }\n\n /* The one reverting action goes left, away from the ones that commit. */\n .buttons .left {\n margin-right: auto;\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 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-2column>\n <div field grid-span>\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 grid-span>\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 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\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 <div class=\"buttons\">\n ${\n this.editing\n ? html`\n <md-text-button class=\"left\" @click=${this.deleteAppliance.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n `\n : ''\n }\n <md-text-button @click=${this.cancel.bind(this)}>${i18next.t('button.cancel')}</md-text-button>\n <md-filled-button @click=${this.save.bind(this)}>\n ${this.editing ? i18next.t('button.update') : i18next.t('button.register')}\n </md-filled-button>\n </div>\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 private renderCredential(appliance: any, exp?: number) {\n return html`\n <fieldset>\n <legend>${i18next.t('text.appliance credential')}</legend>\n <div field grid-span>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${appliance.accessToken || ''}></secret-field>\n ${\n exp\n ? html`<div input-hint>\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 </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 this.fetchAppliance()\n }\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 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"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import '@material/web/button/text-button.js';
|
|
3
|
+
import '@material/web/button/filled-button.js';
|
|
4
|
+
import { LitElement } from 'lit';
|
|
5
|
+
import './secret-field.js';
|
|
6
|
+
declare const ApplicationEditor_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
7
|
+
export declare class ApplicationEditor extends ApplicationEditor_base {
|
|
8
|
+
static styles: import("lit").CSSResult[];
|
|
9
|
+
/** The application to edit. Absent means this popup is registering a new one. */
|
|
10
|
+
applicationId?: string;
|
|
11
|
+
form: HTMLFormElement;
|
|
12
|
+
private application;
|
|
13
|
+
private missing;
|
|
14
|
+
private failed;
|
|
15
|
+
private loadFailed;
|
|
16
|
+
private get editing();
|
|
17
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
18
|
+
private renderEndpoints;
|
|
19
|
+
private renderCredential;
|
|
20
|
+
connectedCallback(): Promise<void>;
|
|
21
|
+
fetchApplication(): Promise<void>;
|
|
22
|
+
private cancel;
|
|
23
|
+
save(e: Event): Promise<void>;
|
|
24
|
+
private createApplication;
|
|
25
|
+
private updateApplication;
|
|
26
|
+
private generateSecret;
|
|
27
|
+
private deleteApplication;
|
|
28
|
+
private saved;
|
|
29
|
+
private close;
|
|
30
|
+
}
|
|
31
|
+
export {};
|