@things-factory/auth-ui 10.1.13 → 10.1.17
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/role-template-picker.js +6 -8
- package/dist-client/components/role-template-picker.js.map +1 -1
- package/dist-client/components/secret-field.d.ts +35 -0
- package/dist-client/components/secret-field.js +149 -0
- package/dist-client/components/secret-field.js.map +1 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.d.ts +1 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.js +46 -0
- package/dist-client/pages/app-binding/app-binding-list-query.test.js.map +1 -0
- package/dist-client/pages/app-binding/app-binding.d.ts +1 -0
- package/dist-client/pages/app-binding/app-binding.js +8 -8
- package/dist-client/pages/app-binding/app-binding.js.map +1 -1
- package/dist-client/pages/app-binding/app-bindings.d.ts +63 -2
- package/dist-client/pages/app-binding/app-bindings.js +199 -117
- package/dist-client/pages/app-binding/app-bindings.js.map +1 -1
- package/dist-client/pages/appliance/appliance-list-query.test.d.ts +1 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js +58 -0
- package/dist-client/pages/appliance/appliance-list-query.test.js.map +1 -0
- package/dist-client/pages/appliance/appliance.d.ts +1 -0
- package/dist-client/pages/appliance/appliance.js +2 -4
- package/dist-client/pages/appliance/appliance.js.map +1 -1
- package/dist-client/pages/appliance/home.d.ts +61 -2
- package/dist-client/pages/appliance/home.js +200 -122
- package/dist-client/pages/appliance/home.js.map +1 -1
- package/dist-client/pages/application/application.d.ts +1 -0
- package/dist-client/pages/application/application.js +10 -3
- package/dist-client/pages/application/application.js.map +1 -1
- package/dist-client/pages/application/applications.d.ts +61 -2
- package/dist-client/pages/application/applications.js +193 -128
- package/dist-client/pages/application/applications.js.map +1 -1
- package/dist-client/pages/application/register.js +81 -27
- package/dist-client/pages/application/register.js.map +1 -1
- package/dist-client/pages/auth-provider/auth-provider-management.d.ts +4 -2
- package/dist-client/pages/auth-provider/auth-provider-management.js +35 -6
- package/dist-client/pages/auth-provider/auth-provider-management.js.map +1 -1
- package/dist-client/pages/domain-owner/domain-owner-management.d.ts +2 -0
- package/dist-client/pages/domain-owner/domain-owner-management.js +34 -7
- package/dist-client/pages/domain-owner/domain-owner-management.js.map +1 -1
- package/dist-client/pages/role/role-list-page.d.ts +20 -0
- package/dist-client/pages/role/role-list-page.js +58 -1
- package/dist-client/pages/role/role-list-page.js.map +1 -1
- package/dist-client/pages/role/role-privilege-page.js +1 -10
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/pages/secrets-are-not-printed.test.d.ts +1 -0
- package/dist-client/pages/secrets-are-not-printed.test.js +70 -0
- package/dist-client/pages/secrets-are-not-printed.test.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/application-status.d.ts +32 -0
- package/dist-client/utils/application-status.js +45 -0
- package/dist-client/utils/application-status.js.map +1 -0
- package/dist-client/utils/application-status.test.d.ts +1 -0
- package/dist-client/utils/application-status.test.js +45 -0
- package/dist-client/utils/application-status.test.js.map +1 -0
- package/dist-client/utils/form-values.d.ts +42 -0
- package/dist-client/utils/form-values.js +57 -0
- package/dist-client/utils/form-values.js.map +1 -0
- package/dist-client/utils/form-values.test.d.ts +1 -0
- package/dist-client/utils/form-values.test.js +62 -0
- package/dist-client/utils/form-values.test.js.map +1 -0
- package/dist-client/utils/list-result.d.ts +62 -0
- package/dist-client/utils/list-result.js +92 -0
- package/dist-client/utils/list-result.js.map +1 -0
- package/dist-client/utils/list-result.test.d.ts +1 -0
- package/dist-client/utils/list-result.test.js +160 -0
- package/dist-client/utils/list-result.test.js.map +1 -0
- package/dist-client/utils/privilege-axis.d.ts +52 -0
- package/dist-client/utils/privilege-axis.js +55 -0
- package/dist-client/utils/privilege-axis.js.map +1 -0
- package/dist-client/utils/privilege-axis.test.d.ts +1 -0
- package/dist-client/utils/privilege-axis.test.js +39 -0
- package/dist-client/utils/privilege-axis.test.js.map +1 -0
- package/dist-client/utils/secret-display.d.ts +51 -0
- package/dist-client/utils/secret-display.js +61 -0
- package/dist-client/utils/secret-display.js.map +1 -0
- package/dist-client/utils/secret-display.test.d.ts +1 -0
- package/dist-client/utils/secret-display.test.js +59 -0
- package/dist-client/utils/secret-display.test.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/translations/en.json +33 -0
- package/translations/ja.json +33 -0
- package/translations/ko.json +33 -0
- package/translations/ms.json +33 -0
- package/translations/zh.json +33 -0
|
@@ -1,149 +1,231 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
|
-
import '@material/web/
|
|
3
|
-
import '@
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import '@operato/data-grist/ox-grist.js';
|
|
4
|
+
import '@operato/context/ox-context-page-toolbar.js';
|
|
4
5
|
import gql from 'graphql-tag';
|
|
5
6
|
import { css, html } from 'lit';
|
|
6
|
-
import { customElement, property } from 'lit/decorators.js';
|
|
7
|
-
import { client } from '@operato/graphql';
|
|
7
|
+
import { customElement, property, query } from 'lit/decorators.js';
|
|
8
|
+
import { client, gqlContext } from '@operato/graphql';
|
|
9
|
+
import { i18next, localize } from '@operato/i18n';
|
|
8
10
|
import { navigate, PageView } from '@operato/shell';
|
|
9
|
-
|
|
11
|
+
import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles';
|
|
12
|
+
import { isMobileDevice } from '@operato/utils';
|
|
13
|
+
import { p13n } from '@operato/p13n';
|
|
14
|
+
import { listOutcome, pageFromResponse } from '../../utils/list-result.js';
|
|
15
|
+
/**
|
|
16
|
+
* Applications bound to this domain.
|
|
17
|
+
*
|
|
18
|
+
* A binding is a `User` row with `userType: 'application'` — the account an application signs
|
|
19
|
+
* in as. The list shows who it is and what it may do; what it signs in *with* is elsewhere.
|
|
20
|
+
*
|
|
21
|
+
* ── The server answers honestly now ──────────────────────────────────────────
|
|
22
|
+
* Until dde62cd6a1 this query paged before it filtered: the database took a page of users of
|
|
23
|
+
* every type, JavaScript kept the applications among them, and the total reported how many
|
|
24
|
+
* survived that one page. A grid told there were three when there were thirty never offered a
|
|
25
|
+
* second page. Wiring paging up was pointless until that was fixed; it works now.
|
|
26
|
+
*
|
|
27
|
+
* ── ⚠ The refresh token is not asked for, and a test holds that ──────────────
|
|
28
|
+
* AppBinding's refresh token is the application's actual credential — oauth2-server.ts writes
|
|
29
|
+
* it into the password column verbatim, not hashed — and the field carries its own
|
|
30
|
+
* privilege directive for security:query.
|
|
31
|
+
*
|
|
32
|
+
* A credential that can be exchanged for access tokens does not belong in a grid cell, where
|
|
33
|
+
* it sits in plain text beside a name for anyone who can open the screen, and in any
|
|
34
|
+
* screenshot of it. `app-binding-list-query.test.ts` fails if the word appears here.
|
|
35
|
+
*/
|
|
36
|
+
let AppBindings = class AppBindings extends p13n(localize(i18next)(PageView)) {
|
|
10
37
|
constructor() {
|
|
11
38
|
super(...arguments);
|
|
12
|
-
this.
|
|
39
|
+
this.active = false;
|
|
40
|
+
this.mode = isMobileDevice() ? 'CARD' : 'GRID';
|
|
41
|
+
/** The last fetch did not answer. Distinct from answering with nothing. */
|
|
42
|
+
this.failed = false;
|
|
13
43
|
}
|
|
14
44
|
static { this.styles = [
|
|
45
|
+
CommonGristStyles,
|
|
46
|
+
CommonHeaderStyles,
|
|
47
|
+
ScrollbarStyles,
|
|
15
48
|
css `
|
|
16
|
-
:host {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
overflow:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
color: var(--title-text-color);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
[page-description] { margin: var(--page-description-margin);
|
|
36
|
-
font: var(--page-description-font);
|
|
37
|
-
color: var(--page-description-color);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
table { width: 100%;
|
|
41
|
-
margin: var(--spacing-large) 0;
|
|
42
|
-
border-collapse: collapse;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
tr { background-color: var(--tr-background-color);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
tr:nth-child(odd) { background-color: var(--tr-background-odd-color);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
tr:hover { background-color: var(--tr-background-hover-color);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
th { border-top: var(--th-border-top);
|
|
55
|
-
border-bottom: var(--td-border-bottom);
|
|
56
|
-
padding: var(--th-padding);
|
|
57
|
-
|
|
58
|
-
font: var(--th-font);
|
|
59
|
-
color: var(--th-color);
|
|
60
|
-
text-transform: var(--th-text-transform);
|
|
61
|
-
text-align: left;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
td { padding: var(--td-padding);
|
|
65
|
-
border-bottom: var(--td-border-bottom);
|
|
66
|
-
font: var(--td-font);
|
|
67
|
-
color: var(--td-color);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
td a { color: var(--md-sys-color-primary);
|
|
71
|
-
font: bold 16px var(--theme-font);
|
|
72
|
-
|
|
73
|
-
display: block;
|
|
74
|
-
text-decoration: none;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.text-align-center { text-align: center;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.text-align-right { text-align: right;
|
|
81
|
-
}
|
|
49
|
+
:host {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ox-grist {
|
|
56
|
+
overflow-y: auto;
|
|
57
|
+
flex: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[failed] {
|
|
61
|
+
padding: var(--spacing-medium);
|
|
62
|
+
color: var(--md-sys-color-error);
|
|
63
|
+
font: var(--label-font, inherit);
|
|
64
|
+
}
|
|
82
65
|
`
|
|
83
66
|
]; }
|
|
84
67
|
get context() {
|
|
85
|
-
return {
|
|
86
|
-
|
|
68
|
+
return {
|
|
69
|
+
title: i18next.t('text.bound applications'),
|
|
70
|
+
help: 'auth/application',
|
|
71
|
+
search: {
|
|
72
|
+
handler: (search) => {
|
|
73
|
+
this.grist.searchText = search;
|
|
74
|
+
},
|
|
75
|
+
value: this.grist?.searchText || ''
|
|
76
|
+
},
|
|
77
|
+
actions: [
|
|
78
|
+
{
|
|
79
|
+
title: i18next.t('text.applications'),
|
|
80
|
+
action: () => navigate('applications'),
|
|
81
|
+
icon: 'apps'
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
toolbar: false
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
87
|
render() {
|
|
90
|
-
var appBindings = this.appBindings || [];
|
|
91
88
|
return html `
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<tr>
|
|
105
|
-
<td>
|
|
106
|
-
<a href=${`app-binding/${appBinding.id}`}>${appBinding.name}</a>
|
|
107
|
-
${appBinding.description}
|
|
108
|
-
</td>
|
|
109
|
-
<td>${appBinding.scope}</td>
|
|
110
|
-
<td>${appBinding.status}</td>
|
|
111
|
-
</tr>
|
|
112
|
-
`)}
|
|
113
|
-
</table>
|
|
114
|
-
<md-elevated-button @click=${e => navigate('applications')}>registered applications ..</md-elevated-button>
|
|
89
|
+
<ox-grist
|
|
90
|
+
.mode=${this.mode}
|
|
91
|
+
.config=${this.config}
|
|
92
|
+
.fetchHandler=${this.fetchHandler.bind(this)}
|
|
93
|
+
.personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')}
|
|
94
|
+
>
|
|
95
|
+
<div slot="headroom" class="header">
|
|
96
|
+
<ox-context-page-toolbar class="actions" .context=${this.context}></ox-context-page-toolbar>
|
|
97
|
+
</div>
|
|
98
|
+
</ox-grist>
|
|
99
|
+
|
|
100
|
+
${this.failed ? html `<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}
|
|
115
101
|
`;
|
|
116
102
|
}
|
|
117
|
-
async
|
|
103
|
+
async pageInitialized() {
|
|
104
|
+
this.config = {
|
|
105
|
+
rows: { appendable: false, selectable: { multiple: false } },
|
|
106
|
+
columns: [
|
|
107
|
+
{ type: 'gutter', gutterName: 'sequence' },
|
|
108
|
+
{
|
|
109
|
+
type: 'gutter',
|
|
110
|
+
gutterName: 'button',
|
|
111
|
+
icon: 'chevron_right',
|
|
112
|
+
title: i18next.t('button.detail'),
|
|
113
|
+
handlers: {
|
|
114
|
+
click: (_columns, _data, _column, record) => {
|
|
115
|
+
if (record?.id) {
|
|
116
|
+
navigate(`app-binding/${record.id}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'string',
|
|
123
|
+
name: 'name',
|
|
124
|
+
header: i18next.t('field.name'),
|
|
125
|
+
record: { editable: false },
|
|
126
|
+
sortable: true,
|
|
127
|
+
width: 180
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'string',
|
|
131
|
+
name: 'applicationName',
|
|
132
|
+
header: i18next.t('label.application'),
|
|
133
|
+
record: { editable: false },
|
|
134
|
+
width: 180
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'string',
|
|
138
|
+
name: 'email',
|
|
139
|
+
header: i18next.t('field.email'),
|
|
140
|
+
record: { editable: false },
|
|
141
|
+
sortable: true,
|
|
142
|
+
width: 220
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'string',
|
|
146
|
+
name: 'scope',
|
|
147
|
+
header: i18next.t('field.scope'),
|
|
148
|
+
record: { editable: false },
|
|
149
|
+
width: 220
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'string',
|
|
153
|
+
name: 'status',
|
|
154
|
+
header: i18next.t('field.status'),
|
|
155
|
+
record: { align: 'center', editable: false },
|
|
156
|
+
sortable: true,
|
|
157
|
+
width: 130
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: 'datetime',
|
|
161
|
+
name: 'updatedAt',
|
|
162
|
+
header: i18next.t('field.updated at'),
|
|
163
|
+
sortable: true,
|
|
164
|
+
width: 180
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async pageUpdated(_changes, _lifecycle) {
|
|
118
170
|
if (this.active) {
|
|
119
|
-
|
|
171
|
+
await this.updateComplete;
|
|
172
|
+
this.grist.fetch();
|
|
120
173
|
}
|
|
121
174
|
}
|
|
122
|
-
async
|
|
123
|
-
const response = await client.query({
|
|
124
|
-
|
|
175
|
+
async fetchHandler({ page, limit, sortings = [], filters = [] }) {
|
|
176
|
+
const response = await client.query({
|
|
177
|
+
query: gql `
|
|
178
|
+
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
|
|
179
|
+
appBindings(filters: $filters, pagination: $pagination, sortings: $sortings) {
|
|
180
|
+
items {
|
|
181
|
+
id
|
|
125
182
|
name
|
|
126
|
-
|
|
127
|
-
name
|
|
128
|
-
}
|
|
129
|
-
scope
|
|
183
|
+
email
|
|
130
184
|
status
|
|
131
|
-
|
|
185
|
+
scope
|
|
186
|
+
updatedAt
|
|
187
|
+
application {
|
|
188
|
+
id
|
|
189
|
+
name
|
|
190
|
+
}
|
|
191
|
+
}
|
|
132
192
|
total
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
`
|
|
136
|
-
});
|
|
137
|
-
if (!response.errors) {
|
|
138
|
-
return response.data.appBindings;
|
|
193
|
+
}
|
|
139
194
|
}
|
|
195
|
+
`,
|
|
196
|
+
variables: { filters, pagination: { page, limit }, sortings },
|
|
197
|
+
context: gqlContext()
|
|
198
|
+
});
|
|
199
|
+
const { items, total, failed } = listOutcome(pageFromResponse(response, 'appBindings'));
|
|
200
|
+
this.failed = failed;
|
|
201
|
+
return {
|
|
202
|
+
total,
|
|
203
|
+
records: items.map((item) => ({ ...item, applicationName: item.application?.name || '' }))
|
|
204
|
+
};
|
|
140
205
|
}
|
|
141
206
|
};
|
|
142
207
|
__decorate([
|
|
143
|
-
property({ type:
|
|
144
|
-
__metadata("design:type",
|
|
145
|
-
], AppBindings.prototype, "
|
|
208
|
+
property({ type: Boolean }),
|
|
209
|
+
__metadata("design:type", Boolean)
|
|
210
|
+
], AppBindings.prototype, "active", void 0);
|
|
211
|
+
__decorate([
|
|
212
|
+
property({ type: Object }),
|
|
213
|
+
__metadata("design:type", Object)
|
|
214
|
+
], AppBindings.prototype, "config", void 0);
|
|
215
|
+
__decorate([
|
|
216
|
+
property({ type: String }),
|
|
217
|
+
__metadata("design:type", String)
|
|
218
|
+
], AppBindings.prototype, "mode", void 0);
|
|
219
|
+
__decorate([
|
|
220
|
+
property({ type: Boolean }),
|
|
221
|
+
__metadata("design:type", Boolean)
|
|
222
|
+
], AppBindings.prototype, "failed", void 0);
|
|
223
|
+
__decorate([
|
|
224
|
+
query('ox-grist'),
|
|
225
|
+
__metadata("design:type", Function)
|
|
226
|
+
], AppBindings.prototype, "grist", void 0);
|
|
146
227
|
AppBindings = __decorate([
|
|
147
228
|
customElement('app-bindings-page')
|
|
148
229
|
], AppBindings);
|
|
230
|
+
export { AppBindings };
|
|
149
231
|
//# sourceMappingURL=app-bindings.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-bindings.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-bindings.ts"],"names":[],"mappings":";AAAA,OAAO,yCAAyC,CAAA;AAChD,OAAO,yCAAyC,CAAA;AAEhD,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGnD,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,QAAQ;IAAlC;;QAuE6B,gBAAW,GAAU,EAAE,CAAA;IA4DpD,CAAC;aAnI6C,WAAM,GAAG;QACnD,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAmEF;KACF,AArEiD,CAqEjD;IAID,IAAI,OAAO;QAAS,OAAO,EAAQ,KAAK,EAAE,sBAAsB;YAC5D,IAAI,EAAE,kBAAkB;SAC5B,CAAA;IACD,CAAC;IAEA,MAAM;QAAS,IAAI,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,CAAA;QAErD,OAAO,IAAI,CAAA;;;;;;;;;;;;UAYL,WAAW,CAAC,GAAG,CACf,UAAU,CAAC,EAAE,CAAC,IAAI,CAAA;;;0BAGF,eAAe,UAAU,CAAC,EAAE,EAAE,IAAI,UAAU,CAAC,IAAI;kBACzD,UAAU,CAAC,WAAW;;oBAEpB,UAAU,CAAC,KAAK;oBAChB,UAAU,CAAC,MAAM;;WAE1B,CACF;;mCAE0B,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;KAC3D,CAAA;IACJ,CAAC;IAEA,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM;QAAQ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAAO,IAAI,CAAC,WAAW,GAAG,CAAC,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,KAAK,CAAA;QACtI,CAAC;IACD,CAAC;IAEA,KAAK,CAAC,gBAAgB;QAAS,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,EAAQ,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;OAYhF;SACL,CAAC,CAAA;QAEC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAO,OAAO,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAA;QACjE,CAAC;IACD,CAAC;;AA3D2B;IAA1B,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;;gDAAwB;AAvE9C,WAAW;IADhB,aAAa,CAAC,mBAAmB,CAAC;GAC7B,WAAW,CAmIhB","sourcesContent":["import '@material/web/button/elevated-button.js'\nimport '@material/web/button/outlined-button.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { client } from '@operato/graphql'\nimport { navigate, PageView } from '@operato/shell'\n\n@customElement('app-bindings-page')\nclass AppBindings extends PageView { static styles = [\n css`\n :host { background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n\n overflow: auto;\n }\n\n md-elevated-button { text-transform: capitalize;\n }\n\n md-outlined-button { float: right;\n margin-top: var(--spacing-medium);\n text-transform: capitalize;\n }\n\n h2 { margin: var(--title-margin);\n font: var(--title-font);\n color: var(--title-text-color);\n }\n\n [page-description] { margin: var(--page-description-margin);\n font: var(--page-description-font);\n color: var(--page-description-color);\n }\n\n table { width: 100%;\n margin: var(--spacing-large) 0;\n border-collapse: collapse;\n }\n\n tr { background-color: var(--tr-background-color);\n }\n\n tr:nth-child(odd) { background-color: var(--tr-background-odd-color);\n }\n\n tr:hover { background-color: var(--tr-background-hover-color);\n }\n\n th { border-top: var(--th-border-top);\n border-bottom: var(--td-border-bottom);\n padding: var(--th-padding);\n\n font: var(--th-font);\n color: var(--th-color);\n text-transform: var(--th-text-transform);\n text-align: left;\n }\n\n td { padding: var(--td-padding);\n border-bottom: var(--td-border-bottom);\n font: var(--td-font);\n color: var(--td-color);\n }\n\n td a { color: var(--md-sys-color-primary);\n font: bold 16px var(--theme-font);\n\n display: block;\n text-decoration: none;\n }\n\n .text-align-center { text-align: center;\n }\n\n .text-align-right { text-align: right;\n }\n `\n ]\n\n @property({ type: Array }) appBindings: any[] = []\n\n get context() { return { title: `application bindings`,\n help: 'auth/application'\n }\n }\n\n render() { var appBindings = this.appBindings || []\n\n return html`\n <div>\n <h2>Bound Applications</h2>\n <p page-description>Bound Applications goes here</p>\n </div>\n\n <table>\n <tr>\n <th>app name</th>\n <th>Scope</th>\n <th>status</th>\n </tr>\n ${appBindings.map(\n appBinding => html`\n <tr>\n <td>\n <a href=${`app-binding/${appBinding.id}`}>${appBinding.name}</a>\n ${appBinding.description}\n </td>\n <td>${appBinding.scope}</td>\n <td>${appBinding.status}</td>\n </tr>\n `\n )}\n </table>\n <md-elevated-button @click=${e => navigate('applications')}>registered applications ..</md-elevated-button>\n `\n }\n\n async pageUpdated(changes, lifecycle, before) { if (this.active) { this.appBindings = (await this.fetchAppBindings()).items\n }\n }\n\n async fetchAppBindings() { const response = await client.query({ query: gql`\n query { appBindings { items { id\n name\n application { id\n name\n }\n scope\n status\n }\n total\n }\n }\n `\n })\n\n if (!response.errors) { return response.data.appBindings\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"app-bindings.js","sourceRoot":"","sources":["../../../client/pages/app-binding/app-bindings.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport '@operato/data-grist/ox-grist.js'\nimport '@operato/context/ox-context-page-toolbar.js'\n\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\n\nimport type { DataGrist, FetchOption } from '@operato/data-grist'\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\nimport { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'\nimport { isMobileDevice } from '@operato/utils'\nimport { p13n } from '@operato/p13n'\n\nimport { listOutcome, pageFromResponse } from '../../utils/list-result.js'\n\n/**\n * Applications bound to this domain.\n *\n * A binding is a `User` row with `userType: 'application'` — the account an application signs\n * in as. The list shows who it is and what it may do; what it signs in *with* is elsewhere.\n *\n * ── The server answers honestly now ──────────────────────────────────────────\n * Until dde62cd6a1 this query paged before it filtered: the database took a page of users of\n * every type, JavaScript kept the applications among them, and the total reported how many\n * survived that one page. A grid told there were three when there were thirty never offered a\n * second page. Wiring paging up was pointless until that was fixed; it works now.\n *\n * ── ⚠ The refresh token is not asked for, and a test holds that ──────────────\n * AppBinding's refresh token is the application's actual credential — oauth2-server.ts writes\n * it into the password column verbatim, not hashed — and the field carries its own\n * privilege directive for security:query.\n *\n * A credential that can be exchanged for access tokens does not belong in a grid cell, where\n * it sits in plain text beside a name for anyone who can open the screen, and in any\n * screenshot of it. `app-binding-list-query.test.ts` fails if the word appears here.\n */\n@customElement('app-bindings-page')\nexport class AppBindings extends p13n(localize(i18next)(PageView)) {\n static styles = [\n CommonGristStyles,\n CommonHeaderStyles,\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n ox-grist {\n overflow-y: auto;\n flex: 1;\n }\n\n [failed] {\n padding: var(--spacing-medium);\n color: var(--md-sys-color-error);\n font: var(--label-font, inherit);\n }\n `\n ]\n\n @property({ type: Boolean }) active: boolean = false\n @property({ type: Object }) config: any\n @property({ type: String }) mode: 'CARD' | 'LIST' | 'GRID' = isMobileDevice() ? 'CARD' : 'GRID'\n\n /** The last fetch did not answer. Distinct from answering with nothing. */\n @property({ type: Boolean }) failed: boolean = false\n\n @query('ox-grist') private grist!: DataGrist\n\n get context() {\n return {\n title: i18next.t('text.bound applications'),\n help: 'auth/application',\n search: {\n handler: (search: string) => {\n this.grist.searchText = search\n },\n value: this.grist?.searchText || ''\n },\n actions: [\n {\n title: i18next.t('text.applications'),\n action: () => navigate('applications'),\n icon: 'apps'\n }\n ],\n toolbar: false\n }\n }\n\n render() {\n return html`\n <ox-grist\n .mode=${this.mode}\n .config=${this.config}\n .fetchHandler=${this.fetchHandler.bind(this)}\n .personalConfigProvider=${this.getPagePreferenceProvider('ox-grist')!}\n >\n <div slot=\"headroom\" class=\"header\">\n <ox-context-page-toolbar class=\"actions\" .context=${this.context}></ox-context-page-toolbar>\n </div>\n </ox-grist>\n\n ${this.failed ? html`<div failed>${i18next.t('text.the list could not be loaded')}</div>` : ''}\n `\n }\n\n async pageInitialized() {\n this.config = {\n rows: { appendable: false, selectable: { multiple: false } },\n columns: [\n { type: 'gutter', gutterName: 'sequence' },\n {\n type: 'gutter',\n gutterName: 'button',\n icon: 'chevron_right',\n title: i18next.t('button.detail'),\n handlers: {\n click: (_columns: any, _data: any, _column: any, record: any) => {\n if (record?.id) {\n navigate(`app-binding/${record.id}`)\n }\n }\n }\n },\n {\n type: 'string',\n name: 'name',\n header: i18next.t('field.name'),\n record: { editable: false },\n sortable: true,\n width: 180\n },\n {\n type: 'string',\n name: 'applicationName',\n header: i18next.t('label.application'),\n record: { editable: false },\n width: 180\n },\n {\n type: 'string',\n name: 'email',\n header: i18next.t('field.email'),\n record: { editable: false },\n sortable: true,\n width: 220\n },\n {\n type: 'string',\n name: 'scope',\n header: i18next.t('field.scope'),\n record: { editable: false },\n width: 220\n },\n {\n type: 'string',\n name: 'status',\n header: i18next.t('field.status'),\n record: { align: 'center', editable: false },\n sortable: true,\n width: 130\n },\n {\n type: 'datetime',\n name: 'updatedAt',\n header: i18next.t('field.updated at'),\n sortable: true,\n width: 180\n }\n ]\n }\n }\n\n async pageUpdated(_changes: any, _lifecycle: any) {\n if (this.active) {\n await this.updateComplete\n this.grist.fetch()\n }\n }\n\n async fetchHandler({ page, limit, sortings = [], filters = [] }: FetchOption) {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n appBindings(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items {\n id\n name\n email\n status\n scope\n updatedAt\n application {\n id\n name\n }\n }\n total\n }\n }\n `,\n variables: { filters, pagination: { page, limit }, sortings },\n context: gqlContext()\n })\n\n const { items, total, failed } = listOutcome(pageFromResponse(response, 'appBindings'))\n\n this.failed = failed\n\n return {\n total,\n records: items.map((item: any) => ({ ...item, applicationName: item.application?.name || '' }))\n }\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* A list query must not ask for a field that carries its own privilege gate.
|
|
5
|
+
*
|
|
6
|
+
* `Appliance.accessToken` is declared with
|
|
7
|
+
* `@privilege(category: "security", privilege: "query", domainOwnerGranted: true)` on the
|
|
8
|
+
* field. A viewer without that privilege who asks for it gets the rows **and** an error
|
|
9
|
+
* about the one field.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ An earlier version of this comment said that would black the whole list out. It would
|
|
12
|
+
* have, until `pageFromResponse` learned to read the error's path and keep a page whose only
|
|
13
|
+
* complaint points at a value inside it. That reasoning is gone; the rule stands on its own
|
|
14
|
+
* ground instead:
|
|
15
|
+
*
|
|
16
|
+
* A token in a list is a token shown to everyone who may open the list, sitting in a grid
|
|
17
|
+
* cell, in plain text, next to a name. Nothing about the appliance list needs it, and the
|
|
18
|
+
* screens that do need it ask for it one record at a time. It is one word in a query and
|
|
19
|
+
* nothing else in the build objects to it, which is why a test holds it rather than a
|
|
20
|
+
* comment.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ The comments are stripped before searching. The doc comment above names `accessToken`
|
|
23
|
+
* three times; searching the raw file would find the prose and pass over a query that
|
|
24
|
+
* really does ask for it. That is the defect this repository produced eight times in one
|
|
25
|
+
* day — a measurement that reads the same words as the thing it measures.
|
|
26
|
+
*/
|
|
27
|
+
function source(name) {
|
|
28
|
+
const text = readFileSync(join(__dirname, name), 'utf-8');
|
|
29
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
|
|
30
|
+
}
|
|
31
|
+
/** The fields Appliance declares with no gate of their own. */
|
|
32
|
+
const UNGATED = ['name', 'serialNo', 'brand', 'model', 'netmask', 'description', 'updatedAt'];
|
|
33
|
+
describe('the appliance list query', () => {
|
|
34
|
+
it('does not ask for the access token', () => {
|
|
35
|
+
expect(source('./home.ts')).not.toContain('accessToken');
|
|
36
|
+
});
|
|
37
|
+
it('asks for the fields the columns render', () => {
|
|
38
|
+
/*
|
|
39
|
+
* The other half. A column whose field is not requested renders empty for every row,
|
|
40
|
+
* which reads as "this appliance has no serial number" rather than "nobody asked".
|
|
41
|
+
*/
|
|
42
|
+
const text = source('./home.ts');
|
|
43
|
+
for (const field of UNGATED) {
|
|
44
|
+
expect(text).toContain(field);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it('passes the grist options through, so paging and sorting reach the server', () => {
|
|
48
|
+
/*
|
|
49
|
+
* The screen this replaced fetched everything on every visit and sorted nothing. The
|
|
50
|
+
* server takes ListParam; forgetting to pass it is invisible until the table is long.
|
|
51
|
+
*/
|
|
52
|
+
const text = source('./home.ts');
|
|
53
|
+
expect(text).toContain('$pagination: Pagination');
|
|
54
|
+
expect(text).toContain('$sortings: [Sorting!]');
|
|
55
|
+
expect(text).toContain('$filters: [Filter!]');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=appliance-list-query.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appliance-list-query.test.js","sourceRoot":"","sources":["../../../client/pages/appliance/appliance-list-query.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAS,MAAM,CAAC,IAAY;IAC1B,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;IAEzD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAA;AAC9E,CAAC;AAED,+DAA+D;AAC/D,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,CAAC,CAAA;AAE7F,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,CAAA;IAC1D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD;;;WAGG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAA;QAEhC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF;;;WAGG;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAA;QAEhC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAA;QACjD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAA;QAC/C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,qBAAqB,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["import { readFileSync } from 'fs'\nimport { join } from 'path'\n\n/**\n * A list query must not ask for a field that carries its own privilege gate.\n *\n * `Appliance.accessToken` is declared with\n * `@privilege(category: \"security\", privilege: \"query\", domainOwnerGranted: true)` on the\n * field. A viewer without that privilege who asks for it gets the rows **and** an error\n * about the one field.\n *\n * ⚠ An earlier version of this comment said that would black the whole list out. It would\n * have, until `pageFromResponse` learned to read the error's path and keep a page whose only\n * complaint points at a value inside it. That reasoning is gone; the rule stands on its own\n * ground instead:\n *\n * A token in a list is a token shown to everyone who may open the list, sitting in a grid\n * cell, in plain text, next to a name. Nothing about the appliance list needs it, and the\n * screens that do need it ask for it one record at a time. It is one word in a query and\n * nothing else in the build objects to it, which is why a test holds it rather than a\n * comment.\n *\n * ⚠ The comments are stripped before searching. The doc comment above names `accessToken`\n * three times; searching the raw file would find the prose and pass over a query that\n * really does ask for it. That is the defect this repository produced eight times in one\n * day — a measurement that reads the same words as the thing it measures.\n */\nfunction source(name: string): string {\n const text = readFileSync(join(__dirname, name), 'utf-8')\n\n return text.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '').replace(/^[ \\t]*\\/\\/.*$/gm, '')\n}\n\n/** The fields Appliance declares with no gate of their own. */\nconst UNGATED = ['name', 'serialNo', 'brand', 'model', 'netmask', 'description', 'updatedAt']\n\ndescribe('the appliance list query', () => {\n it('does not ask for the access token', () => {\n expect(source('./home.ts')).not.toContain('accessToken')\n })\n\n it('asks for the fields the columns render', () => {\n /*\n * The other half. A column whose field is not requested renders empty for every row,\n * which reads as \"this appliance has no serial number\" rather than \"nobody asked\".\n */\n const text = source('./home.ts')\n\n for (const field of UNGATED) {\n expect(text).toContain(field)\n }\n })\n\n it('passes the grist options through, so paging and sorting reach the server', () => {\n /*\n * The screen this replaced fetched everything on every visit and sorted nothing. The\n * server takes ListParam; forgetting to pass it is invisible until the table is long.\n */\n const text = source('./home.ts')\n\n expect(text).toContain('$pagination: Pagination')\n expect(text).toContain('$sortings: [Sorting!]')\n expect(text).toContain('$filters: [Filter!]')\n })\n})\n"]}
|
|
@@ -7,6 +7,7 @@ import { customElement, property, queryAll } from 'lit/decorators.js';
|
|
|
7
7
|
import { asyncReplace } from 'lit/directives/async-replace.js';
|
|
8
8
|
import { client } from '@operato/graphql';
|
|
9
9
|
import { navigate, PageView } from '@operato/shell';
|
|
10
|
+
import '../../components/secret-field.js';
|
|
10
11
|
import { parseJwt, sleep } from '@operato/utils';
|
|
11
12
|
import { i18next } from '@operato/i18n';
|
|
12
13
|
let Appliance = class Appliance extends PageView {
|
|
@@ -218,10 +219,7 @@ let Appliance = class Appliance extends PageView {
|
|
|
218
219
|
<div field-2column>
|
|
219
220
|
<div field grid-span>
|
|
220
221
|
<label for="access-token">${i18next.t('label.access token')}</label>
|
|
221
|
-
<
|
|
222
|
-
<button button-in-field clipboard-copy @click=${e => e.preventDefault()}>
|
|
223
|
-
${i18next.t('button.copy')}
|
|
224
|
-
</button>
|
|
222
|
+
<secret-field id="access-token" .value=${appliance.accessToken || ''}></secret-field>
|
|
225
223
|
${accessTokenExp
|
|
226
224
|
? html `<div input-hint>
|
|
227
225
|
${i18next.t('text.token expiry time')} ${new Date(accessTokenExp * 1000).toLocaleString()} :
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appliance.js","sourceRoot":"","sources":["../../../client/pages/appliance/appliance.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport Clipboard from 'clipboard'\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, queryAll } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\nimport { client } from '@operato/graphql'\nimport { navigate, PageView } from '@operato/shell'\nimport { parseJwt, sleep } from '@operato/utils'\nimport { i18next } from '@operato/i18n'\n\n@customElement('appliance-page')\nclass Appliance extends PageView { static styles = [\n css`\n :host { display: flex;\n flex-direction: column;\n overflow-y: auto;\n position: relative;\n\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n }\n\n h2 { margin: var(--title-margin);\n font: var(--title-font);\n color: var(--title-text-color);\n }\n\n [page-description] { margin: var(--page-description-margin);\n font: var(--page-description-font);\n color: var(--page-description-color);\n }\n\n [icon] { position: absolute;\n top: 10px;\n right: 10px;\n max-width: 80px;\n }\n\n [icon] img { max-width: 100%;\n max-height: 100%;\n }\n\n [button-primary] { background-color: var(--button-primary-background-color);\n border: var(--button-border);\n border-radius: var(--button-border-radius);\n margin: var(--button-margin);\n padding: var(--button-primary-padding);\n color: var(--button-primary-color);\n font: var(--button-primary-font);\n text-transform: var(--button-text-transform);\n\n text-decoration: none;\n }\n\n [button-primary]:hover { background-color: var(--button-primary-active-background-color);\n box-shadow: var(--button-active-box-shadow);\n }\n\n [fieldset-container] { background-color: var(--md-sys-color-surface);\n margin: var(--spacing-large) 0 var(--spacing-medium) 0;\n padding: var(--spacing-medium);\n border-radius: var(--border-radius);\n box-shadow: var(--box-shadow);\n\n label { 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 select {\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 flex: 1;\n }\n\n select:focus,\n input:focus,\n button { outline: none;\n }\n\n form { max-width: var(--content-container-max-width);\n }\n }\n\n [fieldset-container] fieldset { margin: 0;\n margin-top: -15px;\n }\n\n fieldset { border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: var(--fieldset-margin);\n padding: var(--fieldset-padding);\n }\n\n legend { padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n [field-2column] { display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: 15px;\n }\n\n [field] { display: flex;\n flex-direction: column;\n position: relative;\n }\n\n [grid-span] { grid-column: span 2;\n }\n\n button { display: flex;\n align-items: center;\n gap: var(--spacing-small);\n }\n\n button,\n input[type='submit'],\n [button-in-field] { background-color: var(--button-background-color);\n border: var(--button-border);\n border-radius: var(--button-border-radius);\n padding: var(--button-padding);\n color: var(--button-color);\n font: var(--button-font);\n text-transform: var(--button-text-transform);\n\n margin: var(--spacing-medium) 0 var(--spacing-medium) var(--spacing-medium);\n float: right;\n text-decoration: none;\n }\n\n button:hover,\n input[type='submit']:hover { border: var(--button-activ-border);\n box-shadow: var(--button-active-box-shadow);\n }\n\n [button-in-field] { border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;\n position: absolute;\n top: 12px;\n right: 0;\n max-height: 36px;\n }\n\n [input-hint] { font: var(--input-hint-font);\n color: var(--input-hint-color);\n }\n\n @media screen and (max-width: 480px) { [field] { grid-column: span 2;\n }\n }\n `\n ]\n\n @property({ type: Object }) appliance: any\n @property({ type: String }) accessToken?: string\n @property({ type: Object }) _accessTokenInfo: any\n\n @queryAll('[clipboard-copy]') copybuttons\n\n private clipboard?: Clipboard\n\n get context() { return { title: { icon: 'devices',\n text: this.appliance.name\n }\n }\n }\n\n render() { var appliance = this.appliance || {}\n var accessTokenExp = this._accessTokenInfo?.exp\n\n return html`\n <div>\n <h2><md-icon>devices</md-icon> ${appliance.name}</h2>\n <p page-description>${appliance.description}</p>\n </div>\n\n <form>\n <div fieldset-container>\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 id=\"name\" type=\"text\" name=\"name\" .value=${appliance.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=\"brand\">${i18next.t('field.brand')}</label>\n <input id=\"brand\" type=\"text\" name=\"brand\" .value=${appliance.brand} />\n </div>\n\n <div field>\n <label for=\"serial-no\">${i18next.t('field.serial-no')}</label>\n <input id=\"serial-no\" type=\"text\" name=\"serialNo\" .value=${appliance.serialNo} />\n </div>\n\n <div field>\n <label for=\"model\">${i18next.t('field.model')}</label>\n <input id=\"model\" type=\"text\" name=\"model\" .value=${appliance.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 </div>\n\n <div fieldset-container>\n <fieldset>\n <legend>${i18next.t('text.appliance credential')}</legend>\n <div field-2column>\n <div field grid-span>\n <label for=\"access-token\">${i18next.t('label.access token')}</label>\n <input id=\"access-token\" type=\"text\" .value=${appliance.accessToken} readonly />\n <button button-in-field clipboard-copy @click=${e => e.preventDefault()}>\n ${i18next.t('button.copy')}\n </button>\n ${accessTokenExp\n ? html`<div input-hint>\n ${i18next.t('text.token expiry time')} ${new Date(accessTokenExp * 1000).toLocaleString()} :\n ${asyncReplace(this.expTimer(accessTokenExp))}\n </div>`\n : html``}\n </div>\n </div>\n </fieldset>\n </div>\n\n <button @click=${this.deleteAppliance.bind(this)}>${i18next.t('button.delete this appliance')}</button>\n <button @click=${this.generateApplianceSecret.bind(this)}>\n ${i18next.t('button.generate new access token')}\n </button>\n <button @click=${this.updateAppliance.bind(this)}>${i18next.t('button.update')}</button>\n </form>\n `\n }\n\n firstUpdated() { this.clipboard = new Clipboard(this.copybuttons, { target: (trigger => trigger.parentElement.querySelector('input')) as any\n })\n }\n\n updated(changes) { /*\n * If this page properties are changed, this callback will be invoked.\n * This callback will be called back only when this page is activated.\n */\n if (changes.has('appliance')) { const { accessToken } = this.appliance || {}\n this.accessToken = accessToken\n }\n\n if (changes.has('accessToken')) { this._accessTokenInfo = this.accessToken ? parseJwt(this.accessToken) : {}\n }\n }\n\n async pageUpdated(changes, lifecycle, before) { if (this.active) { await this.fetchAppliance()\n }\n }\n\n async fetchAppliance() { const response = await client.query({ query: gql`\n query ($id: String!) { appliance(id: $id) { id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n }\n }\n `,\n variables: { id: this.lifecycle.resourceId\n }\n })\n\n this.appliance = response.data.appliance\n }\n\n async updateAppliance(e) { e.preventDefault()\n\n const form = this.renderRoot.querySelector('form') as HTMLFormElement\n const formData = new FormData(form)\n\n const patch = Array.from(formData.entries()).reduce((patch, [key, value]) => { patch[key] = value\n return patch\n }, {})\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!, $patch: AppliancePatch!) { updateAppliance(id: $id, patch: $patch) { id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n }\n }\n `,\n variables: { id,\n patch\n }\n })\n\n if (response.errors) { console.error('update fail')\n } else { this.appliance = response.data.updateAppliance\n console.log('update sucess')\n }\n }\n\n async deleteAppliance(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { deleteAppliance(id: $id)\n }\n `,\n variables: { id\n }\n })\n\n if (response.errors) { console.error('delete fail')\n } else { navigate('appliance-home')\n }\n }\n\n async generateApplianceSecret(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { generateApplianceSecret(id: $id) { id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n }\n }\n `,\n variables: { id\n }\n })\n\n if (response.errors) { console.error('generate-appliance-secret fail')\n } else { this.appliance = response.data.generateApplianceSecret\n console.log('generate-appliance-secret sucess')\n }\n }\n\n async *expTimer(exp) { const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.active) { var remain = Math.floor(Number(exp) - Date.now() / 1000)\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.js","sourceRoot":"","sources":["../../../client/pages/appliance/appliance.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport Clipboard from 'clipboard'\nimport gql from 'graphql-tag'\nimport { css, html } from 'lit'\nimport { customElement, property, queryAll } from 'lit/decorators.js'\nimport { asyncReplace } from 'lit/directives/async-replace.js'\nimport { client } from '@operato/graphql'\nimport { navigate, PageView } from '@operato/shell'\n\nimport '../../components/secret-field.js'\nimport { parseJwt, sleep } from '@operato/utils'\nimport { i18next } from '@operato/i18n'\n\n@customElement('appliance-page')\nclass Appliance extends PageView { static styles = [\n css`\n :host { display: flex;\n flex-direction: column;\n overflow-y: auto;\n position: relative;\n\n background-color: var(--md-sys-color-background);\n padding: var(--spacing-large);\n }\n\n h2 { margin: var(--title-margin);\n font: var(--title-font);\n color: var(--title-text-color);\n }\n\n [page-description] { margin: var(--page-description-margin);\n font: var(--page-description-font);\n color: var(--page-description-color);\n }\n\n [icon] { position: absolute;\n top: 10px;\n right: 10px;\n max-width: 80px;\n }\n\n [icon] img { max-width: 100%;\n max-height: 100%;\n }\n\n [button-primary] { background-color: var(--button-primary-background-color);\n border: var(--button-border);\n border-radius: var(--button-border-radius);\n margin: var(--button-margin);\n padding: var(--button-primary-padding);\n color: var(--button-primary-color);\n font: var(--button-primary-font);\n text-transform: var(--button-text-transform);\n\n text-decoration: none;\n }\n\n [button-primary]:hover { background-color: var(--button-primary-active-background-color);\n box-shadow: var(--button-active-box-shadow);\n }\n\n [fieldset-container] { background-color: var(--md-sys-color-surface);\n margin: var(--spacing-large) 0 var(--spacing-medium) 0;\n padding: var(--spacing-medium);\n border-radius: var(--border-radius);\n box-shadow: var(--box-shadow);\n\n label { 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 select {\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 flex: 1;\n }\n\n select:focus,\n input:focus,\n button { outline: none;\n }\n\n form { max-width: var(--content-container-max-width);\n }\n }\n\n [fieldset-container] fieldset { margin: 0;\n margin-top: -15px;\n }\n\n fieldset { border-radius: var(--border-radius);\n border: var(--border-dim-color);\n margin: var(--fieldset-margin);\n padding: var(--fieldset-padding);\n }\n\n legend { padding: var(--legend-padding);\n font-weight: bold;\n color: var(--legend-color);\n }\n\n [field-2column] { display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: 15px;\n }\n\n [field] { display: flex;\n flex-direction: column;\n position: relative;\n }\n\n [grid-span] { grid-column: span 2;\n }\n\n button { display: flex;\n align-items: center;\n gap: var(--spacing-small);\n }\n\n button,\n input[type='submit'],\n [button-in-field] { background-color: var(--button-background-color);\n border: var(--button-border);\n border-radius: var(--button-border-radius);\n padding: var(--button-padding);\n color: var(--button-color);\n font: var(--button-font);\n text-transform: var(--button-text-transform);\n\n margin: var(--spacing-medium) 0 var(--spacing-medium) var(--spacing-medium);\n float: right;\n text-decoration: none;\n }\n\n button:hover,\n input[type='submit']:hover { border: var(--button-activ-border);\n box-shadow: var(--button-active-box-shadow);\n }\n\n [button-in-field] { border-radius: 0 var(--button-border-radius) var(--button-border-radius) 0;\n position: absolute;\n top: 12px;\n right: 0;\n max-height: 36px;\n }\n\n [input-hint] { font: var(--input-hint-font);\n color: var(--input-hint-color);\n }\n\n @media screen and (max-width: 480px) { [field] { grid-column: span 2;\n }\n }\n `\n ]\n\n @property({ type: Object }) appliance: any\n @property({ type: String }) accessToken?: string\n @property({ type: Object }) _accessTokenInfo: any\n\n @queryAll('[clipboard-copy]') copybuttons\n\n private clipboard?: Clipboard\n\n get context() { return { title: { icon: 'devices',\n text: this.appliance.name\n }\n }\n }\n\n render() { var appliance = this.appliance || {}\n var accessTokenExp = this._accessTokenInfo?.exp\n\n return html`\n <div>\n <h2><md-icon>devices</md-icon> ${appliance.name}</h2>\n <p page-description>${appliance.description}</p>\n </div>\n\n <form>\n <div fieldset-container>\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 id=\"name\" type=\"text\" name=\"name\" .value=${appliance.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=\"brand\">${i18next.t('field.brand')}</label>\n <input id=\"brand\" type=\"text\" name=\"brand\" .value=${appliance.brand} />\n </div>\n\n <div field>\n <label for=\"serial-no\">${i18next.t('field.serial-no')}</label>\n <input id=\"serial-no\" type=\"text\" name=\"serialNo\" .value=${appliance.serialNo} />\n </div>\n\n <div field>\n <label for=\"model\">${i18next.t('field.model')}</label>\n <input id=\"model\" type=\"text\" name=\"model\" .value=${appliance.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 </div>\n\n <div fieldset-container>\n <fieldset>\n <legend>${i18next.t('text.appliance 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=${appliance.accessToken || ''}></secret-field>\n ${accessTokenExp\n ? html`<div input-hint>\n ${i18next.t('text.token expiry time')} ${new Date(accessTokenExp * 1000).toLocaleString()} :\n ${asyncReplace(this.expTimer(accessTokenExp))}\n </div>`\n : html``}\n </div>\n </div>\n </fieldset>\n </div>\n\n <button @click=${this.deleteAppliance.bind(this)}>${i18next.t('button.delete this appliance')}</button>\n <button @click=${this.generateApplianceSecret.bind(this)}>\n ${i18next.t('button.generate new access token')}\n </button>\n <button @click=${this.updateAppliance.bind(this)}>${i18next.t('button.update')}</button>\n </form>\n `\n }\n\n firstUpdated() { this.clipboard = new Clipboard(this.copybuttons, { target: (trigger => trigger.parentElement.querySelector('input')) as any\n })\n }\n\n updated(changes) { /*\n * If this page properties are changed, this callback will be invoked.\n * This callback will be called back only when this page is activated.\n */\n if (changes.has('appliance')) { const { accessToken } = this.appliance || {}\n this.accessToken = accessToken\n }\n\n if (changes.has('accessToken')) { this._accessTokenInfo = this.accessToken ? parseJwt(this.accessToken) : {}\n }\n }\n\n async pageUpdated(changes, lifecycle, before) { if (this.active) { await this.fetchAppliance()\n }\n }\n\n async fetchAppliance() { const response = await client.query({ query: gql`\n query ($id: String!) { appliance(id: $id) { id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n }\n }\n `,\n variables: { id: this.lifecycle.resourceId\n }\n })\n\n this.appliance = response.data.appliance\n }\n\n async updateAppliance(e) { e.preventDefault()\n\n const form = this.renderRoot.querySelector('form') as HTMLFormElement\n const formData = new FormData(form)\n\n const patch = Array.from(formData.entries()).reduce((patch, [key, value]) => { patch[key] = value\n return patch\n }, {})\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!, $patch: AppliancePatch!) { updateAppliance(id: $id, patch: $patch) { id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n }\n }\n `,\n variables: { id,\n patch\n }\n })\n\n if (response.errors) { console.error('update fail')\n } else { this.appliance = response.data.updateAppliance\n console.log('update sucess')\n }\n }\n\n async deleteAppliance(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { deleteAppliance(id: $id)\n }\n `,\n variables: { id\n }\n })\n\n if (response.errors) { console.error('delete fail')\n } else { navigate('appliance-home')\n }\n }\n\n async generateApplianceSecret(e) { e.preventDefault()\n\n const id = this.lifecycle.resourceId\n\n const response = await client.mutate({ mutation: gql`\n mutation ($id: String!) { generateApplianceSecret(id: $id) { id\n name\n description\n serialNo\n brand\n model\n netmask\n accessToken\n }\n }\n `,\n variables: { id\n }\n })\n\n if (response.errors) { console.error('generate-appliance-secret fail')\n } else { this.appliance = response.data.generateApplianceSecret\n console.log('generate-appliance-secret sucess')\n }\n }\n\n async *expTimer(exp) { const DAY = 24 * 60 * 60\n const HOUR = 60 * 60\n const MIN = 60\n\n while (this.active) { var remain = Math.floor(Number(exp) - Date.now() / 1000)\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,2 +1,61 @@
|
|
|
1
|
-
import '@material/web/
|
|
2
|
-
import '@
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import '@operato/data-grist/ox-grist.js';
|
|
3
|
+
import '@operato/context/ox-context-page-toolbar.js';
|
|
4
|
+
import type { FetchOption } from '@operato/data-grist';
|
|
5
|
+
import { PageView } from '@operato/shell';
|
|
6
|
+
declare const Appliances_base: (new (...args: any[]) => {
|
|
7
|
+
__preferenceProviders: {
|
|
8
|
+
[element: string]: import("@operato/p13n").PagePreferenceProvider;
|
|
9
|
+
};
|
|
10
|
+
getPagePreferenceProvider(element: string): import("@operato/p13n").PagePreferenceProvider | undefined;
|
|
11
|
+
}) & (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
|
|
12
|
+
/**
|
|
13
|
+
* Registered appliances.
|
|
14
|
+
*
|
|
15
|
+
* ── What this screen looked like before ──────────────────────────────────────
|
|
16
|
+
* A hand-written table with no paging, sorting or search, and three columns that were not
|
|
17
|
+
* data: "API health" printed OK, "Installs" printed 1, "status" printed draft, identical on
|
|
18
|
+
* every row. An appliance that had never checked in read exactly like one reporting fine.
|
|
19
|
+
* Those are gone — Appliance declares none of the three — and the columns here are the
|
|
20
|
+
* fields it does declare.
|
|
21
|
+
*
|
|
22
|
+
* ── ⚠ The access token is not asked for, and a test holds that ──────────────
|
|
23
|
+
* `Appliance.accessToken` carries its own
|
|
24
|
+
* `@privilege(category: "security", privilege: "query", domainOwnerGranted: true)`. A list
|
|
25
|
+
* is the wrong place for it regardless of who may read it: a token in a grid cell is a token
|
|
26
|
+
* in plain text beside a name, visible to everyone who can open the screen and to anyone
|
|
27
|
+
* looking over their shoulder. The screens that genuinely need it ask one record at a time.
|
|
28
|
+
*
|
|
29
|
+
* `appliance-list-query.test.ts` fails if the word reappears here.
|
|
30
|
+
*/
|
|
31
|
+
export declare class Appliances extends Appliances_base {
|
|
32
|
+
static styles: import("lit").CSSResult[];
|
|
33
|
+
active: boolean;
|
|
34
|
+
config: any;
|
|
35
|
+
mode: 'CARD' | 'LIST' | 'GRID';
|
|
36
|
+
/** The last fetch did not answer. Distinct from answering with nothing. */
|
|
37
|
+
failed: boolean;
|
|
38
|
+
private grist;
|
|
39
|
+
get context(): {
|
|
40
|
+
title: string;
|
|
41
|
+
help: string;
|
|
42
|
+
search: {
|
|
43
|
+
handler: (search: string) => void;
|
|
44
|
+
value: string;
|
|
45
|
+
};
|
|
46
|
+
actions: {
|
|
47
|
+
title: string;
|
|
48
|
+
action: () => void;
|
|
49
|
+
icon: string;
|
|
50
|
+
}[];
|
|
51
|
+
toolbar: boolean;
|
|
52
|
+
};
|
|
53
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
54
|
+
pageInitialized(): Promise<void>;
|
|
55
|
+
pageUpdated(_changes: any, _lifecycle: any): Promise<void>;
|
|
56
|
+
fetchHandler({ page, limit, sortings, filters }: FetchOption): Promise<{
|
|
57
|
+
total: number;
|
|
58
|
+
records: any[];
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
export {};
|