@things-factory/auth-ui 10.1.194 → 10.1.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/bootstrap.js +4 -1
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/access-decision-popup.js +7 -5
- package/dist-client/components/access-decision-popup.js.map +1 -1
- package/dist-client/components/access-request-popup.d.ts +9 -1
- package/dist-client/components/access-request-popup.js +55 -4
- package/dist-client/components/access-request-popup.js.map +1 -1
- package/dist-client/components/my-login-history.js +14 -10
- package/dist-client/components/my-login-history.js.map +1 -1
- package/dist-client/components/role-task-edit.d.ts +22 -0
- package/dist-client/components/role-task-edit.js +17 -0
- package/dist-client/components/role-task-edit.js.map +1 -0
- package/dist-client/components/role-task-edit.test.d.ts +1 -0
- package/dist-client/components/role-task-edit.test.js +34 -0
- package/dist-client/components/role-task-edit.test.js.map +1 -0
- package/dist-client/components/role-template-picker.d.ts +5 -0
- package/dist-client/components/role-template-picker.js +41 -5
- package/dist-client/components/role-template-picker.js.map +1 -1
- package/dist-client/components/user-status-renderer.d.ts +9 -0
- package/dist-client/components/user-status-renderer.js +36 -0
- package/dist-client/components/user-status-renderer.js.map +1 -0
- package/dist-client/entity360/person-360.d.ts +13 -0
- package/dist-client/entity360/person-360.js +96 -0
- package/dist-client/entity360/person-360.js.map +1 -1
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +14 -1
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/access-request/access-requests.js +4 -2
- package/dist-client/pages/access-request/access-requests.js.map +1 -1
- package/dist-client/pages/access-request/my-access-requests.js +3 -2
- package/dist-client/pages/access-request/my-access-requests.js.map +1 -1
- package/dist-client/pages/audit/audit-page.d.ts +46 -0
- package/dist-client/pages/audit/audit-page.js +417 -0
- package/dist-client/pages/audit/audit-page.js.map +1 -0
- package/dist-client/pages/role/role-privilege-page.d.ts +46 -0
- package/dist-client/pages/role/role-privilege-page.js +500 -5
- package/dist-client/pages/role/role-privilege-page.js.map +1 -1
- package/dist-client/pages/role/role-task-review.test.d.ts +1 -0
- package/dist-client/pages/role/role-task-review.test.js +60 -0
- package/dist-client/pages/role/role-task-review.test.js.map +1 -0
- package/dist-client/pages/user/user-list-page.js +1 -31
- package/dist-client/pages/user/user-list-page.js.map +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/access-request.d.ts +15 -1
- package/dist-client/utils/access-request.js +19 -2
- package/dist-client/utils/access-request.js.map +1 -1
- package/dist-client/utils/access-request.test.js +26 -1
- package/dist-client/utils/access-request.test.js.map +1 -1
- package/dist-client/utils/audit-words.d.ts +30 -0
- package/dist-client/utils/audit-words.js +57 -0
- package/dist-client/utils/audit-words.js.map +1 -0
- package/dist-client/utils/audit-words.test.d.ts +1 -0
- package/dist-client/utils/audit-words.test.js +47 -0
- package/dist-client/utils/audit-words.test.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/things-factory.config.js +1 -0
- package/translations/en.json +78 -1
- package/translations/es.json +78 -1
- package/translations/ja.json +78 -1
- package/translations/ko.json +78 -1
- package/translations/ms.json +78 -1
- package/translations/zh.json +78 -1
|
@@ -198,6 +198,18 @@ let RoleTemplatePicker = class RoleTemplatePicker extends localize(i18next)(LitE
|
|
|
198
198
|
line-height: 1.5;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
[note][attention] {
|
|
202
|
+
color: var(--md-sys-color-error);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* The seat's work, one per line - the words a person reads first; the modules below are the detail. */
|
|
206
|
+
ul[tasks] {
|
|
207
|
+
margin: 0;
|
|
208
|
+
padding: 0 0 0 1.1em;
|
|
209
|
+
font-size: 0.92rem;
|
|
210
|
+
line-height: 1.6;
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
dl[grants] {
|
|
202
214
|
display: grid;
|
|
203
215
|
grid-template-columns: minmax(8em, max-content) 1fr;
|
|
@@ -274,6 +286,8 @@ let RoleTemplatePicker = class RoleTemplatePicker extends localize(i18next)(LitE
|
|
|
274
286
|
missingCount
|
|
275
287
|
defaultNameTaken
|
|
276
288
|
grantsNothingDeliberately
|
|
289
|
+
tasks
|
|
290
|
+
definitionErrors
|
|
277
291
|
grants {
|
|
278
292
|
category
|
|
279
293
|
axes
|
|
@@ -310,9 +324,11 @@ let RoleTemplatePicker = class RoleTemplatePicker extends localize(i18next)(LitE
|
|
|
310
324
|
${template.defaultNameTaken
|
|
311
325
|
? html `<span tag>${i18next.t('text.already in this domain')}</span>`
|
|
312
326
|
: nothing}
|
|
313
|
-
${template.
|
|
314
|
-
? html `<span tag warn>${i18next.t('text.
|
|
315
|
-
:
|
|
327
|
+
${template.definitionErrors.length
|
|
328
|
+
? html `<span tag warn>${i18next.t('text.template cannot be applied')}</span>`
|
|
329
|
+
: template.missingCount
|
|
330
|
+
? html `<span tag warn>${i18next.t('text.x not here', { x: template.missingCount })}</span>`
|
|
331
|
+
: nothing}
|
|
316
332
|
</div>
|
|
317
333
|
<small>${this.summarize(template)}</small>
|
|
318
334
|
</li>
|
|
@@ -389,7 +405,7 @@ let RoleTemplatePicker = class RoleTemplatePicker extends localize(i18next)(LitE
|
|
|
389
405
|
${template.defaultNameTaken && this.name === template.name
|
|
390
406
|
? html `<p note>${i18next.t('text.a role of this name exists and will be added to')}</p>`
|
|
391
407
|
: nothing}
|
|
392
|
-
${this.renderOpens(template)}
|
|
408
|
+
${this.renderTasks(template)} ${this.renderOpens(template)}
|
|
393
409
|
</div>
|
|
394
410
|
`;
|
|
395
411
|
}
|
|
@@ -432,6 +448,22 @@ let RoleTemplatePicker = class RoleTemplatePicker extends localize(i18next)(LitE
|
|
|
432
448
|
: nothing}
|
|
433
449
|
`;
|
|
434
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* The work this seat does, by task (ADR-0139 decision 11) — named in the words of the module that declares each.
|
|
453
|
+
* A template the framework refuses to apply says why here, before anyone presses create.
|
|
454
|
+
*/
|
|
455
|
+
renderTasks(template) {
|
|
456
|
+
return html `
|
|
457
|
+
${template.definitionErrors.length
|
|
458
|
+
? html `<p note attention>${i18next.t('text.template cannot be applied because', { list: template.definitionErrors.join(' · ') })}</p>`
|
|
459
|
+
: nothing}
|
|
460
|
+
${template.tasks.length
|
|
461
|
+
? html `<ul tasks>
|
|
462
|
+
${template.tasks.map(id => html `<li>${taskName(id)}</li>`)}
|
|
463
|
+
</ul>`
|
|
464
|
+
: nothing}
|
|
465
|
+
`;
|
|
466
|
+
}
|
|
435
467
|
pick(template) {
|
|
436
468
|
this.scratch = false;
|
|
437
469
|
this.selected = template;
|
|
@@ -443,7 +475,7 @@ let RoleTemplatePicker = class RoleTemplatePicker extends localize(i18next)(LitE
|
|
|
443
475
|
this.name = '';
|
|
444
476
|
}
|
|
445
477
|
canCreate() {
|
|
446
|
-
return Boolean(this.name.trim()) && (this.scratch || Boolean(this.selected));
|
|
478
|
+
return Boolean(this.name.trim()) && (this.scratch || (Boolean(this.selected) && !this.selected.definitionErrors.length));
|
|
447
479
|
}
|
|
448
480
|
async create() {
|
|
449
481
|
const created = this.scratch
|
|
@@ -482,4 +514,8 @@ RoleTemplatePicker = __decorate([
|
|
|
482
514
|
customElement('role-template-picker')
|
|
483
515
|
], RoleTemplatePicker);
|
|
484
516
|
export { RoleTemplatePicker };
|
|
517
|
+
/** A task's name in the words of the module that declares it; the id when there are none. */
|
|
518
|
+
function taskName(id) {
|
|
519
|
+
return i18next.t(`task.${id}.name`, { defaultValue: id });
|
|
520
|
+
}
|
|
485
521
|
//# sourceMappingURL=role-template-picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-template-picker.js","sourceRoot":"","sources":["../../client/components/role-template-picker.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport type { CSSResultGroup } from 'lit'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\n\nimport { axisLabel } from '../utils/privilege-axis.js'\nimport { CommonHeaderStyles, FormFieldStyles, ScrollbarStyles } from '@operato/styles'\n\ntype GrantView = { category: string; axes: string[]; missingAxes: string[] }\ntype TemplateView = {\n id: string\n name: string\n description?: string\n grants: GrantView[]\n privilegeCount: number\n missingCount: number\n defaultNameTaken: boolean\n grantsNothingDeliberately: boolean\n}\n\n/*\n * The axis vocabulary lives in `utils/privilege-axis.ts` now — one table, not the copy that\n * used to sit here beside an identical one in `role-privilege-page.ts`. It also stopped\n * translating `query`/`mutation` into 「읽기」/「쓰기」; that file says why.\n */\n\n/**\n * The module, in words people use.\n *\n * Same rule as the role privilege page (`moduleLabel` there): use\n * `privilege.category.<category>` when it exists and **fall back to the code word**. Not\n * inventing one is the point — a made-up name would read as product vocabulary and hide\n * from the UX lane that the sentence is still missing.\n */\nfunction moduleLabel(category: string): string {\n return i18next.t(`privilege.category.${category}`, { defaultValue: category })\n}\n\n/** Stable order inside a group, by the name a person actually reads. */\nfunction byName(a: GrantView, b: GrantView): number {\n return moduleLabel(a.category).localeCompare(moduleLabel(b.category))\n}\n\n/**\n * Starting a role.\n *\n * ── Why this stands where the blank row used to ──────────────────────────────\n * A new plant has no roles, and the old answer to \"make one\" was an empty line waiting for\n * a name. Naming it is the easy half; the hard half is which of 84 privileges belong to a\n * quality inspector, and that is a judgement the product has made and the plant has not.\n *\n * miratek is the case: two roles (Administrator, TWIN-CONNECTOR), no MES seats, and eleven\n * approval doors standing empty because there is nothing to route them to. Read from\n * inside, that looks like a plant that has not decided. It is a plant with nothing to\n * decide from.\n *\n * ── Both doors, in one place ─────────────────────────────────────────────────\n * The templates come first and the blank name last, because the order is what teaches: a\n * plant that sees \"quality inspector, 7 privileges\" learns what a seat looks like before\n * it is asked to invent one. Hiding the blank door would be worse, though — the seats a\n * factory actually has (\"night shift lead\", \"outside inspector\") are its own, and the\n * product does not know them.\n *\n * ── Why the name is editable ─────────────────────────────────────────────────\n * The template knows the seat exists; it does not know what the people there call it. And\n * because a role is also what an approval line points at, the same seat is sometimes\n * needed twice — a day and a night inspector hold the same privileges and take different\n * approvals. A fixed name would allow each template exactly once per domain.\n */\n@customElement('role-template-picker')\nexport class RoleTemplatePicker extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n ScrollbarStyles,\n /*\n * Native controls, styled by the shared block.\n *\n * This used `md-filled-text-field` and the Material buttons until the user looked at it\n * (2026-09-11): \"too big, and the style does not fit.\" They are right — a filled field\n * with a floating label is a form control from a different density than the pills,\n * chips and 30px rows this product uses everywhere else.\n *\n * `FormFieldStyles` exists precisely for this: a component draws its input inside its\n * own shadow root, where the theme stylesheet cannot reach, so the token wiring has to\n * be declared per component. It goes first so local rules below can override it.\n */\n FormFieldStyles,\n /* The popup footer already has a convention — plain buttons, pill on pointer devices. */\n CommonHeaderStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-surface-container-lowest);\n color: var(--md-sys-color-on-surface);\n height: 100%;\n overflow: hidden;\n }\n\n [body] {\n display: grid;\n grid-template-columns: minmax(240px, 320px) 1fr;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px);\n flex: 1;\n min-height: 0;\n }\n\n [panel] {\n display: flex;\n flex-direction: column;\n min-height: 0;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-medium, 8px);\n overflow: hidden;\n }\n\n [panel] > header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: var(--spacing-small, 6px) var(--spacing-medium, 8px);\n font-size: var(--fontsize-small, 13px);\n color: var(--md-sys-color-on-surface-variant);\n background-color: var(--md-sys-color-surface-container);\n }\n\n [count] {\n color: var(--md-sys-color-primary);\n font-weight: 600;\n }\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n overflow-y: auto;\n flex: 1;\n }\n\n li[seat] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--spacing-small, 6px) var(--spacing-medium, 8px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n cursor: pointer;\n }\n\n li[seat]:hover {\n background-color: var(--md-sys-color-surface-container-high);\n }\n\n li[seat][selected] {\n background-color: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n\n li[seat] [title-row] {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n li[seat] [name] {\n font-weight: 600;\n }\n\n li[seat] small {\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--fontsize-small, 12px);\n }\n\n li[seat][selected] small {\n color: inherit;\n }\n\n [tag] {\n font-size: var(--fontsize-smaller, 11px);\n padding: 0 6px;\n border-radius: var(--md-sys-shape-corner-full, 999px);\n background-color: var(--md-sys-color-surface-container-highest);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [tag][warn] {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n [detail] {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-large, 14px);\n padding: var(--spacing-large, 14px);\n overflow-y: auto;\n flex: 1;\n }\n\n /* 곁말 — 필드 밑에 붙거나 혼자 선다. 본문과 같은 무게로 두면 읽는 순서가 흐려진다. */\n [note],\n label[field] small {\n margin: 0;\n font-size: var(--fontsize-small, 12px);\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n\n dl[grants] {\n display: grid;\n grid-template-columns: minmax(8em, max-content) 1fr;\n /* 줄 사이가 2px 이었다. 이름이 위아래로 붙어 한 덩어리로 읽혔다. */\n gap: 6px var(--spacing-large, 14px);\n margin: 0;\n align-items: baseline;\n }\n\n /*\n * 모듈 이름이 **기준선**이다 — 굵기로 두 칸이 저절로 갈린다.\n *\n * 처음엔 거꾸로였다: 이름을 흐리게 두고 「쓰기」를 굵게 했더니, 눈이 먼저 닿는 것이\n * 오른쪽 칸이 되어 왼쪽이 뭘 말하는지 훑어지지 않았다. 사람이 이 표에서 찾는 것은\n * 「이 자리가 **어느 모듈**을 여나」이고, 읽기냐 쓰기냐는 그 다음이다.\n */\n dl[grants] dt {\n color: var(--md-sys-color-on-surface);\n font-size: var(--fontsize-small, 13px);\n font-weight: 600;\n }\n\n dl[grants] dd {\n margin: 0;\n display: flex;\n flex-wrap: wrap;\n /* 낱말 사이 가운뎃점 — 「읽기 쓰기」가 한 낱말로 붙어 보이던 것을 가른다. */\n gap: 0 6px;\n }\n\n dl[grants] dd [axis] + [axis]::before {\n content: '·';\n margin-right: 6px;\n color: var(--md-sys-color-outline);\n }\n\n [axis] {\n font-size: var(--fontsize-small, 12px);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [axis][missing] {\n text-decoration: line-through;\n color: var(--md-sys-color-outline);\n }\n\n [empty] {\n margin: auto;\n text-align: center;\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--fontsize-small, 13px);\n padding: var(--spacing-large, 16px);\n }\n\n /* 자리 배치만 더한다 — 모양은 CommonHeaderStyles 의 .footer 가 갖는다. */\n .footer {\n justify-content: flex-end;\n }\n `\n ]\n\n @property({ type: Object }) page?: any\n\n @state() private templates: TemplateView[] = []\n @state() private selected?: TemplateView\n /** `undefined` means \"the blank door\" — name the seat yourself. */\n @state() private scratch = false\n @state() private name = ''\n @state() private description = ''\n\n async connectedCallback() {\n super.connectedCallback()\n await this.fetchTemplates()\n }\n\n private async fetchTemplates() {\n const response = await client.query({\n query: gql`\n query {\n roleTemplates {\n id\n name\n description\n privilegeCount\n missingCount\n defaultNameTaken\n grantsNothingDeliberately\n grants {\n category\n axes\n missingAxes\n }\n }\n }\n `,\n context: gqlContext()\n })\n\n /*\n * A GraphQL error arrives as `data: null` with `errors` set, and `?? []` would draw\n * that as \"this installation offers nothing\" — a refusal made to look like an answer.\n */\n if (response.errors?.length) {\n document.dispatchEvent(\n new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } })\n )\n return\n }\n\n this.templates = response.data?.roleTemplates || []\n }\n\n render() {\n return html`\n <div body>\n <div panel>\n <header>\n ${i18next.t('field.seat')}\n <span count>${this.templates.length}</span>\n </header>\n <ul>\n ${this.templates.map(\n template => html`\n <li seat ?selected=${!this.scratch && this.selected?.id === template.id} @click=${() => this.pick(template)}>\n <div title-row>\n <span name>${template.name}</span>\n ${template.defaultNameTaken\n ? html`<span tag>${i18next.t('text.already in this domain')}</span>`\n : nothing}\n ${template.missingCount\n ? html`<span tag warn>${i18next.t('text.x not here', { x: template.missingCount })}</span>`\n : nothing}\n </div>\n <small>${this.summarize(template)}</small>\n </li>\n `\n )}\n <li seat ?selected=${this.scratch} @click=${() => this.pickScratch()}>\n <div title-row>\n <md-icon>edit</md-icon>\n <span name>${i18next.t('button.name it yourself')}</span>\n </div>\n <small>${i18next.t('text.start from a blank seat')}</small>\n </li>\n </ul>\n </div>\n\n <div panel>${this.renderDetail()}</div>\n </div>\n\n <div class=\"footer\">\n <button @click=${() => this.dispatchEvent(new CustomEvent('close'))}>${i18next.t('button.cancel')}</button>\n <button ok ?disabled=${!this.canCreate()} @click=${() => this.create()}>${i18next.t('button.create')}</button>\n </div>\n `\n }\n\n /**\n * What the seat opens, in one line — **counted, not interpreted.**\n *\n * This briefly said \"changes 6 · only sees 8\", which read well and was not something the\n * data supports: the privilege axis is an unconstrained string and its name does not\n * determine what the resolver behind it does. Counting is safe; grouping by meaning was\n * not. See `renderOpens`.\n *\n * \"0 privileges\" is still two different facts and must not be merged: granting nothing\n * on purpose is an approval seat, and a template whose grants are all missing here is a\n * defect (`missingCount`).\n */\n private summarize(template: TemplateView) {\n if (template.grantsNothingDeliberately) {\n return i18next.t('text.opens nothing, receives approvals')\n }\n\n return i18next.t('text.x modules y privileges', {\n x: template.grants.filter(grant => grant.axes.length).length,\n y: template.privilegeCount\n })\n }\n\n private renderDetail() {\n if (this.scratch) {\n return html`\n <header>${i18next.t('button.name it yourself')}</header>\n <div detail>\n <label field>\n ${i18next.t('field.name')}\n <input .value=${this.name} @input=${(e: any) => (this.name = e.target.value)} />\n </label>\n <label field>\n ${i18next.t('field.description')}\n <input .value=${this.description} @input=${(e: any) => (this.description = e.target.value)} />\n </label>\n <small>${i18next.t('text.a blank seat opens nothing until you give it privileges')}</small>\n </div>\n `\n }\n\n if (!this.selected) {\n return html`<div empty>${i18next.t('text.pick a seat to see what it opens')}</div>`\n }\n\n const template = this.selected\n\n return html`\n <header>${template.name}</header>\n <div detail>\n <label field>\n ${i18next.t('field.name')}\n <input .value=${this.name} @input=${(e: any) => (this.name = e.target.value)} />\n <small>${i18next.t('text.this plant may call the seat what it likes')}</small>\n </label>\n ${template.defaultNameTaken && this.name === template.name\n ? html`<p note>${i18next.t('text.a role of this name exists and will be added to')}</p>`\n : nothing}\n ${this.renderOpens(template)}\n </div>\n `\n }\n\n /**\n * 이 자리가 무엇을 여나 — **선언된 것만.**\n *\n * ⚠ 여기 「바꿀 수 있는 것 / 볼 수만 있는 것」 두 묶음이 있었다. 사용자가 물었다(2026-09-11):\n * 「바꿀 수 있고, 볼 수만 있는 것을 너가 어떻게 알아? 논리적이지 않은 것을 우겨넣지 말고.」\n *\n * 몰랐다. 지어낸 것이다. 근거가 없는 이유가 둘이다.\n *\n * ① `@privilege(privilege:)` 는 **제약 없는 문자열**이다(지시자 선언이 `privilege: String`\n * 이고 enum 이 아니다). 지금 쓰인 값이 mutation 484 · query 314 로 둘뿐인 것은 **오늘\n * 그렇게 쓴 것**이지 둘로 한정된 것이 아니다. 내 코드는 `axis !== 'query'` 로 갈랐으므로,\n * 처음 보는 값은 **알아서가 아니라 query 가 아니라서** 「바꿀 수 있는 것」으로 떨어졌다.\n * ② 이름이 뜻을 보장하지 않는다. 축 이름과 실제 GraphQL 연산이 어긋나는 자리가 6곳이고,\n * 그중 둘은 이 기능의 내 파일이다(`role-template-query.ts` — 템플릿을 **읽는** 질의에\n * `privilege: \"mutation\"` 을 달았다. 역할을 만드는 문이라서 그게 맞다). 축은 연산 종류가\n * 아니라 **어느 문을 지나야 하나**의 이름이다.\n *\n * 그래서 화면은 선언된 것을 그대로 낸다 — 어느 모듈에, 어느 권한이. 읽히게 만드는 것\n * (굵기·줄 간격·가운뎃점)은 아무것도 주장하지 않으므로 그대로 둔다.\n */\n private renderOpens(template: TemplateView) {\n if (template.grantsNothingDeliberately) {\n return html`<p note>${i18next.t('text.opens nothing, receives approvals')}</p>`\n }\n\n return html`\n <dl grants>\n ${[...template.grants].sort(byName).map(\n grant => html`\n <dt>${moduleLabel(grant.category)}</dt>\n <dd>\n ${grant.axes.map(axis => html`<span axis>${axisLabel(axis)}</span>`)}\n ${grant.missingAxes.map(axis => html`<span axis missing>${axisLabel(axis)}</span>`)}\n </dd>\n `\n )}\n </dl>\n ${template.missingCount\n ? html`<p note>${i18next.t('text.struck through are not installed here')}</p>`\n : nothing}\n `\n }\n\n private pick(template: TemplateView) {\n this.scratch = false\n this.selected = template\n this.name = template.name\n }\n\n private pickScratch() {\n this.scratch = true\n this.selected = undefined\n this.name = ''\n }\n\n private canCreate() {\n return Boolean(this.name.trim()) && (this.scratch || Boolean(this.selected))\n }\n\n private async create() {\n const created = this.scratch\n ? await this.page?.createBlankRole?.(this.name.trim(), this.description.trim())\n : await this.page?.createFromTemplate?.(this.selected!.id, this.name.trim())\n\n if (created) {\n this.dispatchEvent(new CustomEvent('close'))\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"role-template-picker.js","sourceRoot":"","sources":["../../client/components/role-template-picker.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport gql from 'graphql-tag'\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport type { CSSResultGroup } from 'lit'\n\nimport { client, gqlContext } from '@operato/graphql'\nimport { i18next, localize } from '@operato/i18n'\n\nimport { axisLabel } from '../utils/privilege-axis.js'\nimport { CommonHeaderStyles, FormFieldStyles, ScrollbarStyles } from '@operato/styles'\n\ntype GrantView = { category: string; axes: string[]; missingAxes: string[] }\ntype TemplateView = {\n id: string\n name: string\n description?: string\n grants: GrantView[]\n privilegeCount: number\n missingCount: number\n defaultNameTaken: boolean\n grantsNothingDeliberately: boolean\n tasks: string[]\n definitionErrors: string[]\n}\n\n/*\n * The axis vocabulary lives in `utils/privilege-axis.ts` now — one table, not the copy that\n * used to sit here beside an identical one in `role-privilege-page.ts`. It also stopped\n * translating `query`/`mutation` into 「읽기」/「쓰기」; that file says why.\n */\n\n/**\n * The module, in words people use.\n *\n * Same rule as the role privilege page (`moduleLabel` there): use\n * `privilege.category.<category>` when it exists and **fall back to the code word**. Not\n * inventing one is the point — a made-up name would read as product vocabulary and hide\n * from the UX lane that the sentence is still missing.\n */\nfunction moduleLabel(category: string): string {\n return i18next.t(`privilege.category.${category}`, { defaultValue: category })\n}\n\n/** Stable order inside a group, by the name a person actually reads. */\nfunction byName(a: GrantView, b: GrantView): number {\n return moduleLabel(a.category).localeCompare(moduleLabel(b.category))\n}\n\n/**\n * Starting a role.\n *\n * ── Why this stands where the blank row used to ──────────────────────────────\n * A new plant has no roles, and the old answer to \"make one\" was an empty line waiting for\n * a name. Naming it is the easy half; the hard half is which of 84 privileges belong to a\n * quality inspector, and that is a judgement the product has made and the plant has not.\n *\n * miratek is the case: two roles (Administrator, TWIN-CONNECTOR), no MES seats, and eleven\n * approval doors standing empty because there is nothing to route them to. Read from\n * inside, that looks like a plant that has not decided. It is a plant with nothing to\n * decide from.\n *\n * ── Both doors, in one place ─────────────────────────────────────────────────\n * The templates come first and the blank name last, because the order is what teaches: a\n * plant that sees \"quality inspector, 7 privileges\" learns what a seat looks like before\n * it is asked to invent one. Hiding the blank door would be worse, though — the seats a\n * factory actually has (\"night shift lead\", \"outside inspector\") are its own, and the\n * product does not know them.\n *\n * ── Why the name is editable ─────────────────────────────────────────────────\n * The template knows the seat exists; it does not know what the people there call it. And\n * because a role is also what an approval line points at, the same seat is sometimes\n * needed twice — a day and a night inspector hold the same privileges and take different\n * approvals. A fixed name would allow each template exactly once per domain.\n */\n@customElement('role-template-picker')\nexport class RoleTemplatePicker extends localize(i18next)(LitElement) {\n static styles: CSSResultGroup = [\n ScrollbarStyles,\n /*\n * Native controls, styled by the shared block.\n *\n * This used `md-filled-text-field` and the Material buttons until the user looked at it\n * (2026-09-11): \"too big, and the style does not fit.\" They are right — a filled field\n * with a floating label is a form control from a different density than the pills,\n * chips and 30px rows this product uses everywhere else.\n *\n * `FormFieldStyles` exists precisely for this: a component draws its input inside its\n * own shadow root, where the theme stylesheet cannot reach, so the token wiring has to\n * be declared per component. It goes first so local rules below can override it.\n */\n FormFieldStyles,\n /* The popup footer already has a convention — plain buttons, pill on pointer devices. */\n CommonHeaderStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n background-color: var(--md-sys-color-surface-container-lowest);\n color: var(--md-sys-color-on-surface);\n height: 100%;\n overflow: hidden;\n }\n\n [body] {\n display: grid;\n grid-template-columns: minmax(240px, 320px) 1fr;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px);\n flex: 1;\n min-height: 0;\n }\n\n [panel] {\n display: flex;\n flex-direction: column;\n min-height: 0;\n background-color: var(--md-sys-color-surface);\n border-radius: var(--md-sys-shape-corner-medium, 8px);\n overflow: hidden;\n }\n\n [panel] > header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: var(--spacing-small, 6px) var(--spacing-medium, 8px);\n font-size: var(--fontsize-small, 13px);\n color: var(--md-sys-color-on-surface-variant);\n background-color: var(--md-sys-color-surface-container);\n }\n\n [count] {\n color: var(--md-sys-color-primary);\n font-weight: 600;\n }\n\n ul {\n list-style: none;\n margin: 0;\n padding: 0;\n overflow-y: auto;\n flex: 1;\n }\n\n li[seat] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--spacing-small, 6px) var(--spacing-medium, 8px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n cursor: pointer;\n }\n\n li[seat]:hover {\n background-color: var(--md-sys-color-surface-container-high);\n }\n\n li[seat][selected] {\n background-color: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n\n li[seat] [title-row] {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n\n li[seat] [name] {\n font-weight: 600;\n }\n\n li[seat] small {\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--fontsize-small, 12px);\n }\n\n li[seat][selected] small {\n color: inherit;\n }\n\n [tag] {\n font-size: var(--fontsize-smaller, 11px);\n padding: 0 6px;\n border-radius: var(--md-sys-shape-corner-full, 999px);\n background-color: var(--md-sys-color-surface-container-highest);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [tag][warn] {\n background-color: var(--md-sys-color-error-container);\n color: var(--md-sys-color-on-error-container);\n }\n\n [detail] {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-large, 14px);\n padding: var(--spacing-large, 14px);\n overflow-y: auto;\n flex: 1;\n }\n\n /* 곁말 — 필드 밑에 붙거나 혼자 선다. 본문과 같은 무게로 두면 읽는 순서가 흐려진다. */\n [note],\n label[field] small {\n margin: 0;\n font-size: var(--fontsize-small, 12px);\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n\n [note][attention] {\n color: var(--md-sys-color-error);\n }\n\n /* The seat's work, one per line - the words a person reads first; the modules below are the detail. */\n ul[tasks] {\n margin: 0;\n padding: 0 0 0 1.1em;\n font-size: 0.92rem;\n line-height: 1.6;\n }\n\n dl[grants] {\n display: grid;\n grid-template-columns: minmax(8em, max-content) 1fr;\n /* 줄 사이가 2px 이었다. 이름이 위아래로 붙어 한 덩어리로 읽혔다. */\n gap: 6px var(--spacing-large, 14px);\n margin: 0;\n align-items: baseline;\n }\n\n /*\n * 모듈 이름이 **기준선**이다 — 굵기로 두 칸이 저절로 갈린다.\n *\n * 처음엔 거꾸로였다: 이름을 흐리게 두고 「쓰기」를 굵게 했더니, 눈이 먼저 닿는 것이\n * 오른쪽 칸이 되어 왼쪽이 뭘 말하는지 훑어지지 않았다. 사람이 이 표에서 찾는 것은\n * 「이 자리가 **어느 모듈**을 여나」이고, 읽기냐 쓰기냐는 그 다음이다.\n */\n dl[grants] dt {\n color: var(--md-sys-color-on-surface);\n font-size: var(--fontsize-small, 13px);\n font-weight: 600;\n }\n\n dl[grants] dd {\n margin: 0;\n display: flex;\n flex-wrap: wrap;\n /* 낱말 사이 가운뎃점 — 「읽기 쓰기」가 한 낱말로 붙어 보이던 것을 가른다. */\n gap: 0 6px;\n }\n\n dl[grants] dd [axis] + [axis]::before {\n content: '·';\n margin-right: 6px;\n color: var(--md-sys-color-outline);\n }\n\n [axis] {\n font-size: var(--fontsize-small, 12px);\n color: var(--md-sys-color-on-surface-variant);\n }\n\n [axis][missing] {\n text-decoration: line-through;\n color: var(--md-sys-color-outline);\n }\n\n [empty] {\n margin: auto;\n text-align: center;\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--fontsize-small, 13px);\n padding: var(--spacing-large, 16px);\n }\n\n /* 자리 배치만 더한다 — 모양은 CommonHeaderStyles 의 .footer 가 갖는다. */\n .footer {\n justify-content: flex-end;\n }\n `\n ]\n\n @property({ type: Object }) page?: any\n\n @state() private templates: TemplateView[] = []\n @state() private selected?: TemplateView\n /** `undefined` means \"the blank door\" — name the seat yourself. */\n @state() private scratch = false\n @state() private name = ''\n @state() private description = ''\n\n async connectedCallback() {\n super.connectedCallback()\n await this.fetchTemplates()\n }\n\n private async fetchTemplates() {\n const response = await client.query({\n query: gql`\n query {\n roleTemplates {\n id\n name\n description\n privilegeCount\n missingCount\n defaultNameTaken\n grantsNothingDeliberately\n tasks\n definitionErrors\n grants {\n category\n axes\n missingAxes\n }\n }\n }\n `,\n context: gqlContext()\n })\n\n /*\n * A GraphQL error arrives as `data: null` with `errors` set, and `?? []` would draw\n * that as \"this installation offers nothing\" — a refusal made to look like an answer.\n */\n if (response.errors?.length) {\n document.dispatchEvent(\n new CustomEvent('notify', { detail: { level: 'error', message: response.errors[0]?.message } })\n )\n return\n }\n\n this.templates = response.data?.roleTemplates || []\n }\n\n render() {\n return html`\n <div body>\n <div panel>\n <header>\n ${i18next.t('field.seat')}\n <span count>${this.templates.length}</span>\n </header>\n <ul>\n ${this.templates.map(\n template => html`\n <li seat ?selected=${!this.scratch && this.selected?.id === template.id} @click=${() => this.pick(template)}>\n <div title-row>\n <span name>${template.name}</span>\n ${template.defaultNameTaken\n ? html`<span tag>${i18next.t('text.already in this domain')}</span>`\n : nothing}\n ${template.definitionErrors.length\n ? html`<span tag warn>${i18next.t('text.template cannot be applied')}</span>`\n : template.missingCount\n ? html`<span tag warn>${i18next.t('text.x not here', { x: template.missingCount })}</span>`\n : nothing}\n </div>\n <small>${this.summarize(template)}</small>\n </li>\n `\n )}\n <li seat ?selected=${this.scratch} @click=${() => this.pickScratch()}>\n <div title-row>\n <md-icon>edit</md-icon>\n <span name>${i18next.t('button.name it yourself')}</span>\n </div>\n <small>${i18next.t('text.start from a blank seat')}</small>\n </li>\n </ul>\n </div>\n\n <div panel>${this.renderDetail()}</div>\n </div>\n\n <div class=\"footer\">\n <button @click=${() => this.dispatchEvent(new CustomEvent('close'))}>${i18next.t('button.cancel')}</button>\n <button ok ?disabled=${!this.canCreate()} @click=${() => this.create()}>${i18next.t('button.create')}</button>\n </div>\n `\n }\n\n /**\n * What the seat opens, in one line — **counted, not interpreted.**\n *\n * This briefly said \"changes 6 · only sees 8\", which read well and was not something the\n * data supports: the privilege axis is an unconstrained string and its name does not\n * determine what the resolver behind it does. Counting is safe; grouping by meaning was\n * not. See `renderOpens`.\n *\n * \"0 privileges\" is still two different facts and must not be merged: granting nothing\n * on purpose is an approval seat, and a template whose grants are all missing here is a\n * defect (`missingCount`).\n */\n private summarize(template: TemplateView) {\n if (template.grantsNothingDeliberately) {\n return i18next.t('text.opens nothing, receives approvals')\n }\n\n return i18next.t('text.x modules y privileges', {\n x: template.grants.filter(grant => grant.axes.length).length,\n y: template.privilegeCount\n })\n }\n\n private renderDetail() {\n if (this.scratch) {\n return html`\n <header>${i18next.t('button.name it yourself')}</header>\n <div detail>\n <label field>\n ${i18next.t('field.name')}\n <input .value=${this.name} @input=${(e: any) => (this.name = e.target.value)} />\n </label>\n <label field>\n ${i18next.t('field.description')}\n <input .value=${this.description} @input=${(e: any) => (this.description = e.target.value)} />\n </label>\n <small>${i18next.t('text.a blank seat opens nothing until you give it privileges')}</small>\n </div>\n `\n }\n\n if (!this.selected) {\n return html`<div empty>${i18next.t('text.pick a seat to see what it opens')}</div>`\n }\n\n const template = this.selected\n\n return html`\n <header>${template.name}</header>\n <div detail>\n <label field>\n ${i18next.t('field.name')}\n <input .value=${this.name} @input=${(e: any) => (this.name = e.target.value)} />\n <small>${i18next.t('text.this plant may call the seat what it likes')}</small>\n </label>\n ${template.defaultNameTaken && this.name === template.name\n ? html`<p note>${i18next.t('text.a role of this name exists and will be added to')}</p>`\n : nothing}\n ${this.renderTasks(template)} ${this.renderOpens(template)}\n </div>\n `\n }\n\n /**\n * 이 자리가 무엇을 여나 — **선언된 것만.**\n *\n * ⚠ 여기 「바꿀 수 있는 것 / 볼 수만 있는 것」 두 묶음이 있었다. 사용자가 물었다(2026-09-11):\n * 「바꿀 수 있고, 볼 수만 있는 것을 너가 어떻게 알아? 논리적이지 않은 것을 우겨넣지 말고.」\n *\n * 몰랐다. 지어낸 것이다. 근거가 없는 이유가 둘이다.\n *\n * ① `@privilege(privilege:)` 는 **제약 없는 문자열**이다(지시자 선언이 `privilege: String`\n * 이고 enum 이 아니다). 지금 쓰인 값이 mutation 484 · query 314 로 둘뿐인 것은 **오늘\n * 그렇게 쓴 것**이지 둘로 한정된 것이 아니다. 내 코드는 `axis !== 'query'` 로 갈랐으므로,\n * 처음 보는 값은 **알아서가 아니라 query 가 아니라서** 「바꿀 수 있는 것」으로 떨어졌다.\n * ② 이름이 뜻을 보장하지 않는다. 축 이름과 실제 GraphQL 연산이 어긋나는 자리가 6곳이고,\n * 그중 둘은 이 기능의 내 파일이다(`role-template-query.ts` — 템플릿을 **읽는** 질의에\n * `privilege: \"mutation\"` 을 달았다. 역할을 만드는 문이라서 그게 맞다). 축은 연산 종류가\n * 아니라 **어느 문을 지나야 하나**의 이름이다.\n *\n * 그래서 화면은 선언된 것을 그대로 낸다 — 어느 모듈에, 어느 권한이. 읽히게 만드는 것\n * (굵기·줄 간격·가운뎃점)은 아무것도 주장하지 않으므로 그대로 둔다.\n */\n private renderOpens(template: TemplateView) {\n if (template.grantsNothingDeliberately) {\n return html`<p note>${i18next.t('text.opens nothing, receives approvals')}</p>`\n }\n\n return html`\n <dl grants>\n ${[...template.grants].sort(byName).map(\n grant => html`\n <dt>${moduleLabel(grant.category)}</dt>\n <dd>\n ${grant.axes.map(axis => html`<span axis>${axisLabel(axis)}</span>`)}\n ${grant.missingAxes.map(axis => html`<span axis missing>${axisLabel(axis)}</span>`)}\n </dd>\n `\n )}\n </dl>\n ${template.missingCount\n ? html`<p note>${i18next.t('text.struck through are not installed here')}</p>`\n : nothing}\n `\n }\n\n /**\n * The work this seat does, by task (ADR-0139 decision 11) — named in the words of the module that declares each.\n * A template the framework refuses to apply says why here, before anyone presses create.\n */\n private renderTasks(template: TemplateView) {\n return html`\n ${template.definitionErrors.length\n ? html`<p note attention>${i18next.t('text.template cannot be applied because', { list: template.definitionErrors.join(' · ') })}</p>`\n : nothing}\n ${template.tasks.length\n ? html`<ul tasks>\n ${template.tasks.map(id => html`<li>${taskName(id)}</li>`)}\n </ul>`\n : nothing}\n `\n }\n\n private pick(template: TemplateView) {\n this.scratch = false\n this.selected = template\n this.name = template.name\n }\n\n private pickScratch() {\n this.scratch = true\n this.selected = undefined\n this.name = ''\n }\n\n private canCreate() {\n return Boolean(this.name.trim()) && (this.scratch || (Boolean(this.selected) && !this.selected!.definitionErrors.length))\n }\n\n private async create() {\n const created = this.scratch\n ? await this.page?.createBlankRole?.(this.name.trim(), this.description.trim())\n : await this.page?.createFromTemplate?.(this.selected!.id, this.name.trim())\n\n if (created) {\n this.dispatchEvent(new CustomEvent('close'))\n }\n }\n}\n\n/** A task's name in the words of the module that declares it; the id when there are none. */\nfunction taskName(id: string): string {\n return i18next.t(`task.${id}.name`, { defaultValue: id })\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import './auth-badge.js';
|
|
2
|
+
/**
|
|
3
|
+
* 잠긴 계정이 **언제 풀리는지**까지 말한다.
|
|
4
|
+
*
|
|
5
|
+
* 판정은 서버(`account-lock-rule.ts`)가 하고 여기서는 남은 시간만 보여 준다 —
|
|
6
|
+
* 화면이 판정을 다시 하면 두 곳이 다른 답을 낸다. `lockedAt` 이 비어 있으면 자동 해제가
|
|
7
|
+
* 없다는 뜻이므로 남은 시간을 말하지 않는다.
|
|
8
|
+
*/
|
|
9
|
+
export declare function statusRenderer(durationMinutes: () => number): (value: string, _column: any, record: any) => import("lit-html").TemplateResult<1>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import './auth-badge.js';
|
|
2
|
+
import { html, nothing } from 'lit';
|
|
3
|
+
import { i18next } from '@operato/i18n';
|
|
4
|
+
/* An account's state as a badge - the user list and the audit screen draw it the same way. */
|
|
5
|
+
/**
|
|
6
|
+
* 잠긴 계정이 **언제 풀리는지**까지 말한다.
|
|
7
|
+
*
|
|
8
|
+
* 판정은 서버(`account-lock-rule.ts`)가 하고 여기서는 남은 시간만 보여 준다 —
|
|
9
|
+
* 화면이 판정을 다시 하면 두 곳이 다른 답을 낸다. `lockedAt` 이 비어 있으면 자동 해제가
|
|
10
|
+
* 없다는 뜻이므로 남은 시간을 말하지 않는다.
|
|
11
|
+
*/
|
|
12
|
+
export function statusRenderer(durationMinutes) {
|
|
13
|
+
return (value, _column, record) => {
|
|
14
|
+
if (value === 'locked') {
|
|
15
|
+
return html `<auth-badge tone="attention"
|
|
16
|
+
>${i18next.t('label.locked-user')}${lockLeft(record, durationMinutes())}</auth-badge
|
|
17
|
+
>`;
|
|
18
|
+
}
|
|
19
|
+
if (value === 'inactive') {
|
|
20
|
+
return html `<auth-badge tone="quiet">${i18next.t('label.inactive-user')}</auth-badge>`;
|
|
21
|
+
}
|
|
22
|
+
/* 활성은 기본값이다 — 색을 쓰지 않는다. 색은 「무엇이 나를 기다리나」에만 쓴다. */
|
|
23
|
+
return html `<auth-badge>${i18next.t('label.activated-user', { defaultValue: value })}</auth-badge>`;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** 잠금이 언제 풀리는지. 판정은 서버가 하고 여기서는 남은 시간만 말한다. */
|
|
27
|
+
function lockLeft(record, minutes) {
|
|
28
|
+
const lockedAt = record?.lockedAt ? new Date(record.lockedAt).getTime() : NaN;
|
|
29
|
+
if (Number.isNaN(lockedAt) || !(minutes > 0)) {
|
|
30
|
+
/* `lockedAt` 이 없으면 자동 해제가 없다는 뜻이다 — 남은 시간을 지어내지 않는다. */
|
|
31
|
+
return nothing;
|
|
32
|
+
}
|
|
33
|
+
const left = Math.ceil((lockedAt + minutes * 60 * 1000 - Date.now()) / 60000);
|
|
34
|
+
return left > 0 ? html ` · ${i18next.t('text.unlocks in x minutes', { x: left })}` : nothing;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=user-status-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-status-renderer.js","sourceRoot":"","sources":["../../client/components/user-status-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,iBAAiB,CAAA;AAExB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AAEnC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,8FAA8F;AAE9F;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,eAA6B;IAC1D,OAAO,CAAC,KAAa,EAAE,OAAY,EAAE,MAAW,EAAE,EAAE;QAClD,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvB,OAAO,IAAI,CAAA;WACN,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC;QACvE,CAAA;QACJ,CAAC;QAED,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;YACzB,OAAO,IAAI,CAAA,4BAA4B,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,eAAe,CAAA;QACxF,CAAC;QAED,mDAAmD;QACnD,OAAO,IAAI,CAAA,eAAe,OAAO,CAAC,CAAC,CAAC,sBAAsB,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,eAAe,CAAA;IACrG,CAAC,CAAA;AACH,CAAC;AAED,+CAA+C;AAC/C,SAAS,QAAQ,CAAC,MAAW,EAAE,OAAe;IAC5C,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,CAAA;IAE7E,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC;QAC7C,wDAAwD;QACxD,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,GAAG,OAAO,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC,CAAA;IAE7E,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,MAAM,OAAO,CAAC,CAAC,CAAC,2BAA2B,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAA;AAC7F,CAAC","sourcesContent":["import './auth-badge.js'\n\nimport { html, nothing } from 'lit'\n\nimport { i18next } from '@operato/i18n'\n\n/* An account's state as a badge - the user list and the audit screen draw it the same way. */\n\n/**\n * 잠긴 계정이 **언제 풀리는지**까지 말한다.\n *\n * 판정은 서버(`account-lock-rule.ts`)가 하고 여기서는 남은 시간만 보여 준다 —\n * 화면이 판정을 다시 하면 두 곳이 다른 답을 낸다. `lockedAt` 이 비어 있으면 자동 해제가\n * 없다는 뜻이므로 남은 시간을 말하지 않는다.\n */\nexport function statusRenderer(durationMinutes: () => number) {\n return (value: string, _column: any, record: any) => {\n if (value === 'locked') {\n return html`<auth-badge tone=\"attention\"\n >${i18next.t('label.locked-user')}${lockLeft(record, durationMinutes())}</auth-badge\n >`\n }\n\n if (value === 'inactive') {\n return html`<auth-badge tone=\"quiet\">${i18next.t('label.inactive-user')}</auth-badge>`\n }\n\n /* 활성은 기본값이다 — 색을 쓰지 않는다. 색은 「무엇이 나를 기다리나」에만 쓴다. */\n return html`<auth-badge>${i18next.t('label.activated-user', { defaultValue: value })}</auth-badge>`\n }\n}\n\n/** 잠금이 언제 풀리는지. 판정은 서버가 하고 여기서는 남은 시간만 말한다. */\nfunction lockLeft(record: any, minutes: number) {\n const lockedAt = record?.lockedAt ? new Date(record.lockedAt).getTime() : NaN\n\n if (Number.isNaN(lockedAt) || !(minutes > 0)) {\n /* `lockedAt` 이 없으면 자동 해제가 없다는 뜻이다 — 남은 시간을 지어내지 않는다. */\n return nothing\n }\n\n const left = Math.ceil((lockedAt + minutes * 60 * 1000 - Date.now()) / 60000)\n\n return left > 0 ? html` · ${i18next.t('text.unlocks in x minutes', { x: left })}` : nothing\n}\n"]}
|
|
@@ -9,6 +9,19 @@ import { type Entity360DoorAnswer, type Entity360Subject } from '@operato/layout
|
|
|
9
9
|
* only (and its parent's) - `User.roles` answers every domain's and must not be used here.
|
|
10
10
|
*/
|
|
11
11
|
export declare function registerPersonKind(): void;
|
|
12
|
+
/** The person's last requests in this domain - for those who hold `audit:query` (ADR-0141 §6.1). */
|
|
13
|
+
export declare function registerPersonAuditSection(): void;
|
|
14
|
+
export declare class PersonAuditSection extends LitElement {
|
|
15
|
+
static styles: import("lit").CSSResult;
|
|
16
|
+
subject?: Entity360Subject;
|
|
17
|
+
answer?: Entity360DoorAnswer<{
|
|
18
|
+
at: string;
|
|
19
|
+
what: string;
|
|
20
|
+
}[]>;
|
|
21
|
+
updated(changed: Map<string, unknown>): void;
|
|
22
|
+
private read;
|
|
23
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
24
|
+
}
|
|
12
25
|
export declare class PersonRolesSection extends LitElement {
|
|
13
26
|
static styles: import("lit").CSSResult;
|
|
14
27
|
subject?: Entity360Subject;
|
|
@@ -4,6 +4,7 @@ import { css, html, LitElement } from 'lit';
|
|
|
4
4
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
5
|
import { client } from '@operato/graphql';
|
|
6
6
|
import { i18next } from '@operato/i18n';
|
|
7
|
+
import { lastDays, recordWords } from '../utils/audit-words.js';
|
|
7
8
|
import { readEntity360Door, registerEntity360Kind, registerEntity360Section } from '@operato/layout';
|
|
8
9
|
/**
|
|
9
10
|
* The person 360 (ADR-0067, design/ui/entity360-person.md): a `User` value anywhere - a grid's
|
|
@@ -26,6 +27,101 @@ export function registerPersonKind() {
|
|
|
26
27
|
render: subject => html `<person-roles-section .subject=${subject}></person-roles-section>`
|
|
27
28
|
});
|
|
28
29
|
}
|
|
30
|
+
/** The person's last requests in this domain - for those who hold `audit:query` (ADR-0141 §6.1). */
|
|
31
|
+
export function registerPersonAuditSection() {
|
|
32
|
+
registerEntity360Section('person', {
|
|
33
|
+
name: 'audit',
|
|
34
|
+
order: 40,
|
|
35
|
+
render: subject => html `<person-audit-section .subject=${subject}></person-audit-section>`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const AUDIT_SHOWN = 10;
|
|
39
|
+
let PersonAuditSection = class PersonAuditSection extends LitElement {
|
|
40
|
+
static { this.styles = css `
|
|
41
|
+
ul {
|
|
42
|
+
margin: 0;
|
|
43
|
+
padding: 0;
|
|
44
|
+
list-style: none;
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: max-content 1fr;
|
|
47
|
+
gap: 4px 12px;
|
|
48
|
+
font: 13px/1.5 sans-serif;
|
|
49
|
+
color: var(--md-sys-color-on-surface);
|
|
50
|
+
}
|
|
51
|
+
li {
|
|
52
|
+
display: contents;
|
|
53
|
+
}
|
|
54
|
+
time {
|
|
55
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
56
|
+
font-variant-numeric: tabular-nums;
|
|
57
|
+
}
|
|
58
|
+
`; }
|
|
59
|
+
updated(changed) {
|
|
60
|
+
if (changed.has('subject'))
|
|
61
|
+
this.read();
|
|
62
|
+
}
|
|
63
|
+
async read() {
|
|
64
|
+
const id = this.subject?.id;
|
|
65
|
+
this.answer = undefined;
|
|
66
|
+
if (!id)
|
|
67
|
+
return;
|
|
68
|
+
const { from, to } = lastDays(30);
|
|
69
|
+
const response = await client.query({
|
|
70
|
+
query: gql `
|
|
71
|
+
query ($filter: AuditRecordFilter!) {
|
|
72
|
+
auditRecords(filter: $filter) {
|
|
73
|
+
items {
|
|
74
|
+
request {
|
|
75
|
+
createdAt
|
|
76
|
+
}
|
|
77
|
+
actions {
|
|
78
|
+
kind
|
|
79
|
+
door
|
|
80
|
+
decision
|
|
81
|
+
outcome
|
|
82
|
+
code
|
|
83
|
+
count
|
|
84
|
+
detail
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
`,
|
|
90
|
+
variables: { filter: { from: from.toISOString(), to: to.toISOString(), actorId: id, limit: AUDIT_SHOWN } },
|
|
91
|
+
fetchPolicy: 'no-cache'
|
|
92
|
+
});
|
|
93
|
+
if (this.subject?.id !== id)
|
|
94
|
+
return;
|
|
95
|
+
const t = i18next.t.bind(i18next);
|
|
96
|
+
this.answer = readEntity360Door(response, data => (data?.auditRecords?.items || [])
|
|
97
|
+
.map((item) => ({ at: item.request.createdAt, what: recordWords(t, item.actions) }))
|
|
98
|
+
.filter((line) => line.what));
|
|
99
|
+
}
|
|
100
|
+
render() {
|
|
101
|
+
const answer = this.answer;
|
|
102
|
+
return html `<ox-entity360-section
|
|
103
|
+
.heading=${i18next.t('audit.title')}
|
|
104
|
+
.state=${answer?.state ?? 'loading'}
|
|
105
|
+
.message=${answer?.message ?? ''}
|
|
106
|
+
>
|
|
107
|
+
<ul>
|
|
108
|
+
${(answer?.data || []).map(line => html `<li><time>${new Date(line.at).toLocaleString()}</time><span>${line.what}</span></li>`)}
|
|
109
|
+
</ul>
|
|
110
|
+
</ox-entity360-section>`;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
__decorate([
|
|
114
|
+
property({ attribute: false }),
|
|
115
|
+
__metadata("design:type", Object)
|
|
116
|
+
], PersonAuditSection.prototype, "subject", void 0);
|
|
117
|
+
__decorate([
|
|
118
|
+
state(),
|
|
119
|
+
__metadata("design:type", Object)
|
|
120
|
+
], PersonAuditSection.prototype, "answer", void 0);
|
|
121
|
+
PersonAuditSection = __decorate([
|
|
122
|
+
customElement('person-audit-section')
|
|
123
|
+
], PersonAuditSection);
|
|
124
|
+
export { PersonAuditSection };
|
|
29
125
|
let PersonRolesSection = class PersonRolesSection extends LitElement {
|
|
30
126
|
static { this.styles = css `
|
|
31
127
|
ul {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"person-360.js","sourceRoot":"","sources":["../../client/entity360/person-360.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport {\n readEntity360Door,\n registerEntity360Kind,\n registerEntity360Section,\n type Entity360DoorAnswer,\n type Entity360Subject\n} from '@operato/layout'\n\n/**\n * The person 360 (ADR-0067, design/ui/entity360-person.md): a `User` value anywhere - a grid's\n * `updater`, a request's author - opens it. auth-ui declares the kind because the account is its\n * fact; organization and worklist hang their own sections on it with their own doors.\n *\n * This section is identity and the roles held on this site. `userRoles` answers this domain's roles\n * only (and its parent's) - `User.roles` answers every domain's and must not be used here.\n */\n\nexport function registerPersonKind() {\n registerEntity360Kind({\n kind: 'person',\n typenames: ['User'],\n title: ({ value }) => value.name || value.email || '',\n context: ({ value }) => (value.name && value.email ? value.email : '')\n })\n\n registerEntity360Section('person', {\n name: 'roles',\n order: 30,\n render: subject => html`<person-roles-section .subject=${subject}></person-roles-section>`\n })\n}\n\n@customElement('person-roles-section')\nexport class PersonRolesSection extends LitElement {\n static styles = css`\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n li {\n padding: 2px 8px;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n font: 13px/1.5 sans-serif;\n }\n `\n\n @property({ attribute: false }) subject?: Entity360Subject\n @state() answer?: Entity360DoorAnswer<{ id: string; name: string }[]>\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('subject')) this.read()\n }\n\n private async read() {\n const id = this.subject?.id\n this.answer = undefined\n if (!id) return\n\n const response = await client.query({\n query: gql`\n query ($userId: String!) {\n userRoles(userId: $userId) {\n id\n name\n assigned\n }\n }\n `,\n variables: { userId: id }\n })\n\n /* A later subject may have arrived while this one was being read. */\n if (this.subject?.id !== id) return\n this.answer = readEntity360Door(response, data => (data?.userRoles || []).filter((role: any) => role.assigned))\n }\n\n render() {\n const answer = this.answer\n return html`<ox-entity360-section\n .heading=${i18next.t('field.roles', { defaultValue: 'roles' })}\n .state=${answer?.state ?? 'loading'}\n .message=${answer?.message ?? ''}\n >\n <ul>\n ${(answer?.data || []).map(role => html`<li>${role.name}</li>`)}\n </ul>\n </ox-entity360-section>`\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"person-360.js","sourceRoot":"","sources":["../../client/entity360/person-360.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\n\nimport { lastDays, recordWords } from '../utils/audit-words.js'\nimport {\n readEntity360Door,\n registerEntity360Kind,\n registerEntity360Section,\n type Entity360DoorAnswer,\n type Entity360Subject\n} from '@operato/layout'\n\n/**\n * The person 360 (ADR-0067, design/ui/entity360-person.md): a `User` value anywhere - a grid's\n * `updater`, a request's author - opens it. auth-ui declares the kind because the account is its\n * fact; organization and worklist hang their own sections on it with their own doors.\n *\n * This section is identity and the roles held on this site. `userRoles` answers this domain's roles\n * only (and its parent's) - `User.roles` answers every domain's and must not be used here.\n */\n\nexport function registerPersonKind() {\n registerEntity360Kind({\n kind: 'person',\n typenames: ['User'],\n title: ({ value }) => value.name || value.email || '',\n context: ({ value }) => (value.name && value.email ? value.email : '')\n })\n\n registerEntity360Section('person', {\n name: 'roles',\n order: 30,\n render: subject => html`<person-roles-section .subject=${subject}></person-roles-section>`\n })\n}\n\n/** The person's last requests in this domain - for those who hold `audit:query` (ADR-0141 §6.1). */\nexport function registerPersonAuditSection() {\n registerEntity360Section('person', {\n name: 'audit',\n order: 40,\n render: subject => html`<person-audit-section .subject=${subject}></person-audit-section>`\n })\n}\n\nconst AUDIT_SHOWN = 10\n\n@customElement('person-audit-section')\nexport class PersonAuditSection extends LitElement {\n static styles = css`\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 4px 12px;\n font: 13px/1.5 sans-serif;\n color: var(--md-sys-color-on-surface);\n }\n li {\n display: contents;\n }\n time {\n color: var(--md-sys-color-on-surface-variant);\n font-variant-numeric: tabular-nums;\n }\n `\n\n @property({ attribute: false }) subject?: Entity360Subject\n @state() answer?: Entity360DoorAnswer<{ at: string; what: string }[]>\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('subject')) this.read()\n }\n\n private async read() {\n const id = this.subject?.id\n this.answer = undefined\n if (!id) return\n\n const { from, to } = lastDays(30)\n const response = await client.query({\n query: gql`\n query ($filter: AuditRecordFilter!) {\n auditRecords(filter: $filter) {\n items {\n request {\n createdAt\n }\n actions {\n kind\n door\n decision\n outcome\n code\n count\n detail\n }\n }\n }\n }\n `,\n variables: { filter: { from: from.toISOString(), to: to.toISOString(), actorId: id, limit: AUDIT_SHOWN } },\n fetchPolicy: 'no-cache'\n })\n\n if (this.subject?.id !== id) return\n const t = i18next.t.bind(i18next)\n this.answer = readEntity360Door(response, data =>\n (data?.auditRecords?.items || [])\n .map((item: any) => ({ at: item.request.createdAt, what: recordWords(t, item.actions) }))\n .filter((line: any) => line.what)\n )\n }\n\n render() {\n const answer = this.answer\n return html`<ox-entity360-section\n .heading=${i18next.t('audit.title')}\n .state=${answer?.state ?? 'loading'}\n .message=${answer?.message ?? ''}\n >\n <ul>\n ${(answer?.data || []).map(line => html`<li><time>${new Date(line.at).toLocaleString()}</time><span>${line.what}</span></li>`)}\n </ul>\n </ox-entity360-section>`\n }\n}\n\n@customElement('person-roles-section')\nexport class PersonRolesSection extends LitElement {\n static styles = css`\n ul {\n margin: 0;\n padding: 0;\n list-style: none;\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n li {\n padding: 2px 8px;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n font: 13px/1.5 sans-serif;\n }\n `\n\n @property({ attribute: false }) subject?: Entity360Subject\n @state() answer?: Entity360DoorAnswer<{ id: string; name: string }[]>\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('subject')) this.read()\n }\n\n private async read() {\n const id = this.subject?.id\n this.answer = undefined\n if (!id) return\n\n const response = await client.query({\n query: gql`\n query ($userId: String!) {\n userRoles(userId: $userId) {\n id\n name\n assigned\n }\n }\n `,\n variables: { userId: id }\n })\n\n /* A later subject may have arrived while this one was being read. */\n if (this.subject?.id !== id) return\n this.answer = readEntity360Door(response, data => (data?.userRoles || []).filter((role: any) => role.assigned))\n }\n\n render() {\n const answer = this.answer\n return html`<ox-entity360-section\n .heading=${i18next.t('field.roles', { defaultValue: 'roles' })}\n .state=${answer?.state ?? 'loading'}\n .message=${answer?.message ?? ''}\n >\n <ul>\n ${(answer?.data || []).map(role => html`<li>${role.name}</li>`)}\n </ul>\n </ox-entity360-section>`\n }\n}\n"]}
|
package/dist-client/index.d.ts
CHANGED
package/dist-client/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export * from './components/role-selector.js';
|
|
|
8
8
|
export * from './components/domain-switch.js';
|
|
9
9
|
export * from './components/invite-user.js';
|
|
10
10
|
export async function setAuthManagementMenus(options) {
|
|
11
|
-
const { role = true, appliance = true, application = true, user = true, authProvider = true, domain = true, attribute = true } = options || {};
|
|
11
|
+
const { role = true, appliance = true, application = true, user = true, authProvider = true, domain = true, attribute = true, audit = true } = options || {};
|
|
12
12
|
if (await hasPrivilege({ privilege: 'mutation', category: 'user', domainOwnerGranted: true, superUserGranted: true })) {
|
|
13
13
|
role &&
|
|
14
14
|
store.dispatch({
|
|
@@ -66,6 +66,19 @@ export async function setAuthManagementMenus(options) {
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
/* The audit asks its own privilege - the domain owner does not pass on ownership (ADR-0141 §5). */
|
|
70
|
+
if (audit && (await hasPrivilege({ privilege: 'query', category: 'audit', superUserGranted: true }))) {
|
|
71
|
+
store.dispatch({
|
|
72
|
+
type: ADD_MORENDA,
|
|
73
|
+
morenda: {
|
|
74
|
+
icon: html ` <md-icon>policy</md-icon> `,
|
|
75
|
+
name: html ` <ox-i18n msgid="audit.title"></ox-i18n> `,
|
|
76
|
+
action: () => {
|
|
77
|
+
navigate('audit');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
69
82
|
if (await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true })) {
|
|
70
83
|
domain &&
|
|
71
84
|
store.dispatch({
|
package/dist-client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA,OAAO,4BAA4B,CAAA;AACnC,OAAO,0BAA0B,CAAA;AAEjC,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAE/B,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AAEpE,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/i18n/ox-i18n.js'\n\nimport { html } from 'lit-html'\n\nimport { navigate, store } from '@operato/shell'\nimport { ADD_MORENDA } from '@things-factory/more-base/client'\nimport { hasPrivilege } from '@things-factory/auth-base/dist-client'\n\nexport * from './components/role-selector.js'\nexport * from './components/domain-switch.js'\nexport * from './components/invite-user.js'\n\nexport type AuthManagementMenuOptions = {\n role?: boolean\n appliance?: boolean\n application?: boolean\n user?: boolean\n authProvider?: boolean\n domain?: boolean\n attribute?: boolean\n audit?: boolean\n}\n\nexport async function setAuthManagementMenus(options?: any) {\n const {\n role = true,\n appliance = true,\n application = true,\n user = true,\n authProvider = true,\n domain = true,\n attribute = true,\n audit = true\n } = options || {}\n\n if (\n await hasPrivilege({ privilege: 'mutation', category: 'user', domainOwnerGranted: true, superUserGranted: true })\n ) {\n role &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>how_to_reg</md-icon> `,\n name: html` <ox-i18n msgid=\"text.role_management\"></ox-i18n> `,\n action: () => {\n navigate('roles')\n }\n }\n })\n\n appliance &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>devices</md-icon> `,\n name: html` <ox-i18n msgid=\"text.appliance\"></ox-i18n> `,\n action: () => {\n navigate('appliance-home')\n }\n }\n })\n\n application &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>apps</md-icon> `,\n name: html` <ox-i18n msgid=\"text.application management\"></ox-i18n> `,\n action: () => {\n navigate('applications')\n }\n }\n })\n\n user &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>people</md-icon> `,\n name: html` <ox-i18n msgid=\"text.user management\"></ox-i18n> `,\n action: () => {\n navigate('users')\n }\n }\n })\n\n authProvider &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>id_card</md-icon> `,\n name: html` <ox-i18n msgid=\"text.auth-provider management\"></ox-i18n> `,\n action: () => {\n navigate('auth-providers')\n }\n }\n })\n }\n\n /* The audit asks its own privilege - the domain owner does not pass on ownership (ADR-0141 §5). */\n if (audit && (await hasPrivilege({ privilege: 'query', category: 'audit', superUserGranted: true }))) {\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>policy</md-icon> `,\n name: html` <ox-i18n msgid=\"audit.title\"></ox-i18n> `,\n action: () => {\n navigate('audit')\n }\n }\n })\n }\n\n if (await hasPrivilege({ domainOwnerGranted: true, superUserGranted: true })) {\n domain &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>supervised_user_circle</md-icon> `,\n name: html` <ox-i18n msgid=\"text.domain owners\"></ox-i18n> `,\n action: () => {\n navigate('domain-owners')\n }\n }\n })\n }\n\n if (await hasPrivilege({ superUserGranted: true })) {\n domain &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>business</md-icon> `,\n name: html` <ox-i18n msgid=\"text.domain management\"></ox-i18n> `,\n action: () => {\n navigate('domains')\n }\n }\n })\n\n attribute &&\n store.dispatch({\n type: ADD_MORENDA,\n morenda: {\n icon: html` <md-icon>dataset</md-icon> `,\n name: html` <ox-i18n msgid=\"text.attribute management\"></ox-i18n> `,\n action: () => {\n navigate('attributes')\n }\n }\n })\n }\n}\n"]}
|
|
@@ -12,7 +12,7 @@ import { navigate, PageView } from '@operato/shell';
|
|
|
12
12
|
import { CommonHeaderStyles, CommonGristStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
|
|
13
13
|
import { isMobileDevice } from '@operato/utils';
|
|
14
14
|
import { p13n } from '@operato/p13n';
|
|
15
|
-
import {
|
|
15
|
+
import { neededWords } from '../../utils/access-request.js';
|
|
16
16
|
import { listOutcome, pageFromResponse } from '../../utils/list-result.js';
|
|
17
17
|
/**
|
|
18
18
|
* Who asked for what in this domain, and what became of it (ADR-0103).
|
|
@@ -118,6 +118,7 @@ let AccessRequestsPage = class AccessRequestsPage extends p13n(localize(i18next)
|
|
|
118
118
|
id
|
|
119
119
|
category
|
|
120
120
|
privilege
|
|
121
|
+
taskId
|
|
121
122
|
reason
|
|
122
123
|
state
|
|
123
124
|
requester {
|
|
@@ -146,6 +147,7 @@ let AccessRequestsPage = class AccessRequestsPage extends p13n(localize(i18next)
|
|
|
146
147
|
id
|
|
147
148
|
category
|
|
148
149
|
privilege
|
|
150
|
+
taskId
|
|
149
151
|
operationName
|
|
150
152
|
reason
|
|
151
153
|
count
|
|
@@ -186,7 +188,7 @@ let AccessRequestsPage = class AccessRequestsPage extends p13n(localize(i18next)
|
|
|
186
188
|
/* A pending row past its time is expired - the same reading the review makes. */
|
|
187
189
|
state: item.state === 'PENDING' && item.expiresAt && new Date(item.expiresAt).getTime() <= Date.now() ? 'EXPIRED' : item.state,
|
|
188
190
|
requesterName: item.requester?.name || item.requester?.email || '',
|
|
189
|
-
needed:
|
|
191
|
+
needed: neededWords(t, item),
|
|
190
192
|
deciderName: item.decidedBy?.name || '',
|
|
191
193
|
grantedRoleName: item.grantedRole?.name || ''
|
|
192
194
|
})).map((record) => ({ ...record, stateWords: t(`access-request.state.${record.state.toLowerCase()}`) }))
|