@things-factory/auth-ui 10.1.20 → 10.1.24
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.js +61 -57
- package/dist-client/components/app-binding-detail.js.map +1 -1
- package/dist-client/components/appliance-editor.js +67 -43
- package/dist-client/components/appliance-editor.js.map +1 -1
- package/dist-client/components/application-editor.js +112 -91
- package/dist-client/components/application-editor.js.map +1 -1
- package/dist-client/components/create-domain-popup.js +2 -1
- package/dist-client/components/create-domain-popup.js.map +1 -1
- package/dist-client/components/my-login-history.js +16 -12
- package/dist-client/components/my-login-history.js.map +1 -1
- package/dist-client/components/ownership-transfer-popup.js +2 -0
- package/dist-client/components/ownership-transfer-popup.js.map +1 -1
- package/dist-client/components/user-detail.js +8 -7
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -9,7 +9,7 @@ import { asyncReplace } from 'lit/directives/async-replace.js';
|
|
|
9
9
|
import { client } from '@operato/graphql';
|
|
10
10
|
import { i18next, localize } from '@operato/i18n';
|
|
11
11
|
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
12
|
-
import { CommonHeaderStyles, CommonPopupStyles } from '@operato/styles';
|
|
12
|
+
import { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles';
|
|
13
13
|
import { parseJwt, sleep } from '@operato/utils';
|
|
14
14
|
import './secret-field.js';
|
|
15
15
|
/**
|
|
@@ -41,27 +41,48 @@ let AppBindingDetail = class AppBindingDetail extends localize(i18next)(LitEleme
|
|
|
41
41
|
this.failed = false;
|
|
42
42
|
this.loadFailed = false;
|
|
43
43
|
}
|
|
44
|
+
/*
|
|
45
|
+
* ── Where the scrolling happens ─────────────────────────────────────────
|
|
46
|
+
* The button row stays put and only the middle scrolls. That is the house shape for a
|
|
47
|
+
* popup, and this had it wrong: the host itself scrolled, so the row you press to finish
|
|
48
|
+
* walked off the bottom of the card along with everything else.
|
|
49
|
+
*
|
|
50
|
+
* So the host clips, and the content area between the title and the buttons is the one
|
|
51
|
+
* thing with overflow. Two details carry it:
|
|
52
|
+
*
|
|
53
|
+
* min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below
|
|
54
|
+
* its own content. Without this the overflow rule sits there doing
|
|
55
|
+
* nothing and the card grows instead — it reads as a working rule.
|
|
56
|
+
* no padding CommonPopupStyles already pads the host. Padding here again doubles the
|
|
57
|
+
* inset and pushes the scrollbar in off the card edge.
|
|
58
|
+
*
|
|
59
|
+
* ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.
|
|
60
|
+
*/
|
|
44
61
|
static { this.styles = [
|
|
45
62
|
CommonHeaderStyles,
|
|
46
63
|
CommonPopupStyles,
|
|
64
|
+
ScrollbarStyles,
|
|
47
65
|
css `
|
|
66
|
+
/* the host does not scroll; the content area below does. see the note on styles. */
|
|
48
67
|
:host {
|
|
49
|
-
|
|
50
|
-
flex-direction: column;
|
|
51
|
-
|
|
52
|
-
background-color: var(--md-sys-color-surface);
|
|
53
|
-
overflow: auto;
|
|
68
|
+
overflow: hidden;
|
|
54
69
|
}
|
|
55
70
|
|
|
71
|
+
/* min-height:0 is load-bearing — see the note on styles. */
|
|
56
72
|
[body] {
|
|
57
73
|
flex: 1;
|
|
58
|
-
|
|
74
|
+
min-height: 0;
|
|
75
|
+
overflow-y: auto;
|
|
76
|
+
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: var(--popup-gap);
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
fieldset {
|
|
62
83
|
border-radius: var(--border-radius);
|
|
63
84
|
border: var(--border-dim-color);
|
|
64
|
-
margin: 0
|
|
85
|
+
margin: 0;
|
|
65
86
|
padding: var(--popup-padding);
|
|
66
87
|
}
|
|
67
88
|
|
|
@@ -71,19 +92,11 @@ let AppBindingDetail = class AppBindingDetail extends localize(i18next)(LitEleme
|
|
|
71
92
|
color: var(--legend-color);
|
|
72
93
|
}
|
|
73
94
|
|
|
74
|
-
|
|
75
|
-
display: grid;
|
|
76
|
-
grid-template-columns: 1fr 1fr;
|
|
77
|
-
grid-gap: var(--popup-gap);
|
|
78
|
-
}
|
|
79
|
-
|
|
95
|
+
/* one labelled box. .row puts two of them side by side; alone it spans the card. */
|
|
80
96
|
[field] {
|
|
81
97
|
display: flex;
|
|
82
98
|
flex-direction: column;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
[grid-span] {
|
|
86
|
-
grid-column: span 2;
|
|
99
|
+
flex: 1;
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
label {
|
|
@@ -108,11 +121,6 @@ let AppBindingDetail = class AppBindingDetail extends localize(i18next)(LitEleme
|
|
|
108
121
|
outline: none;
|
|
109
122
|
}
|
|
110
123
|
|
|
111
|
-
[input-hint] {
|
|
112
|
-
font-size: var(--popup-font-size-small);
|
|
113
|
-
color: var(--input-hint-color);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
124
|
.buttons .left {
|
|
117
125
|
margin-right: auto;
|
|
118
126
|
}
|
|
@@ -134,37 +142,35 @@ let AppBindingDetail = class AppBindingDetail extends localize(i18next)(LitEleme
|
|
|
134
142
|
<div body>
|
|
135
143
|
<fieldset>
|
|
136
144
|
<legend>${i18next.t('text.application')}</legend>
|
|
137
|
-
<div field
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
</div>
|
|
145
|
+
<div field>
|
|
146
|
+
<label for="app-name">${i18next.t('field.name')}</label>
|
|
147
|
+
<input id="app-name" type="text" .value=${app.name || ''} readonly />
|
|
148
|
+
</div>
|
|
142
149
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
<div field>
|
|
151
|
+
<label for="app-description">${i18next.t('field.description')}</label>
|
|
152
|
+
<input id="app-description" type="text" .value=${app.description || ''} readonly />
|
|
153
|
+
</div>
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
</div>
|
|
155
|
+
<div field>
|
|
156
|
+
<label for="app-email">${i18next.t('field.contact email')}</label>
|
|
157
|
+
<input id="app-email" type="text" .value=${app.email || ''} readonly />
|
|
152
158
|
</div>
|
|
153
159
|
</fieldset>
|
|
154
160
|
|
|
155
161
|
<fieldset>
|
|
156
162
|
<legend>${i18next.t('text.binding')}</legend>
|
|
157
|
-
<div field
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
</div>
|
|
163
|
+
<div field>
|
|
164
|
+
<label for="binding-name">${i18next.t('field.name')}</label>
|
|
165
|
+
<input id="binding-name" type="text" .value=${binding.name || ''} readonly />
|
|
166
|
+
</div>
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
<div field>
|
|
169
|
+
<label for="binding-email">${i18next.t('field.email')}</label>
|
|
170
|
+
<input id="binding-email" type="text" .value=${binding.email || ''} readonly />
|
|
171
|
+
</div>
|
|
167
172
|
|
|
173
|
+
<div class="row">
|
|
168
174
|
<div field>
|
|
169
175
|
<label for="binding-status">${i18next.t('field.status')}</label>
|
|
170
176
|
<input id="binding-status" type="text" .value=${binding.status || ''} readonly />
|
|
@@ -179,18 +185,16 @@ let AppBindingDetail = class AppBindingDetail extends localize(i18next)(LitEleme
|
|
|
179
185
|
|
|
180
186
|
<fieldset>
|
|
181
187
|
<legend>${i18next.t('text.binding credential')}</legend>
|
|
182
|
-
<div field
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
</div>
|
|
188
|
+
<div field>
|
|
189
|
+
<label for="access-token">${i18next.t('label.access token')}</label>
|
|
190
|
+
<secret-field id="access-token" .value=${this.accessToken || ''}></secret-field>
|
|
191
|
+
${this.expiry(this.accessToken)}
|
|
192
|
+
</div>
|
|
188
193
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
</div>
|
|
194
|
+
<div field>
|
|
195
|
+
<label for="refresh-token">${i18next.t('label.refresh token')}</label>
|
|
196
|
+
<secret-field id="refresh-token" .value=${binding.refreshToken || ''}></secret-field>
|
|
197
|
+
${this.expiry(binding.refreshToken)}
|
|
194
198
|
</div>
|
|
195
199
|
</fieldset>
|
|
196
200
|
|
|
@@ -218,7 +222,7 @@ let AppBindingDetail = class AppBindingDetail extends localize(i18next)(LitEleme
|
|
|
218
222
|
if (!exp) {
|
|
219
223
|
return '';
|
|
220
224
|
}
|
|
221
|
-
return html `<div
|
|
225
|
+
return html `<div class="note">
|
|
222
226
|
${i18next.t('text.expires at x', { x: new Date(exp * 1000).toLocaleString() })} ·
|
|
223
227
|
${asyncReplace(this.expTimer(exp))}
|
|
224
228
|
</div>`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-binding-detail.js","sourceRoot":"","sources":["../../client/components/app-binding-detail.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, 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'\n\n/**\n * One application binding, in a popup.\n *\n * ── Read-only, and that is the point ────────────────────────────────────────\n * A binding is not authored here. It is what an application ends up with after it walks the\n * OAuth flow, so there is no create and no edit — only two things a person can do to one:\n * renew its access token, or revoke it by deleting the binding. Everything else on this\n * screen is the record saying what it is.\n *\n * ── Two defects the page this replaced carried ──────────────────────────────\n * 1. **The expiry line said 1970.** A JWT `exp` is in seconds; the page passed it straight to\n * `new Date(...)`, which reads milliseconds. So `new Date(1760000000)` — three weeks after\n * the epoch — was printed as the moment a token expires, beside a countdown computed in\n * seconds that was right. Two numbers for the same instant, disagreeing by half a century.\n * 2. **A field was labelled `name` and showed the email.** `<input .value=${binding.email}>`\n * under `<label>name</label>`.\n *\n * ── Why the access token starts empty ───────────────────────────────────────\n * `AppBinding` stores a refresh token; an access token is minted, handed over, and not kept.\n * So the access-token field has nothing in it until this screen mints a fresh one, and that\n * is the honest state rather than a gap to fill.\n */\n@customElement('app-binding-detail')\nexport class AppBindingDetail 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 [body] {\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 opacity: 0.7;\n }\n\n input:focus {\n outline: none;\n }\n\n [input-hint] {\n font-size: var(--popup-font-size-small);\n color: var(--input-hint-color);\n }\n\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 @property({ type: String }) appBindingId?: string\n\n @state() private binding: any = null\n\n /** Minted by `renew`, never stored on the record. Empty until someone presses it. */\n @state() private accessToken?: string\n\n @state() private failed: boolean = false\n\n @state() private loadFailed: boolean = false\n\n render() {\n if (this.loadFailed) {\n return html`<div class=\"failure\">${i18next.t('text.the binding could not be loaded')}</div>`\n }\n\n const binding = this.binding || {}\n const app = binding.application || {}\n\n return html`\n <div body>\n <fieldset>\n <legend>${i18next.t('text.application')}</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"app-name\">${i18next.t('field.name')}</label>\n <input id=\"app-name\" type=\"text\" .value=${app.name || ''} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"app-description\">${i18next.t('field.description')}</label>\n <input id=\"app-description\" type=\"text\" .value=${app.description || ''} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"app-email\">${i18next.t('field.contact email')}</label>\n <input id=\"app-email\" type=\"text\" .value=${app.email || ''} readonly />\n </div>\n </div>\n </fieldset>\n\n <fieldset>\n <legend>${i18next.t('text.binding')}</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"binding-name\">${i18next.t('field.name')}</label>\n <input id=\"binding-name\" type=\"text\" .value=${binding.name || ''} readonly />\n </div>\n\n <div field grid-span>\n <label for=\"binding-email\">${i18next.t('field.email')}</label>\n <input id=\"binding-email\" type=\"text\" .value=${binding.email || ''} readonly />\n </div>\n\n <div field>\n <label for=\"binding-status\">${i18next.t('field.status')}</label>\n <input id=\"binding-status\" type=\"text\" .value=${binding.status || ''} readonly />\n </div>\n\n <div field>\n <label for=\"binding-scope\">${i18next.t('field.scope')}</label>\n <input id=\"binding-scope\" type=\"text\" .value=${binding.scope || ''} readonly />\n </div>\n </div>\n </fieldset>\n\n <fieldset>\n <legend>${i18next.t('text.binding credential')}</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${this.accessToken || ''}></secret-field>\n ${this.expiry(this.accessToken)}\n </div>\n\n <div field grid-span>\n <label for=\"refresh-token\">${i18next.t('label.refresh token')}</label>\n <secret-field id=\"refresh-token\" .value=${binding.refreshToken || ''}></secret-field>\n ${this.expiry(binding.refreshToken)}\n </div>\n </div>\n </fieldset>\n\n ${this.failed ? html`<div class=\"failure\">${i18next.t('text.could not be saved')}</div>` : ''}\n </div>\n\n <div class=\"buttons\">\n <md-text-button class=\"left\" @click=${this.deleteBinding.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n <md-text-button @click=${this.close.bind(this)}>${i18next.t('button.close')}</md-text-button>\n <md-filled-button @click=${this.renewAccessToken.bind(this)}>\n ${i18next.t('button.renew access token')}\n </md-filled-button>\n </div>\n `\n }\n\n /*\n * ⚠ A JWT `exp` is **seconds** since the epoch. The page this replaced handed it to\n * `new Date()` unchanged, which reads milliseconds, and printed a date in 1970 next to a\n * countdown that was computed correctly in seconds.\n */\n private expiry(token?: string) {\n const exp = token ? parseJwt(token)?.exp : undefined\n\n if (!exp) {\n return ''\n }\n\n return html`<div input-hint>\n ${i18next.t('text.expires at x', { x: new Date(exp * 1000).toLocaleString() })} ·\n ${asyncReplace(this.expTimer(exp))}\n </div>`\n }\n\n async connectedCallback() {\n super.connectedCallback()\n\n await this.fetchBinding()\n }\n\n async fetchBinding() {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n appBinding(id: $id) {\n id\n name\n description\n email\n scope\n status\n application {\n id\n name\n description\n email\n icon\n }\n refreshToken\n }\n }\n `,\n variables: { id: this.appBindingId }\n })\n\n /*\n * `AppBinding.refreshToken` carries its own privilege gate, so a reader without it gets\n * the record and an error pointing at that one field. That is not a failed read — the\n * rest of the binding is there and worth showing. Only a refusal of the record itself,\n * which arrives with no data, blanks the popup.\n */\n if (!response.data?.appBinding) {\n this.loadFailed = true\n return\n }\n\n this.loadFailed = false\n this.binding = response.data.appBinding\n }\n\n private async renewAccessToken(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.renew 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 renewApplicationAccessToken(id: $id) {\n accessToken\n refreshToken\n }\n }\n `,\n variables: { id: this.appBindingId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n /*\n * Stays open. The access token is minted here and stored nowhere — if this popup closed,\n * the value would be gone for good.\n */\n const { accessToken, refreshToken } = response.data.renewApplicationAccessToken\n\n this.failed = false\n this.accessToken = accessToken\n this.binding = { ...this.binding, refreshToken }\n this.dispatchEvent(new CustomEvent('app-binding-changed', { bubbles: true, composed: true }))\n }\n\n private async deleteBinding(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.binding?.application?.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 deleteAppBinding(id: $id)\n }\n `,\n variables: { id: this.appBindingId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.failed = false\n this.dispatchEvent(new CustomEvent('app-binding-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":"app-binding-detail.js","sourceRoot":"","sources":["../../client/components/app-binding-detail.ts"],"names":[],"mappings":";AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,qCAAqC,CAAA;AAC5C,OAAO,uCAAuC,CAAA;AAE9C,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAE9D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACxF,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAEhD,OAAO,mBAAmB,CAAA;AAE1B;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEI,IAAM,gBAAgB,GAAtB,MAAM,gBAAiB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA5D;;QA+FY,YAAO,GAAQ,IAAI,CAAA;QAKnB,WAAM,GAAY,KAAK,CAAA;QAEvB,eAAU,GAAY,KAAK,CAAA;IAoQ9C,CAAC;IAzWC;;;;;;;;;;;;;;;;OAgBG;aACI,WAAM,GAAG;QACd,kBAAkB;QAClB,iBAAiB;QACjB,eAAe;QACf,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAoEF;KACF,AAzEY,CAyEZ;IAaD,MAAM;QACJ,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO,IAAI,CAAA,wBAAwB,OAAO,CAAC,CAAC,CAAC,sCAAsC,CAAC,QAAQ,CAAA;QAC9F,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAA;QAClC,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAA;QAErC,OAAO,IAAI,CAAA;;;oBAGK,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC;;oCAEb,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;sDACL,GAAG,CAAC,IAAI,IAAI,EAAE;;;;2CAIzB,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC;6DACZ,GAAG,CAAC,WAAW,IAAI,EAAE;;;;qCAI7C,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC;uDACd,GAAG,CAAC,KAAK,IAAI,EAAE;;;;;oBAKlD,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;;wCAEL,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;0DACL,OAAO,CAAC,IAAI,IAAI,EAAE;;;;yCAInC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;2DACN,OAAO,CAAC,KAAK,IAAI,EAAE;;;;;4CAKlC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;8DACP,OAAO,CAAC,MAAM,IAAI,EAAE;;;;2CAIvC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;6DACN,OAAO,CAAC,KAAK,IAAI,EAAE;;;;;;oBAM5D,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC;;wCAEhB,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC;qDAClB,IAAI,CAAC,WAAW,IAAI,EAAE;cAC7D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;;;;yCAIF,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC;sDACnB,OAAO,CAAC,YAAY,IAAI,EAAE;cAClE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC;;;;UAIrC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,wBAAwB,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;;;;8CAIvD,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;YAC/D,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;;iCAEL,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC;mCAChD,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC;YACvD,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;;;KAG7C,CAAA;IACH,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,KAAc;QAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;QAEpD,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,EAAE,CAAA;QACX,CAAC;QAED,OAAO,IAAI,CAAA;QACP,OAAO,CAAC,CAAC,CAAC,mBAAmB,EAAE,EAAE,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC;QAC5E,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;WAC7B,CAAA;IACT,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,KAAK,CAAC,iBAAiB,EAAE,CAAA;QAEzB,MAAM,IAAI,CAAC,YAAY,EAAE,CAAA;IAC3B,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;;;;OAmBT;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE;SACrC,CAAC,CAAA;QAEF;;;;;WAKG;QACH,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;YACtB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,UAAU,GAAG,KAAK,CAAA;QACvB,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAA;IACzC,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,CAAQ;QACrC,CAAC,CAAC,cAAc,EAAE,CAAA;QAElB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC;YACpC,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,2BAA2B,CAAC;YAC7C,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,yDAAyD,CAAC;YAC1E,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE;YACpD,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE;SACnD,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;OAOZ;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE;SACrC,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QAED;;;WAGG;QACH,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,2BAA2B,CAAA;QAE/E,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,CAAA;QAChD,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,qBAAqB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC/F,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,CAAQ;QAClC,CAAC,CAAC,cAAc,EAAE,CAAA;QAElB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC;YACpC,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;YACjC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,wCAAwC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;YACvG,aAAa,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE;YACnD,YAAY,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,EAAE;SACnD,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;OAIZ;YACD,SAAS,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE;SACrC,CAAC,CAAA;QAEF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,OAAM;QACR,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,qBAAqB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC7F,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAEO,KAAK;QACX,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IACjF,CAAC;IAEO,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAW;QACjC,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;QACxB,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,CAAA;QACpB,MAAM,GAAG,GAAG,EAAE,CAAA;QAEd,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;YACxB,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;YAExD;;;eAGG;YACH,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAChB,MAAM,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAA;gBAC/B,OAAM;YACR,CAAC;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAA;YACrC,MAAM,IAAI,IAAI,GAAG,GAAG,CAAA;YACpB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;YACvC,MAAM,IAAI,KAAK,GAAG,IAAI,CAAA;YACtB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAA;YACrC,MAAM,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,GAAG,CAAA;YAEhC,MAAM,OAAO,CAAC,CAAC,CAAC,qBAAqB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YAEnE,MAAM,KAAK,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;;AA5Q2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;;sDAAsB;AAEhC;IAAhB,KAAK,EAAE;;iDAA4B;AAGnB;IAAhB,KAAK,EAAE;;qDAA6B;AAEpB;IAAhB,KAAK,EAAE;;gDAAgC;AAEvB;IAAhB,KAAK,EAAE;;oDAAoC;AAtGjC,gBAAgB;IAD5B,aAAa,CAAC,oBAAoB,CAAC;GACvB,gBAAgB,CA0W5B","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, 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'\n\n/**\n * One application binding, in a popup.\n *\n * ── Read-only, and that is the point ────────────────────────────────────────\n * A binding is not authored here. It is what an application ends up with after it walks the\n * OAuth flow, so there is no create and no edit — only two things a person can do to one:\n * renew its access token, or revoke it by deleting the binding. Everything else on this\n * screen is the record saying what it is.\n *\n * ── Two defects the page this replaced carried ──────────────────────────────\n * 1. **The expiry line said 1970.** A JWT `exp` is in seconds; the page passed it straight to\n * `new Date(...)`, which reads milliseconds. So `new Date(1760000000)` — three weeks after\n * the epoch — was printed as the moment a token expires, beside a countdown computed in\n * seconds that was right. Two numbers for the same instant, disagreeing by half a century.\n * 2. **A field was labelled `name` and showed the email.** `<input .value=${binding.email}>`\n * under `<label>name</label>`.\n *\n * ── Why the access token starts empty ───────────────────────────────────────\n * `AppBinding` stores a refresh token; an access token is minted, handed over, and not kept.\n * So the access-token field has nothing in it until this screen mints a fresh one, and that\n * is the honest state rather than a gap to fill.\n */\n@customElement('app-binding-detail')\nexport class AppBindingDetail 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 static styles = [\n CommonHeaderStyles,\n CommonPopupStyles,\n ScrollbarStyles,\n css`\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 [body] {\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 opacity: 0.7;\n }\n\n input:focus {\n outline: none;\n }\n\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 @property({ type: String }) appBindingId?: string\n\n @state() private binding: any = null\n\n /** Minted by `renew`, never stored on the record. Empty until someone presses it. */\n @state() private accessToken?: string\n\n @state() private failed: boolean = false\n\n @state() private loadFailed: boolean = false\n\n render() {\n if (this.loadFailed) {\n return html`<div class=\"failure\">${i18next.t('text.the binding could not be loaded')}</div>`\n }\n\n const binding = this.binding || {}\n const app = binding.application || {}\n\n return html`\n <div body>\n <fieldset>\n <legend>${i18next.t('text.application')}</legend>\n <div field>\n <label for=\"app-name\">${i18next.t('field.name')}</label>\n <input id=\"app-name\" type=\"text\" .value=${app.name || ''} readonly />\n </div>\n\n <div field>\n <label for=\"app-description\">${i18next.t('field.description')}</label>\n <input id=\"app-description\" type=\"text\" .value=${app.description || ''} readonly />\n </div>\n\n <div field>\n <label for=\"app-email\">${i18next.t('field.contact email')}</label>\n <input id=\"app-email\" type=\"text\" .value=${app.email || ''} readonly />\n </div>\n </fieldset>\n\n <fieldset>\n <legend>${i18next.t('text.binding')}</legend>\n <div field>\n <label for=\"binding-name\">${i18next.t('field.name')}</label>\n <input id=\"binding-name\" type=\"text\" .value=${binding.name || ''} readonly />\n </div>\n\n <div field>\n <label for=\"binding-email\">${i18next.t('field.email')}</label>\n <input id=\"binding-email\" type=\"text\" .value=${binding.email || ''} readonly />\n </div>\n\n <div class=\"row\">\n <div field>\n <label for=\"binding-status\">${i18next.t('field.status')}</label>\n <input id=\"binding-status\" type=\"text\" .value=${binding.status || ''} readonly />\n </div>\n\n <div field>\n <label for=\"binding-scope\">${i18next.t('field.scope')}</label>\n <input id=\"binding-scope\" type=\"text\" .value=${binding.scope || ''} readonly />\n </div>\n </div>\n </fieldset>\n\n <fieldset>\n <legend>${i18next.t('text.binding credential')}</legend>\n <div field>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <secret-field id=\"access-token\" .value=${this.accessToken || ''}></secret-field>\n ${this.expiry(this.accessToken)}\n </div>\n\n <div field>\n <label for=\"refresh-token\">${i18next.t('label.refresh token')}</label>\n <secret-field id=\"refresh-token\" .value=${binding.refreshToken || ''}></secret-field>\n ${this.expiry(binding.refreshToken)}\n </div>\n </fieldset>\n\n ${this.failed ? html`<div class=\"failure\">${i18next.t('text.could not be saved')}</div>` : ''}\n </div>\n\n <div class=\"buttons\">\n <md-text-button class=\"left\" @click=${this.deleteBinding.bind(this)}>\n ${i18next.t('button.delete')}\n </md-text-button>\n <md-text-button @click=${this.close.bind(this)}>${i18next.t('button.close')}</md-text-button>\n <md-filled-button @click=${this.renewAccessToken.bind(this)}>\n ${i18next.t('button.renew access token')}\n </md-filled-button>\n </div>\n `\n }\n\n /*\n * ⚠ A JWT `exp` is **seconds** since the epoch. The page this replaced handed it to\n * `new Date()` unchanged, which reads milliseconds, and printed a date in 1970 next to a\n * countdown that was computed correctly in seconds.\n */\n private expiry(token?: string) {\n const exp = token ? parseJwt(token)?.exp : undefined\n\n if (!exp) {\n return ''\n }\n\n return html`<div class=\"note\">\n ${i18next.t('text.expires at x', { x: new Date(exp * 1000).toLocaleString() })} ·\n ${asyncReplace(this.expTimer(exp))}\n </div>`\n }\n\n async connectedCallback() {\n super.connectedCallback()\n\n await this.fetchBinding()\n }\n\n async fetchBinding() {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n appBinding(id: $id) {\n id\n name\n description\n email\n scope\n status\n application {\n id\n name\n description\n email\n icon\n }\n refreshToken\n }\n }\n `,\n variables: { id: this.appBindingId }\n })\n\n /*\n * `AppBinding.refreshToken` carries its own privilege gate, so a reader without it gets\n * the record and an error pointing at that one field. That is not a failed read — the\n * rest of the binding is there and worth showing. Only a refusal of the record itself,\n * which arrives with no data, blanks the popup.\n */\n if (!response.data?.appBinding) {\n this.loadFailed = true\n return\n }\n\n this.loadFailed = false\n this.binding = response.data.appBinding\n }\n\n private async renewAccessToken(e: Event) {\n e.preventDefault()\n\n const confirmed = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('button.renew 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 renewApplicationAccessToken(id: $id) {\n accessToken\n refreshToken\n }\n }\n `,\n variables: { id: this.appBindingId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n /*\n * Stays open. The access token is minted here and stored nowhere — if this popup closed,\n * the value would be gone for good.\n */\n const { accessToken, refreshToken } = response.data.renewApplicationAccessToken\n\n this.failed = false\n this.accessToken = accessToken\n this.binding = { ...this.binding, refreshToken }\n this.dispatchEvent(new CustomEvent('app-binding-changed', { bubbles: true, composed: true }))\n }\n\n private async deleteBinding(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.binding?.application?.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 deleteAppBinding(id: $id)\n }\n `,\n variables: { id: this.appBindingId }\n })\n\n if (response.errors) {\n this.failed = true\n return\n }\n\n this.failed = false\n this.dispatchEvent(new CustomEvent('app-binding-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"]}
|
|
@@ -9,7 +9,7 @@ import { asyncReplace } from 'lit/directives/async-replace.js';
|
|
|
9
9
|
import { client } from '@operato/graphql';
|
|
10
10
|
import { i18next, localize } from '@operato/i18n';
|
|
11
11
|
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
12
|
-
import { CommonHeaderStyles, CommonPopupStyles } from '@operato/styles';
|
|
12
|
+
import { CommonHeaderStyles, CommonPopupStyles, ScrollbarStyles } from '@operato/styles';
|
|
13
13
|
import { parseJwt, sleep } from '@operato/utils';
|
|
14
14
|
import './secret-field.js';
|
|
15
15
|
import { changedValues, filledValues, missingRequired } from '../utils/form-values.js';
|
|
@@ -57,27 +57,62 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
57
57
|
/** The single-record read did not answer. Distinct from answering with nothing. */
|
|
58
58
|
this.loadFailed = false;
|
|
59
59
|
}
|
|
60
|
+
/*
|
|
61
|
+
* ── Where the scrolling happens ─────────────────────────────────────────
|
|
62
|
+
* The button row stays put and only the middle scrolls. That is the house shape for a
|
|
63
|
+
* popup, and this had it wrong: the host itself scrolled, so the row you press to finish
|
|
64
|
+
* walked off the bottom of the card along with everything else.
|
|
65
|
+
*
|
|
66
|
+
* So the host clips, and the content area between the title and the buttons is the one
|
|
67
|
+
* thing with overflow. Two details carry it:
|
|
68
|
+
*
|
|
69
|
+
* min-height: 0 A flex item defaults to min-height auto, which refuses to shrink below
|
|
70
|
+
* its own content. Without this the overflow rule sits there doing
|
|
71
|
+
* nothing and the card grows instead — it reads as a working rule.
|
|
72
|
+
* no padding CommonPopupStyles already pads the host. Padding here again doubles the
|
|
73
|
+
* inset and pushes the scrollbar in off the card edge.
|
|
74
|
+
*
|
|
75
|
+
* ScrollbarStyles gives the scrollbar the product's own shape rather than the browser's.
|
|
76
|
+
*
|
|
77
|
+
* ── Why the in-content buttons need a rule of their own ─────────────────
|
|
78
|
+
* CommonPopupStyles makes controls pill-shaped because a popup is a rounded card and the
|
|
79
|
+
* things inside it follow the card. But it says that through `.buttons`, so only the commit
|
|
80
|
+
* row got it: the actions sitting inside a fieldset — generate a new token, generate a new
|
|
81
|
+
* secret — kept the page radius and read as a different kind of button on the same card.
|
|
82
|
+
* Measured on a live popup: 5px inside the content, 13986px in the row below it.
|
|
83
|
+
*/
|
|
60
84
|
static { this.styles = [
|
|
61
85
|
CommonHeaderStyles,
|
|
62
86
|
CommonPopupStyles,
|
|
87
|
+
ScrollbarStyles,
|
|
63
88
|
css `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
/* in-content actions follow the card too, not just the commit row. see the note on styles. */
|
|
90
|
+
fieldset md-text-button,
|
|
91
|
+
fieldset md-filled-button {
|
|
92
|
+
--md-text-button-container-shape: var(--popup-control-radius);
|
|
93
|
+
--md-filled-button-container-shape: var(--popup-control-radius);
|
|
94
|
+
}
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
/* the host does not scroll; the content area below does. see the note on styles. */
|
|
97
|
+
:host {
|
|
98
|
+
overflow: hidden;
|
|
70
99
|
}
|
|
71
100
|
|
|
101
|
+
/* min-height:0 is load-bearing — see the note on styles. */
|
|
72
102
|
form {
|
|
73
103
|
flex: 1;
|
|
74
|
-
|
|
104
|
+
min-height: 0;
|
|
105
|
+
overflow-y: auto;
|
|
106
|
+
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
gap: var(--popup-gap);
|
|
75
110
|
}
|
|
76
111
|
|
|
77
112
|
fieldset {
|
|
78
113
|
border-radius: var(--border-radius);
|
|
79
114
|
border: var(--border-dim-color);
|
|
80
|
-
margin: 0
|
|
115
|
+
margin: 0;
|
|
81
116
|
padding: var(--popup-padding);
|
|
82
117
|
}
|
|
83
118
|
|
|
@@ -87,19 +122,11 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
87
122
|
color: var(--legend-color);
|
|
88
123
|
}
|
|
89
124
|
|
|
90
|
-
|
|
91
|
-
display: grid;
|
|
92
|
-
grid-template-columns: 1fr 1fr;
|
|
93
|
-
grid-gap: var(--popup-gap);
|
|
94
|
-
}
|
|
95
|
-
|
|
125
|
+
/* one labelled box. .row puts two of them side by side; alone it spans the card. */
|
|
96
126
|
[field] {
|
|
97
127
|
display: flex;
|
|
98
128
|
flex-direction: column;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
[grid-span] {
|
|
102
|
-
grid-column: span 2;
|
|
129
|
+
flex: 1;
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
label {
|
|
@@ -132,11 +159,6 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
132
159
|
margin-top: var(--spacing-tiny, 2px);
|
|
133
160
|
}
|
|
134
161
|
|
|
135
|
-
[input-hint] {
|
|
136
|
-
font-size: var(--popup-font-size-small);
|
|
137
|
-
color: var(--input-hint-color);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
162
|
/* The one reverting action goes left, away from the ones that commit. */
|
|
141
163
|
.buttons .left {
|
|
142
164
|
margin-right: auto;
|
|
@@ -162,26 +184,26 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
162
184
|
<form @submit=${(e) => e.preventDefault()}>
|
|
163
185
|
<fieldset>
|
|
164
186
|
<legend>${i18next.t('text.appliance')}</legend>
|
|
165
|
-
<div field
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</div>
|
|
187
|
+
<div field>
|
|
188
|
+
<label for="name">${i18next.t('field.name')}</label>
|
|
189
|
+
<input
|
|
190
|
+
id="name"
|
|
191
|
+
type="text"
|
|
192
|
+
name="name"
|
|
193
|
+
.value=${appliance.name || ''}
|
|
194
|
+
required
|
|
195
|
+
autofocus
|
|
196
|
+
?invalid=${this.missing.includes('name')}
|
|
197
|
+
/>
|
|
198
|
+
${this.fieldError('name')}
|
|
199
|
+
</div>
|
|
179
200
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
<div field>
|
|
202
|
+
<label for="description">${i18next.t('field.description')}</label>
|
|
203
|
+
<input id="description" type="text" name="description" .value=${appliance.description || ''} />
|
|
204
|
+
</div>
|
|
184
205
|
|
|
206
|
+
<div class="row">
|
|
185
207
|
<div field>
|
|
186
208
|
<label for="serial-no">${i18next.t('field.serial-no')}</label>
|
|
187
209
|
<input
|
|
@@ -207,7 +229,9 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
207
229
|
/>
|
|
208
230
|
${this.fieldError('brand')}
|
|
209
231
|
</div>
|
|
232
|
+
</div>
|
|
210
233
|
|
|
234
|
+
<div class="row">
|
|
211
235
|
<div field>
|
|
212
236
|
<label for="model">${i18next.t('field.model')}</label>
|
|
213
237
|
<input
|
|
@@ -255,11 +279,11 @@ let ApplianceEditor = class ApplianceEditor extends localize(i18next)(LitElement
|
|
|
255
279
|
return html `
|
|
256
280
|
<fieldset>
|
|
257
281
|
<legend>${i18next.t('text.appliance credential')}</legend>
|
|
258
|
-
<div field
|
|
282
|
+
<div field>
|
|
259
283
|
<label for="access-token">${i18next.t('label.access token')}</label>
|
|
260
284
|
<secret-field id="access-token" .value=${appliance.accessToken || ''}></secret-field>
|
|
261
285
|
${exp
|
|
262
|
-
? html `<div
|
|
286
|
+
? html `<div class="note">
|
|
263
287
|
${i18next.t('text.token expiry time')} ${new Date(exp * 1000).toLocaleString()} :
|
|
264
288
|
${asyncReplace(this.expTimer(exp))}
|
|
265
289
|
</div>`
|
|
@@ -1 +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"]}
|
|
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, ScrollbarStyles } 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 /*\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 /* 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>\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 <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>\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 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 </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"]}
|