@idp.global/catalog 1.1.1
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/assets/v2/draw-41d55381-b162-433d-956c-fe4cd400212e.png +0 -0
- package/assets/v2/draw-bd386bf3-ce94-4452-88da-05ae372fae35.png +0 -0
- package/assets/v2/draw-be69231a-f491-4bd3-9a47-71561ce14c15.png +0 -0
- package/assets/v2/draw-d58859a8-0615-44ec-80fe-626033c888f7.png +0 -0
- package/assets/v2/draw-db67b502-d987-4226-a5f8-d57e3f8d7ba9.png +0 -0
- package/assets/v2/draw-e6ac3ef5-5a93-464d-9d3f-5ae7b7278bdd.png +0 -0
- package/changelog.md +26 -0
- package/dist_bundle/bundle.js +6385 -0
- package/dist_ts_web/00_commitinfo_data.d.ts +8 -0
- package/dist_ts_web/00_commitinfo_data.js +9 -0
- package/dist_ts_web/elements/idp-admin-shell.d.ts +292 -0
- package/dist_ts_web/elements/idp-admin-shell.js +2615 -0
- package/dist_ts_web/elements/idp-approval-card.d.ts +25 -0
- package/dist_ts_web/elements/idp-approval-card.js +274 -0
- package/dist_ts_web/elements/idp-badge.d.ts +14 -0
- package/dist_ts_web/elements/idp-badge.js +135 -0
- package/dist_ts_web/elements/idp-button.d.ts +21 -0
- package/dist_ts_web/elements/idp-button.js +234 -0
- package/dist_ts_web/elements/idp-card.d.ts +15 -0
- package/dist_ts_web/elements/idp-card.js +139 -0
- package/dist_ts_web/elements/idp-checkbox.d.ts +23 -0
- package/dist_ts_web/elements/idp-checkbox.js +233 -0
- package/dist_ts_web/elements/idp-dashboard-window.d.ts +23 -0
- package/dist_ts_web/elements/idp-dashboard-window.js +587 -0
- package/dist_ts_web/elements/idp-data-table.d.ts +45 -0
- package/dist_ts_web/elements/idp-data-table.js +472 -0
- package/dist_ts_web/elements/idp-form-submit.d.ts +20 -0
- package/dist_ts_web/elements/idp-form-submit.js +136 -0
- package/dist_ts_web/elements/idp-form.d.ts +31 -0
- package/dist_ts_web/elements/idp-form.js +197 -0
- package/dist_ts_web/elements/idp-icon.d.ts +18 -0
- package/dist_ts_web/elements/idp-icon.js +204 -0
- package/dist_ts_web/elements/idp-inbox-preview.d.ts +18 -0
- package/dist_ts_web/elements/idp-inbox-preview.js +245 -0
- package/dist_ts_web/elements/idp-input.d.ts +26 -0
- package/dist_ts_web/elements/idp-input.js +249 -0
- package/dist_ts_web/elements/idp-landing-hero.d.ts +15 -0
- package/dist_ts_web/elements/idp-landing-hero.js +209 -0
- package/dist_ts_web/elements/idp-landing-page.d.ts +21 -0
- package/dist_ts_web/elements/idp-landing-page.js +694 -0
- package/dist_ts_web/elements/idp-mobile-frame.d.ts +14 -0
- package/dist_ts_web/elements/idp-mobile-frame.js +195 -0
- package/dist_ts_web/elements/idp-mobile-showcase.d.ts +16 -0
- package/dist_ts_web/elements/idp-mobile-showcase.js +339 -0
- package/dist_ts_web/elements/idp-select.d.ts +28 -0
- package/dist_ts_web/elements/idp-select.js +179 -0
- package/dist_ts_web/elements/idp-toggle.d.ts +15 -0
- package/dist_ts_web/elements/idp-toggle.js +134 -0
- package/dist_ts_web/elements/index.d.ts +20 -0
- package/dist_ts_web/elements/index.js +21 -0
- package/dist_ts_web/elements/tokens.d.ts +21 -0
- package/dist_ts_web/elements/tokens.js +190 -0
- package/dist_ts_web/index.d.ts +1 -0
- package/dist_ts_web/index.js +2 -0
- package/dist_ts_web/pages/index.d.ts +5 -0
- package/dist_ts_web/pages/index.js +23 -0
- package/dist_watch/bundle.js +120699 -0
- package/dist_watch/bundle.js.map +7 -0
- package/dist_watch/index.html +29 -0
- package/html/index.html +29 -0
- package/html/index.ts +39 -0
- package/license +21 -0
- package/package.json +64 -0
- package/readme.md +103 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/elements/idp-admin-shell.ts +2664 -0
- package/ts_web/elements/idp-approval-card.ts +182 -0
- package/ts_web/elements/idp-badge.ts +84 -0
- package/ts_web/elements/idp-button.ts +166 -0
- package/ts_web/elements/idp-card.ts +76 -0
- package/ts_web/elements/idp-checkbox.ts +143 -0
- package/ts_web/elements/idp-dashboard-window.ts +564 -0
- package/ts_web/elements/idp-data-table.ts +407 -0
- package/ts_web/elements/idp-form-submit.ts +65 -0
- package/ts_web/elements/idp-form.ts +159 -0
- package/ts_web/elements/idp-icon.ts +263 -0
- package/ts_web/elements/idp-inbox-preview.ts +192 -0
- package/ts_web/elements/idp-input.ts +151 -0
- package/ts_web/elements/idp-landing-hero.ts +165 -0
- package/ts_web/elements/idp-landing-page.ts +655 -0
- package/ts_web/elements/idp-mobile-frame.ts +137 -0
- package/ts_web/elements/idp-mobile-showcase.ts +295 -0
- package/ts_web/elements/idp-select.ts +119 -0
- package/ts_web/elements/idp-toggle.ts +77 -0
- package/ts_web/elements/index.ts +20 -0
- package/ts_web/elements/tokens.ts +193 -0
- package/ts_web/index.ts +1 -0
- package/ts_web/pages/index.ts +26 -0
|
@@ -0,0 +1,2664 @@
|
|
|
1
|
+
import { DeesElement, html, property, state, customElement, css, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
+
import { idpElementStyles } from './tokens.js';
|
|
3
|
+
import './idp-badge.js';
|
|
4
|
+
import './idp-icon.js';
|
|
5
|
+
import './idp-data-table.js';
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
interface HTMLElementTagNameMap {
|
|
9
|
+
'idp-admin-shell': IdpAdminShell;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type TNavItem = {
|
|
14
|
+
id: TIdpAdminPage;
|
|
15
|
+
label: string;
|
|
16
|
+
icon: string;
|
|
17
|
+
badge?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type TKpi = {
|
|
21
|
+
label: string;
|
|
22
|
+
value: string;
|
|
23
|
+
unit?: string;
|
|
24
|
+
delta: string;
|
|
25
|
+
deltaKind: 'up' | 'live';
|
|
26
|
+
sub: string;
|
|
27
|
+
accent: string;
|
|
28
|
+
spark: number[];
|
|
29
|
+
sparkColor: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type TIdpAdminPage =
|
|
33
|
+
| 'overview'
|
|
34
|
+
| 'profile'
|
|
35
|
+
| 'security'
|
|
36
|
+
| 'sessions'
|
|
37
|
+
| 'apps'
|
|
38
|
+
| 'org-general'
|
|
39
|
+
| 'org-settings'
|
|
40
|
+
| 'org-members'
|
|
41
|
+
| 'org-apps'
|
|
42
|
+
| 'support'
|
|
43
|
+
| 'ga-users'
|
|
44
|
+
| 'ga-orgs'
|
|
45
|
+
| 'ga-apps';
|
|
46
|
+
|
|
47
|
+
export interface IIdpAdminUser {
|
|
48
|
+
name: string;
|
|
49
|
+
email: string;
|
|
50
|
+
initials?: string;
|
|
51
|
+
username?: string;
|
|
52
|
+
mobileNumber?: string;
|
|
53
|
+
status?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface IIdpAdminOrg {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
slug: string;
|
|
60
|
+
myRole?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface IIdpAdminNavigateEventDetail {
|
|
64
|
+
page: TIdpAdminPage;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface IIdpAdminOrgSelectEventDetail {
|
|
68
|
+
orgId: string;
|
|
69
|
+
org: IIdpAdminOrg | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface IIdpAdminSession {
|
|
73
|
+
id: string;
|
|
74
|
+
deviceName: string;
|
|
75
|
+
browser: string;
|
|
76
|
+
os: string;
|
|
77
|
+
ip: string;
|
|
78
|
+
lastActive: number;
|
|
79
|
+
createdAt: number;
|
|
80
|
+
isCurrent: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface IIdpAdminActivity {
|
|
84
|
+
id: string;
|
|
85
|
+
action: string;
|
|
86
|
+
description: string;
|
|
87
|
+
timestamp: number;
|
|
88
|
+
ip?: string;
|
|
89
|
+
targetType?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface IIdpAdminApp {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
logoUrl?: string;
|
|
97
|
+
appUrl?: string;
|
|
98
|
+
category?: string;
|
|
99
|
+
type?: string;
|
|
100
|
+
status?: string;
|
|
101
|
+
isConnected?: boolean;
|
|
102
|
+
connectionCount?: number;
|
|
103
|
+
clientId?: string;
|
|
104
|
+
scopes?: string[];
|
|
105
|
+
grants?: string[];
|
|
106
|
+
roleMappings?: IIdpAdminAppRoleMapping[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface IIdpAdminOrgRoleDefinition {
|
|
110
|
+
key: string;
|
|
111
|
+
name: string;
|
|
112
|
+
description?: string;
|
|
113
|
+
createdAt?: number;
|
|
114
|
+
updatedAt?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface IIdpAdminAppRoleMapping {
|
|
118
|
+
orgRoleKey: string;
|
|
119
|
+
appRoles: string[];
|
|
120
|
+
permissions: string[];
|
|
121
|
+
scopes: string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface IIdpAdminMember {
|
|
125
|
+
userId: string;
|
|
126
|
+
name: string;
|
|
127
|
+
email: string;
|
|
128
|
+
roles: string[];
|
|
129
|
+
isCurrentUser?: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface IIdpAdminInvitation {
|
|
133
|
+
id: string;
|
|
134
|
+
email: string;
|
|
135
|
+
roles: string[];
|
|
136
|
+
invitedAt: number;
|
|
137
|
+
expiresAt: number;
|
|
138
|
+
status?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface IIdpAdminPassportDevice {
|
|
142
|
+
id: string;
|
|
143
|
+
label: string;
|
|
144
|
+
platform: string;
|
|
145
|
+
status: string;
|
|
146
|
+
capabilities?: {
|
|
147
|
+
gps?: boolean;
|
|
148
|
+
nfc?: boolean;
|
|
149
|
+
push?: boolean;
|
|
150
|
+
};
|
|
151
|
+
appVersion?: string;
|
|
152
|
+
createdAt: number;
|
|
153
|
+
lastSeenAt?: number;
|
|
154
|
+
lastChallengeAt?: number;
|
|
155
|
+
pushRegistered?: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface IIdpAdminPassportEnrollment {
|
|
159
|
+
challengeId: string;
|
|
160
|
+
pairingToken: string;
|
|
161
|
+
pairingPayload: string;
|
|
162
|
+
signingPayload: string;
|
|
163
|
+
expiresAt: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface IIdpAdminSessionEventDetail {
|
|
167
|
+
sessionId: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface IIdpAdminAppToggleEventDetail {
|
|
171
|
+
appId: string;
|
|
172
|
+
connected: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface IIdpAdminMemberEventDetail {
|
|
176
|
+
userId: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface IIdpAdminMemberRolesEventDetail {
|
|
180
|
+
userId: string;
|
|
181
|
+
roles: string[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface IIdpAdminInvitationEventDetail {
|
|
185
|
+
invitationId: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface IIdpAdminOrgUpdateEventDetail {
|
|
189
|
+
organizationId: string;
|
|
190
|
+
name: string;
|
|
191
|
+
slug: string;
|
|
192
|
+
confirmationText: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface IIdpAdminOrgTransferEventDetail {
|
|
196
|
+
organizationId: string;
|
|
197
|
+
newOwnerId: string;
|
|
198
|
+
confirmationText: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface IIdpAdminOrgDeleteEventDetail {
|
|
202
|
+
organizationId: string;
|
|
203
|
+
confirmationText: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface IIdpAdminOrgRoleUpsertEventDetail {
|
|
207
|
+
organizationId: string;
|
|
208
|
+
roleDefinition: {
|
|
209
|
+
key: string;
|
|
210
|
+
name: string;
|
|
211
|
+
description?: string;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface IIdpAdminOrgRoleDeleteEventDetail {
|
|
216
|
+
organizationId: string;
|
|
217
|
+
roleKey: string;
|
|
218
|
+
confirmationText: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface IIdpAdminAppRoleMappingsEventDetail {
|
|
222
|
+
organizationId: string;
|
|
223
|
+
appId: string;
|
|
224
|
+
roleMappings: IIdpAdminAppRoleMapping[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface IIdpAdminPasswordChangeEventDetail {
|
|
228
|
+
currentPassword: string;
|
|
229
|
+
newPassword: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface IIdpAdminPassportEnrollmentEventDetail {
|
|
233
|
+
deviceLabel: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface IIdpAdminPassportDeviceEventDetail {
|
|
237
|
+
deviceId: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@customElement('idp-admin-shell')
|
|
241
|
+
export class IdpAdminShell extends DeesElement {
|
|
242
|
+
public static demo = () => html`<idp-admin-shell global-admin></idp-admin-shell>`;
|
|
243
|
+
public static demoGroups = ['idp.global v3 full pages'];
|
|
244
|
+
|
|
245
|
+
public static styles = [
|
|
246
|
+
...idpElementStyles,
|
|
247
|
+
css`
|
|
248
|
+
:host {
|
|
249
|
+
display: block;
|
|
250
|
+
height: 100vh;
|
|
251
|
+
max-height: 100vh;
|
|
252
|
+
min-height: 0;
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
color: var(--idp-fg);
|
|
255
|
+
}
|
|
256
|
+
.shell {
|
|
257
|
+
height: 100%;
|
|
258
|
+
min-height: 0;
|
|
259
|
+
display: grid;
|
|
260
|
+
grid-template-columns: 220px minmax(0, 1fr);
|
|
261
|
+
overflow: hidden;
|
|
262
|
+
border: 0;
|
|
263
|
+
background: var(--idp-bg);
|
|
264
|
+
}
|
|
265
|
+
.sidebar {
|
|
266
|
+
height: 100%;
|
|
267
|
+
min-height: 0;
|
|
268
|
+
display: flex;
|
|
269
|
+
flex-direction: column;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
border-right: 1px solid var(--idp-border);
|
|
272
|
+
background: var(--idp-bg);
|
|
273
|
+
}
|
|
274
|
+
.logo-block {
|
|
275
|
+
padding: 14px 16px 10px;
|
|
276
|
+
border-bottom: 1px solid var(--idp-border);
|
|
277
|
+
}
|
|
278
|
+
.logo {
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 8px;
|
|
282
|
+
}
|
|
283
|
+
.logo-icon {
|
|
284
|
+
width: 26px;
|
|
285
|
+
height: 26px;
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
justify-content: center;
|
|
289
|
+
flex-shrink: 0;
|
|
290
|
+
border-radius: 7px;
|
|
291
|
+
background: var(--idp-primary);
|
|
292
|
+
color: var(--idp-primary-fg);
|
|
293
|
+
}
|
|
294
|
+
.logo-text {
|
|
295
|
+
color: var(--idp-fg);
|
|
296
|
+
font-size: 13px;
|
|
297
|
+
font-weight: 700;
|
|
298
|
+
letter-spacing: -0.3px;
|
|
299
|
+
}
|
|
300
|
+
.nav-wrap {
|
|
301
|
+
flex: 1;
|
|
302
|
+
min-height: 0;
|
|
303
|
+
overflow: auto;
|
|
304
|
+
padding: 10px 8px;
|
|
305
|
+
}
|
|
306
|
+
.state-card {
|
|
307
|
+
padding: 42px 24px;
|
|
308
|
+
border: 1px solid var(--idp-border);
|
|
309
|
+
border-radius: var(--idp-radius);
|
|
310
|
+
background: var(--idp-card);
|
|
311
|
+
color: var(--idp-muted-fg);
|
|
312
|
+
text-align: center;
|
|
313
|
+
}
|
|
314
|
+
.state-icon {
|
|
315
|
+
width: 38px;
|
|
316
|
+
height: 38px;
|
|
317
|
+
display: inline-flex;
|
|
318
|
+
align-items: center;
|
|
319
|
+
justify-content: center;
|
|
320
|
+
margin-bottom: 12px;
|
|
321
|
+
border: 1px solid var(--idp-border);
|
|
322
|
+
border-radius: 10px;
|
|
323
|
+
background: var(--idp-muted);
|
|
324
|
+
color: var(--idp-muted-fg);
|
|
325
|
+
}
|
|
326
|
+
.state-title {
|
|
327
|
+
margin-bottom: 4px;
|
|
328
|
+
color: var(--idp-fg);
|
|
329
|
+
font-size: 14px;
|
|
330
|
+
font-weight: 600;
|
|
331
|
+
}
|
|
332
|
+
.state-description {
|
|
333
|
+
max-width: 420px;
|
|
334
|
+
margin: 0 auto;
|
|
335
|
+
font-size: 12px;
|
|
336
|
+
line-height: 1.5;
|
|
337
|
+
}
|
|
338
|
+
.notice-card {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: flex-start;
|
|
341
|
+
gap: 12px;
|
|
342
|
+
padding: 14px 16px;
|
|
343
|
+
border: 1px solid var(--idp-info-border);
|
|
344
|
+
border-radius: var(--idp-radius);
|
|
345
|
+
background: var(--idp-info-bg);
|
|
346
|
+
color: var(--idp-info);
|
|
347
|
+
}
|
|
348
|
+
.notice-card .muted {
|
|
349
|
+
color: var(--idp-muted-fg);
|
|
350
|
+
}
|
|
351
|
+
.nav-section {
|
|
352
|
+
margin-bottom: 20px;
|
|
353
|
+
}
|
|
354
|
+
.nav-title {
|
|
355
|
+
padding: 0 8px;
|
|
356
|
+
margin-bottom: 4px;
|
|
357
|
+
color: var(--idp-muted-fg);
|
|
358
|
+
font-size: 10px;
|
|
359
|
+
font-weight: 600;
|
|
360
|
+
letter-spacing: 0.8px;
|
|
361
|
+
text-transform: uppercase;
|
|
362
|
+
}
|
|
363
|
+
.nav-list {
|
|
364
|
+
display: grid;
|
|
365
|
+
}
|
|
366
|
+
.nav-item,
|
|
367
|
+
.org-switch,
|
|
368
|
+
.plain-button {
|
|
369
|
+
border: 0;
|
|
370
|
+
font-family: inherit;
|
|
371
|
+
}
|
|
372
|
+
.nav-item {
|
|
373
|
+
width: 100%;
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
gap: 8px;
|
|
377
|
+
padding: 6px 8px;
|
|
378
|
+
border-radius: 7px;
|
|
379
|
+
background: transparent;
|
|
380
|
+
color: var(--idp-muted-fg);
|
|
381
|
+
font-size: 13px;
|
|
382
|
+
font-weight: 400;
|
|
383
|
+
text-align: left;
|
|
384
|
+
}
|
|
385
|
+
.nav-item.active {
|
|
386
|
+
background: var(--idp-muted);
|
|
387
|
+
color: var(--idp-fg);
|
|
388
|
+
font-weight: 500;
|
|
389
|
+
}
|
|
390
|
+
.nav-item idp-icon {
|
|
391
|
+
color: currentColor;
|
|
392
|
+
}
|
|
393
|
+
.nav-item.active idp-icon {
|
|
394
|
+
color: var(--idp-accent);
|
|
395
|
+
}
|
|
396
|
+
.org-switch-wrap {
|
|
397
|
+
position: relative;
|
|
398
|
+
margin-bottom: 2px;
|
|
399
|
+
}
|
|
400
|
+
.org-switch {
|
|
401
|
+
width: 100%;
|
|
402
|
+
display: flex;
|
|
403
|
+
align-items: center;
|
|
404
|
+
gap: 8px;
|
|
405
|
+
padding: 6px 8px;
|
|
406
|
+
border-radius: 7px;
|
|
407
|
+
background: transparent;
|
|
408
|
+
color: var(--idp-muted-fg);
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
}
|
|
411
|
+
.org-switch:hover {
|
|
412
|
+
background: var(--idp-muted);
|
|
413
|
+
}
|
|
414
|
+
.org-switch.open {
|
|
415
|
+
background: var(--idp-muted);
|
|
416
|
+
}
|
|
417
|
+
.org-switch.open .org-name {
|
|
418
|
+
color: var(--idp-accent);
|
|
419
|
+
}
|
|
420
|
+
.org-switch.open idp-icon {
|
|
421
|
+
transform: rotate(180deg);
|
|
422
|
+
}
|
|
423
|
+
.org-menu {
|
|
424
|
+
position: absolute;
|
|
425
|
+
top: calc(100% + 4px);
|
|
426
|
+
left: 0;
|
|
427
|
+
right: 0;
|
|
428
|
+
z-index: 40;
|
|
429
|
+
overflow: hidden;
|
|
430
|
+
border: 1px solid var(--idp-border);
|
|
431
|
+
border-radius: 10px;
|
|
432
|
+
background: var(--idp-card);
|
|
433
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
|
434
|
+
}
|
|
435
|
+
.org-menu-title {
|
|
436
|
+
padding: 6px 8px 3px;
|
|
437
|
+
color: var(--idp-muted-fg);
|
|
438
|
+
font-size: 10px;
|
|
439
|
+
font-weight: 600;
|
|
440
|
+
letter-spacing: 0.7px;
|
|
441
|
+
text-transform: uppercase;
|
|
442
|
+
}
|
|
443
|
+
.org-menu-item,
|
|
444
|
+
.org-create {
|
|
445
|
+
width: 100%;
|
|
446
|
+
display: flex;
|
|
447
|
+
align-items: center;
|
|
448
|
+
gap: 8px;
|
|
449
|
+
border: 0;
|
|
450
|
+
background: transparent;
|
|
451
|
+
font-family: inherit;
|
|
452
|
+
text-align: left;
|
|
453
|
+
}
|
|
454
|
+
.org-menu-item {
|
|
455
|
+
padding: 6px 8px;
|
|
456
|
+
cursor: pointer;
|
|
457
|
+
}
|
|
458
|
+
.org-menu-item.selected {
|
|
459
|
+
background: var(--idp-accent-soft);
|
|
460
|
+
}
|
|
461
|
+
.org-menu-item .org-avatar {
|
|
462
|
+
width: 20px;
|
|
463
|
+
height: 20px;
|
|
464
|
+
font-size: 7px;
|
|
465
|
+
}
|
|
466
|
+
.org-menu-name {
|
|
467
|
+
overflow: hidden;
|
|
468
|
+
color: var(--idp-fg);
|
|
469
|
+
font-size: 12px;
|
|
470
|
+
font-weight: 400;
|
|
471
|
+
text-overflow: ellipsis;
|
|
472
|
+
white-space: nowrap;
|
|
473
|
+
}
|
|
474
|
+
.org-menu-item.selected .org-menu-name {
|
|
475
|
+
color: var(--idp-accent);
|
|
476
|
+
}
|
|
477
|
+
.org-menu-role {
|
|
478
|
+
color: var(--idp-muted-fg);
|
|
479
|
+
font-size: 10px;
|
|
480
|
+
}
|
|
481
|
+
.org-menu-divider {
|
|
482
|
+
height: 1px;
|
|
483
|
+
margin: 3px 0;
|
|
484
|
+
background: var(--idp-border);
|
|
485
|
+
}
|
|
486
|
+
.org-create {
|
|
487
|
+
padding: 6px 8px 8px;
|
|
488
|
+
color: var(--idp-muted-fg);
|
|
489
|
+
cursor: pointer;
|
|
490
|
+
font-size: 12px;
|
|
491
|
+
}
|
|
492
|
+
.org-create-icon {
|
|
493
|
+
width: 20px;
|
|
494
|
+
height: 20px;
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
justify-content: center;
|
|
498
|
+
flex-shrink: 0;
|
|
499
|
+
border: 1.5px dashed var(--idp-border);
|
|
500
|
+
border-radius: 5px;
|
|
501
|
+
}
|
|
502
|
+
.org-name {
|
|
503
|
+
flex: 1;
|
|
504
|
+
min-width: 0;
|
|
505
|
+
overflow: hidden;
|
|
506
|
+
color: var(--idp-muted-fg);
|
|
507
|
+
font-size: 13px;
|
|
508
|
+
font-weight: 400;
|
|
509
|
+
text-align: left;
|
|
510
|
+
text-overflow: ellipsis;
|
|
511
|
+
white-space: nowrap;
|
|
512
|
+
}
|
|
513
|
+
.org-avatar,
|
|
514
|
+
.user-avatar {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: center;
|
|
518
|
+
flex-shrink: 0;
|
|
519
|
+
font-family: var(--idp-mono);
|
|
520
|
+
font-weight: 700;
|
|
521
|
+
}
|
|
522
|
+
.org-avatar {
|
|
523
|
+
width: 14px;
|
|
524
|
+
height: 14px;
|
|
525
|
+
border: 1px solid var(--idp-border);
|
|
526
|
+
border-radius: 6px;
|
|
527
|
+
background: var(--idp-accent-soft);
|
|
528
|
+
color: var(--idp-accent);
|
|
529
|
+
font-size: 5px;
|
|
530
|
+
}
|
|
531
|
+
.user-footer {
|
|
532
|
+
display: flex;
|
|
533
|
+
align-items: center;
|
|
534
|
+
gap: 9px;
|
|
535
|
+
padding: 10px 12px;
|
|
536
|
+
border-top: 1px solid var(--idp-border);
|
|
537
|
+
}
|
|
538
|
+
.user-avatar {
|
|
539
|
+
width: 26px;
|
|
540
|
+
height: 26px;
|
|
541
|
+
border-radius: 50%;
|
|
542
|
+
background: oklch(0.85 0.08 240);
|
|
543
|
+
color: oklch(0.3 0.08 240);
|
|
544
|
+
font-size: 9px;
|
|
545
|
+
}
|
|
546
|
+
.user-meta {
|
|
547
|
+
flex: 1;
|
|
548
|
+
min-width: 0;
|
|
549
|
+
}
|
|
550
|
+
.user-name,
|
|
551
|
+
.user-email {
|
|
552
|
+
overflow: hidden;
|
|
553
|
+
text-overflow: ellipsis;
|
|
554
|
+
white-space: nowrap;
|
|
555
|
+
}
|
|
556
|
+
.user-name {
|
|
557
|
+
color: var(--idp-fg);
|
|
558
|
+
font-size: 12px;
|
|
559
|
+
font-weight: 500;
|
|
560
|
+
}
|
|
561
|
+
.user-email {
|
|
562
|
+
color: var(--idp-muted-fg);
|
|
563
|
+
font-size: 10px;
|
|
564
|
+
}
|
|
565
|
+
main {
|
|
566
|
+
height: 100%;
|
|
567
|
+
min-height: 0;
|
|
568
|
+
min-width: 0;
|
|
569
|
+
overflow: auto;
|
|
570
|
+
background: var(--idp-bg);
|
|
571
|
+
}
|
|
572
|
+
.page-head {
|
|
573
|
+
display: flex;
|
|
574
|
+
align-items: flex-end;
|
|
575
|
+
justify-content: space-between;
|
|
576
|
+
gap: 16px;
|
|
577
|
+
flex-wrap: wrap;
|
|
578
|
+
padding: 24px 28px 18px;
|
|
579
|
+
border-bottom: 1px solid var(--idp-border);
|
|
580
|
+
background: var(--idp-bg);
|
|
581
|
+
}
|
|
582
|
+
.eyebrow-row {
|
|
583
|
+
display: flex;
|
|
584
|
+
align-items: center;
|
|
585
|
+
gap: 8px;
|
|
586
|
+
margin-bottom: 4px;
|
|
587
|
+
}
|
|
588
|
+
.eyebrow {
|
|
589
|
+
color: var(--idp-muted-fg);
|
|
590
|
+
font-family: var(--idp-mono);
|
|
591
|
+
font-size: 10px;
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
letter-spacing: 0.08em;
|
|
594
|
+
text-transform: uppercase;
|
|
595
|
+
}
|
|
596
|
+
.live-pill {
|
|
597
|
+
display: inline-flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: 4px;
|
|
600
|
+
padding: 1px 7px;
|
|
601
|
+
border: 1px solid var(--idp-ok-border);
|
|
602
|
+
border-radius: 999px;
|
|
603
|
+
background: var(--idp-ok-bg);
|
|
604
|
+
color: var(--idp-ok);
|
|
605
|
+
font-family: var(--idp-mono);
|
|
606
|
+
font-size: 10px;
|
|
607
|
+
font-weight: 500;
|
|
608
|
+
}
|
|
609
|
+
.live-dot,
|
|
610
|
+
.feed-dot {
|
|
611
|
+
width: 6px;
|
|
612
|
+
height: 6px;
|
|
613
|
+
border-radius: 50%;
|
|
614
|
+
background: var(--idp-ok);
|
|
615
|
+
box-shadow: 0 0 6px var(--idp-ok);
|
|
616
|
+
animation: idp-blink 1.6s ease-in-out infinite;
|
|
617
|
+
}
|
|
618
|
+
@keyframes idp-blink {
|
|
619
|
+
0%, 100% { opacity: 1; }
|
|
620
|
+
50% { opacity: 0.35; }
|
|
621
|
+
}
|
|
622
|
+
h1,
|
|
623
|
+
h2 {
|
|
624
|
+
margin: 0;
|
|
625
|
+
color: var(--idp-fg);
|
|
626
|
+
font-family: var(--idp-display);
|
|
627
|
+
letter-spacing: -0.025em;
|
|
628
|
+
}
|
|
629
|
+
h1 {
|
|
630
|
+
font-size: 26px;
|
|
631
|
+
font-weight: 700;
|
|
632
|
+
line-height: 1.1;
|
|
633
|
+
}
|
|
634
|
+
.lead {
|
|
635
|
+
margin-top: 4px;
|
|
636
|
+
color: var(--idp-muted-fg);
|
|
637
|
+
font-size: 13px;
|
|
638
|
+
}
|
|
639
|
+
.lead code,
|
|
640
|
+
.mono {
|
|
641
|
+
color: var(--idp-info);
|
|
642
|
+
font-family: var(--idp-mono);
|
|
643
|
+
}
|
|
644
|
+
.page-actions {
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: center;
|
|
647
|
+
gap: 12px;
|
|
648
|
+
}
|
|
649
|
+
.tabs {
|
|
650
|
+
display: inline-flex;
|
|
651
|
+
gap: 1px;
|
|
652
|
+
padding: 2px;
|
|
653
|
+
border: 1px solid var(--idp-border);
|
|
654
|
+
border-radius: 6px;
|
|
655
|
+
background: var(--idp-bg-2);
|
|
656
|
+
}
|
|
657
|
+
.tab {
|
|
658
|
+
padding: 4px 10px;
|
|
659
|
+
border-radius: 4px;
|
|
660
|
+
color: var(--idp-muted-fg);
|
|
661
|
+
font-family: var(--idp-mono);
|
|
662
|
+
font-size: 11px;
|
|
663
|
+
font-weight: 500;
|
|
664
|
+
}
|
|
665
|
+
.tab.active {
|
|
666
|
+
background: var(--idp-muted);
|
|
667
|
+
color: var(--idp-fg);
|
|
668
|
+
}
|
|
669
|
+
.plain-button {
|
|
670
|
+
display: inline-flex;
|
|
671
|
+
align-items: center;
|
|
672
|
+
gap: 6px;
|
|
673
|
+
height: 28px;
|
|
674
|
+
padding: 5px 10px;
|
|
675
|
+
border-radius: 8px;
|
|
676
|
+
background: transparent;
|
|
677
|
+
color: var(--idp-fg);
|
|
678
|
+
font-size: 12px;
|
|
679
|
+
font-weight: 500;
|
|
680
|
+
white-space: nowrap;
|
|
681
|
+
}
|
|
682
|
+
.plain-button.outline {
|
|
683
|
+
border: 1px solid var(--idp-border);
|
|
684
|
+
}
|
|
685
|
+
.plain-button.primary {
|
|
686
|
+
background: var(--idp-accent);
|
|
687
|
+
color: var(--idp-accent-fg);
|
|
688
|
+
}
|
|
689
|
+
.plain-button.destructive {
|
|
690
|
+
background: var(--idp-destructive);
|
|
691
|
+
color: var(--idp-accent-fg);
|
|
692
|
+
}
|
|
693
|
+
.plain-button.ghost {
|
|
694
|
+
color: var(--idp-fg);
|
|
695
|
+
}
|
|
696
|
+
.plain-button:disabled {
|
|
697
|
+
opacity: 0.45;
|
|
698
|
+
cursor: not-allowed;
|
|
699
|
+
}
|
|
700
|
+
.body {
|
|
701
|
+
display: flex;
|
|
702
|
+
flex-direction: column;
|
|
703
|
+
gap: 16px;
|
|
704
|
+
padding: 20px 28px 32px;
|
|
705
|
+
}
|
|
706
|
+
.narrow-body {
|
|
707
|
+
max-width: 700px;
|
|
708
|
+
}
|
|
709
|
+
.wide-body {
|
|
710
|
+
max-width: 860px;
|
|
711
|
+
}
|
|
712
|
+
.section-card {
|
|
713
|
+
margin-bottom: 16px;
|
|
714
|
+
padding: 20px;
|
|
715
|
+
border: 1px solid var(--idp-border);
|
|
716
|
+
border-radius: var(--idp-radius);
|
|
717
|
+
background: var(--idp-card);
|
|
718
|
+
}
|
|
719
|
+
.section-card.compact {
|
|
720
|
+
padding: 14px;
|
|
721
|
+
}
|
|
722
|
+
.section-headline {
|
|
723
|
+
display: flex;
|
|
724
|
+
align-items: flex-start;
|
|
725
|
+
justify-content: space-between;
|
|
726
|
+
gap: 16px;
|
|
727
|
+
margin-bottom: 16px;
|
|
728
|
+
}
|
|
729
|
+
.section-title {
|
|
730
|
+
color: var(--idp-fg);
|
|
731
|
+
font-size: 14px;
|
|
732
|
+
font-weight: 600;
|
|
733
|
+
}
|
|
734
|
+
.section-description,
|
|
735
|
+
.muted {
|
|
736
|
+
color: var(--idp-muted-fg);
|
|
737
|
+
font-size: 12px;
|
|
738
|
+
line-height: 1.5;
|
|
739
|
+
}
|
|
740
|
+
.form-row {
|
|
741
|
+
display: grid;
|
|
742
|
+
grid-template-columns: 180px 1fr;
|
|
743
|
+
gap: 16px;
|
|
744
|
+
align-items: start;
|
|
745
|
+
padding: 12px 0;
|
|
746
|
+
border-bottom: 1px solid var(--idp-border);
|
|
747
|
+
}
|
|
748
|
+
.form-row:last-child {
|
|
749
|
+
border-bottom: 0;
|
|
750
|
+
}
|
|
751
|
+
.form-label {
|
|
752
|
+
display: flex;
|
|
753
|
+
gap: 3px;
|
|
754
|
+
color: var(--idp-fg);
|
|
755
|
+
font-size: 13px;
|
|
756
|
+
font-weight: 500;
|
|
757
|
+
}
|
|
758
|
+
.form-hint {
|
|
759
|
+
margin-top: 2px;
|
|
760
|
+
color: var(--idp-muted-fg);
|
|
761
|
+
font-size: 12px;
|
|
762
|
+
line-height: 1.4;
|
|
763
|
+
}
|
|
764
|
+
.input,
|
|
765
|
+
.select,
|
|
766
|
+
.textarea {
|
|
767
|
+
box-sizing: border-box;
|
|
768
|
+
width: 100%;
|
|
769
|
+
border: 1px solid var(--idp-border);
|
|
770
|
+
border-radius: 8px;
|
|
771
|
+
outline: none;
|
|
772
|
+
background: var(--idp-bg);
|
|
773
|
+
color: var(--idp-fg);
|
|
774
|
+
font-family: inherit;
|
|
775
|
+
font-size: 13px;
|
|
776
|
+
}
|
|
777
|
+
.input,
|
|
778
|
+
.select {
|
|
779
|
+
height: 34px;
|
|
780
|
+
padding: 0 10px;
|
|
781
|
+
}
|
|
782
|
+
.textarea {
|
|
783
|
+
min-height: 96px;
|
|
784
|
+
padding: 8px 10px;
|
|
785
|
+
resize: vertical;
|
|
786
|
+
}
|
|
787
|
+
.input-group {
|
|
788
|
+
display: flex;
|
|
789
|
+
overflow: hidden;
|
|
790
|
+
border: 1px solid var(--idp-border);
|
|
791
|
+
border-radius: 8px;
|
|
792
|
+
background: var(--idp-bg);
|
|
793
|
+
}
|
|
794
|
+
.input-prefix {
|
|
795
|
+
height: 34px;
|
|
796
|
+
display: flex;
|
|
797
|
+
align-items: center;
|
|
798
|
+
padding: 0 8px;
|
|
799
|
+
border-right: 1px solid var(--idp-border);
|
|
800
|
+
background: var(--idp-muted);
|
|
801
|
+
color: var(--idp-muted-fg);
|
|
802
|
+
font-family: var(--idp-mono);
|
|
803
|
+
font-size: 12px;
|
|
804
|
+
white-space: nowrap;
|
|
805
|
+
}
|
|
806
|
+
.input-group .input {
|
|
807
|
+
border: 0;
|
|
808
|
+
border-radius: 0;
|
|
809
|
+
background: transparent;
|
|
810
|
+
}
|
|
811
|
+
.divider {
|
|
812
|
+
height: 1px;
|
|
813
|
+
margin: 16px 0;
|
|
814
|
+
background: var(--idp-border);
|
|
815
|
+
}
|
|
816
|
+
.code-block {
|
|
817
|
+
display: flex;
|
|
818
|
+
align-items: center;
|
|
819
|
+
gap: 8px;
|
|
820
|
+
padding: 6px 10px;
|
|
821
|
+
border: 1px solid var(--idp-border);
|
|
822
|
+
border-radius: 7px;
|
|
823
|
+
background: var(--idp-muted);
|
|
824
|
+
}
|
|
825
|
+
.code-block code {
|
|
826
|
+
flex: 1;
|
|
827
|
+
overflow-wrap: anywhere;
|
|
828
|
+
color: var(--idp-fg);
|
|
829
|
+
font-family: var(--idp-mono);
|
|
830
|
+
font-size: 12px;
|
|
831
|
+
}
|
|
832
|
+
.avatar,
|
|
833
|
+
.app-avatar,
|
|
834
|
+
.icon-tile {
|
|
835
|
+
display: flex;
|
|
836
|
+
align-items: center;
|
|
837
|
+
justify-content: center;
|
|
838
|
+
flex-shrink: 0;
|
|
839
|
+
}
|
|
840
|
+
.avatar {
|
|
841
|
+
border-radius: 50%;
|
|
842
|
+
background: oklch(0.85 0.08 240);
|
|
843
|
+
color: oklch(0.3 0.08 240);
|
|
844
|
+
font-family: var(--idp-mono);
|
|
845
|
+
font-weight: 600;
|
|
846
|
+
}
|
|
847
|
+
.app-avatar,
|
|
848
|
+
.icon-tile {
|
|
849
|
+
border: 1px solid var(--idp-border);
|
|
850
|
+
border-radius: 10px;
|
|
851
|
+
background: var(--idp-muted);
|
|
852
|
+
color: var(--idp-muted-fg);
|
|
853
|
+
}
|
|
854
|
+
.app-avatar {
|
|
855
|
+
width: 40px;
|
|
856
|
+
height: 40px;
|
|
857
|
+
background: var(--idp-accent-soft);
|
|
858
|
+
color: var(--idp-accent);
|
|
859
|
+
font-size: 14px;
|
|
860
|
+
font-weight: 700;
|
|
861
|
+
}
|
|
862
|
+
.icon-tile {
|
|
863
|
+
width: 38px;
|
|
864
|
+
height: 38px;
|
|
865
|
+
}
|
|
866
|
+
.list-stack {
|
|
867
|
+
display: grid;
|
|
868
|
+
gap: 12px;
|
|
869
|
+
}
|
|
870
|
+
.row-card {
|
|
871
|
+
display: flex;
|
|
872
|
+
align-items: flex-start;
|
|
873
|
+
gap: 14px;
|
|
874
|
+
padding: 16px;
|
|
875
|
+
border: 1px solid var(--idp-border);
|
|
876
|
+
border-radius: var(--idp-radius);
|
|
877
|
+
background: var(--idp-card);
|
|
878
|
+
}
|
|
879
|
+
.split-row {
|
|
880
|
+
display: flex;
|
|
881
|
+
align-items: center;
|
|
882
|
+
justify-content: space-between;
|
|
883
|
+
gap: 16px;
|
|
884
|
+
}
|
|
885
|
+
.chip-row {
|
|
886
|
+
display: flex;
|
|
887
|
+
flex-wrap: wrap;
|
|
888
|
+
gap: 4px;
|
|
889
|
+
}
|
|
890
|
+
.switch {
|
|
891
|
+
width: 36px;
|
|
892
|
+
height: 20px;
|
|
893
|
+
position: relative;
|
|
894
|
+
flex-shrink: 0;
|
|
895
|
+
border-radius: 10px;
|
|
896
|
+
background: var(--idp-accent);
|
|
897
|
+
}
|
|
898
|
+
.switch::after {
|
|
899
|
+
content: '';
|
|
900
|
+
position: absolute;
|
|
901
|
+
top: 2px;
|
|
902
|
+
left: 18px;
|
|
903
|
+
width: 16px;
|
|
904
|
+
height: 16px;
|
|
905
|
+
border-radius: 8px;
|
|
906
|
+
background: #fff;
|
|
907
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
908
|
+
}
|
|
909
|
+
.sub-tabs {
|
|
910
|
+
width: fit-content;
|
|
911
|
+
display: flex;
|
|
912
|
+
gap: 2px;
|
|
913
|
+
margin-bottom: 16px;
|
|
914
|
+
padding: 3px;
|
|
915
|
+
border-radius: 8px;
|
|
916
|
+
background: var(--idp-muted);
|
|
917
|
+
}
|
|
918
|
+
.sub-tab {
|
|
919
|
+
padding: 5px 12px;
|
|
920
|
+
border-radius: 6px;
|
|
921
|
+
color: var(--idp-muted-fg);
|
|
922
|
+
font-size: 12px;
|
|
923
|
+
}
|
|
924
|
+
.sub-tab.active {
|
|
925
|
+
background: var(--idp-bg);
|
|
926
|
+
color: var(--idp-fg);
|
|
927
|
+
font-weight: 500;
|
|
928
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
929
|
+
}
|
|
930
|
+
.kpis {
|
|
931
|
+
display: grid;
|
|
932
|
+
grid-template-columns: repeat(4, 1fr);
|
|
933
|
+
gap: 12px;
|
|
934
|
+
}
|
|
935
|
+
.kpi,
|
|
936
|
+
.card {
|
|
937
|
+
border: 1px solid var(--idp-border);
|
|
938
|
+
background: var(--idp-card);
|
|
939
|
+
}
|
|
940
|
+
.kpi {
|
|
941
|
+
position: relative;
|
|
942
|
+
min-height: 132px;
|
|
943
|
+
display: flex;
|
|
944
|
+
flex-direction: column;
|
|
945
|
+
gap: 4px;
|
|
946
|
+
overflow: hidden;
|
|
947
|
+
padding: 18px 20px 14px;
|
|
948
|
+
border-radius: 10px;
|
|
949
|
+
}
|
|
950
|
+
.kpi::before {
|
|
951
|
+
content: '';
|
|
952
|
+
position: absolute;
|
|
953
|
+
top: 0;
|
|
954
|
+
left: 0;
|
|
955
|
+
right: 0;
|
|
956
|
+
height: 2px;
|
|
957
|
+
background: var(--kpi-accent);
|
|
958
|
+
}
|
|
959
|
+
.kpi-label {
|
|
960
|
+
color: var(--idp-muted-fg);
|
|
961
|
+
font-size: 11.5px;
|
|
962
|
+
font-weight: 500;
|
|
963
|
+
letter-spacing: 0.02em;
|
|
964
|
+
}
|
|
965
|
+
.kpi-value {
|
|
966
|
+
color: var(--idp-fg);
|
|
967
|
+
font-family: var(--idp-display);
|
|
968
|
+
font-size: 30px;
|
|
969
|
+
font-weight: 700;
|
|
970
|
+
font-variant-numeric: tabular-nums;
|
|
971
|
+
letter-spacing: -0.025em;
|
|
972
|
+
line-height: 1.1;
|
|
973
|
+
}
|
|
974
|
+
.kpi-value span {
|
|
975
|
+
margin-left: 2px;
|
|
976
|
+
color: var(--idp-muted-fg);
|
|
977
|
+
font-size: 14px;
|
|
978
|
+
font-weight: 500;
|
|
979
|
+
}
|
|
980
|
+
.kpi-sub {
|
|
981
|
+
color: var(--idp-fg-3);
|
|
982
|
+
font-family: var(--idp-mono);
|
|
983
|
+
font-size: 11px;
|
|
984
|
+
}
|
|
985
|
+
.kpi-foot {
|
|
986
|
+
display: flex;
|
|
987
|
+
align-items: flex-end;
|
|
988
|
+
justify-content: space-between;
|
|
989
|
+
gap: 8px;
|
|
990
|
+
margin-top: auto;
|
|
991
|
+
}
|
|
992
|
+
.delta {
|
|
993
|
+
display: inline-flex;
|
|
994
|
+
align-items: center;
|
|
995
|
+
gap: 4px;
|
|
996
|
+
color: var(--idp-ok);
|
|
997
|
+
font-family: var(--idp-mono);
|
|
998
|
+
font-size: 11px;
|
|
999
|
+
font-weight: 600;
|
|
1000
|
+
}
|
|
1001
|
+
.sparkline {
|
|
1002
|
+
width: 84px;
|
|
1003
|
+
opacity: 0.85;
|
|
1004
|
+
}
|
|
1005
|
+
.primary-grid,
|
|
1006
|
+
.secondary-grid {
|
|
1007
|
+
display: grid;
|
|
1008
|
+
grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr);
|
|
1009
|
+
gap: 16px;
|
|
1010
|
+
}
|
|
1011
|
+
.card {
|
|
1012
|
+
overflow: hidden;
|
|
1013
|
+
border-radius: 8px;
|
|
1014
|
+
}
|
|
1015
|
+
.card-head {
|
|
1016
|
+
display: flex;
|
|
1017
|
+
align-items: center;
|
|
1018
|
+
gap: 10px;
|
|
1019
|
+
padding: 12px 16px;
|
|
1020
|
+
border-bottom: 1px solid var(--idp-border-soft);
|
|
1021
|
+
}
|
|
1022
|
+
.card-title {
|
|
1023
|
+
color: var(--idp-fg);
|
|
1024
|
+
font-size: 13px;
|
|
1025
|
+
font-weight: 600;
|
|
1026
|
+
}
|
|
1027
|
+
.feed-dot {
|
|
1028
|
+
display: inline-block;
|
|
1029
|
+
width: 8px;
|
|
1030
|
+
height: 8px;
|
|
1031
|
+
border-radius: 4px;
|
|
1032
|
+
background: var(--dot-color);
|
|
1033
|
+
box-shadow: var(--dot-glow, none);
|
|
1034
|
+
}
|
|
1035
|
+
.feed-meta {
|
|
1036
|
+
color: var(--idp-muted-fg);
|
|
1037
|
+
font-family: var(--idp-mono);
|
|
1038
|
+
font-size: 11.5px;
|
|
1039
|
+
}
|
|
1040
|
+
.feed-item {
|
|
1041
|
+
display: grid;
|
|
1042
|
+
grid-template-columns: 14px 1fr auto;
|
|
1043
|
+
gap: 12px;
|
|
1044
|
+
align-items: center;
|
|
1045
|
+
padding: 11px 16px;
|
|
1046
|
+
border-bottom: 1px solid var(--idp-border-soft);
|
|
1047
|
+
}
|
|
1048
|
+
.feed-item:last-child {
|
|
1049
|
+
border-bottom: 0;
|
|
1050
|
+
}
|
|
1051
|
+
.feed-text {
|
|
1052
|
+
color: var(--idp-fg-2);
|
|
1053
|
+
font-size: 12.5px;
|
|
1054
|
+
}
|
|
1055
|
+
.feed-text strong {
|
|
1056
|
+
color: var(--idp-fg);
|
|
1057
|
+
font-weight: 600;
|
|
1058
|
+
}
|
|
1059
|
+
.dialog-backdrop {
|
|
1060
|
+
position: fixed;
|
|
1061
|
+
inset: 0;
|
|
1062
|
+
z-index: 100;
|
|
1063
|
+
display: flex;
|
|
1064
|
+
align-items: center;
|
|
1065
|
+
justify-content: center;
|
|
1066
|
+
padding: 20px;
|
|
1067
|
+
background: rgba(0, 0, 0, 0.42);
|
|
1068
|
+
}
|
|
1069
|
+
.dialog-card {
|
|
1070
|
+
width: min(720px, 100%);
|
|
1071
|
+
max-height: min(760px, calc(100vh - 40px));
|
|
1072
|
+
display: flex;
|
|
1073
|
+
flex-direction: column;
|
|
1074
|
+
overflow: hidden;
|
|
1075
|
+
border: 1px solid var(--idp-border);
|
|
1076
|
+
border-radius: 12px;
|
|
1077
|
+
background: var(--idp-card);
|
|
1078
|
+
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.28);
|
|
1079
|
+
}
|
|
1080
|
+
.dialog-card.wide {
|
|
1081
|
+
width: min(980px, 100%);
|
|
1082
|
+
}
|
|
1083
|
+
.dialog-head {
|
|
1084
|
+
display: flex;
|
|
1085
|
+
align-items: flex-start;
|
|
1086
|
+
justify-content: space-between;
|
|
1087
|
+
gap: 16px;
|
|
1088
|
+
padding: 18px 20px 14px;
|
|
1089
|
+
border-bottom: 1px solid var(--idp-border);
|
|
1090
|
+
}
|
|
1091
|
+
.dialog-title {
|
|
1092
|
+
color: var(--idp-fg);
|
|
1093
|
+
font-size: 15px;
|
|
1094
|
+
font-weight: 700;
|
|
1095
|
+
}
|
|
1096
|
+
.dialog-body {
|
|
1097
|
+
display: grid;
|
|
1098
|
+
gap: 14px;
|
|
1099
|
+
overflow: auto;
|
|
1100
|
+
padding: 18px 20px;
|
|
1101
|
+
}
|
|
1102
|
+
.dialog-actions {
|
|
1103
|
+
display: flex;
|
|
1104
|
+
align-items: center;
|
|
1105
|
+
justify-content: flex-end;
|
|
1106
|
+
gap: 10px;
|
|
1107
|
+
padding: 14px 20px;
|
|
1108
|
+
border-top: 1px solid var(--idp-border);
|
|
1109
|
+
}
|
|
1110
|
+
.role-grid {
|
|
1111
|
+
display: grid;
|
|
1112
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
1113
|
+
gap: 8px;
|
|
1114
|
+
}
|
|
1115
|
+
.role-option {
|
|
1116
|
+
display: flex;
|
|
1117
|
+
align-items: flex-start;
|
|
1118
|
+
gap: 8px;
|
|
1119
|
+
padding: 9px;
|
|
1120
|
+
border: 1px solid var(--idp-border);
|
|
1121
|
+
border-radius: 8px;
|
|
1122
|
+
background: var(--idp-bg);
|
|
1123
|
+
}
|
|
1124
|
+
.role-option input {
|
|
1125
|
+
margin-top: 2px;
|
|
1126
|
+
}
|
|
1127
|
+
.mapping-row {
|
|
1128
|
+
display: grid;
|
|
1129
|
+
grid-template-columns: 160px repeat(3, minmax(0, 1fr));
|
|
1130
|
+
gap: 8px;
|
|
1131
|
+
align-items: start;
|
|
1132
|
+
padding: 10px;
|
|
1133
|
+
border: 1px solid var(--idp-border);
|
|
1134
|
+
border-radius: 8px;
|
|
1135
|
+
background: var(--idp-bg);
|
|
1136
|
+
}
|
|
1137
|
+
.mapping-role {
|
|
1138
|
+
color: var(--idp-fg);
|
|
1139
|
+
font-size: 12px;
|
|
1140
|
+
font-weight: 600;
|
|
1141
|
+
}
|
|
1142
|
+
.mapping-role .muted {
|
|
1143
|
+
display: block;
|
|
1144
|
+
margin-top: 2px;
|
|
1145
|
+
}
|
|
1146
|
+
.mapping-row .form-row {
|
|
1147
|
+
display: block;
|
|
1148
|
+
padding: 0;
|
|
1149
|
+
border-bottom: 0;
|
|
1150
|
+
}
|
|
1151
|
+
.mapping-row .form-label {
|
|
1152
|
+
margin-bottom: 4px;
|
|
1153
|
+
}
|
|
1154
|
+
@media (max-width: 1120px) {
|
|
1155
|
+
.shell {
|
|
1156
|
+
grid-template-columns: 1fr;
|
|
1157
|
+
}
|
|
1158
|
+
.sidebar {
|
|
1159
|
+
display: none;
|
|
1160
|
+
}
|
|
1161
|
+
.kpis {
|
|
1162
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1163
|
+
}
|
|
1164
|
+
.primary-grid,
|
|
1165
|
+
.secondary-grid {
|
|
1166
|
+
grid-template-columns: 1fr;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
@media (max-width: 720px) {
|
|
1170
|
+
.page-head,
|
|
1171
|
+
.body {
|
|
1172
|
+
padding-left: 20px;
|
|
1173
|
+
padding-right: 20px;
|
|
1174
|
+
}
|
|
1175
|
+
.page-actions {
|
|
1176
|
+
flex-wrap: wrap;
|
|
1177
|
+
}
|
|
1178
|
+
.kpis {
|
|
1179
|
+
grid-template-columns: 1fr;
|
|
1180
|
+
}
|
|
1181
|
+
.mapping-row {
|
|
1182
|
+
grid-template-columns: 1fr;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
`,
|
|
1186
|
+
];
|
|
1187
|
+
|
|
1188
|
+
@property({ type: String })
|
|
1189
|
+
public accessor page: TIdpAdminPage = 'overview';
|
|
1190
|
+
|
|
1191
|
+
@property({ type: Object })
|
|
1192
|
+
public accessor user: IIdpAdminUser = {
|
|
1193
|
+
name: '',
|
|
1194
|
+
email: '',
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
@property({ type: Array })
|
|
1198
|
+
public accessor orgs: IIdpAdminOrg[] = [];
|
|
1199
|
+
|
|
1200
|
+
@property({ type: String, attribute: 'selected-org-id' })
|
|
1201
|
+
public accessor selectedOrgId = '';
|
|
1202
|
+
|
|
1203
|
+
@property({ type: Boolean, attribute: 'global-admin', reflect: true })
|
|
1204
|
+
public accessor globalAdmin = false;
|
|
1205
|
+
|
|
1206
|
+
@property({ type: Boolean, attribute: 'data-loading', reflect: true })
|
|
1207
|
+
public accessor dataLoading = false;
|
|
1208
|
+
|
|
1209
|
+
@property({ type: String, attribute: 'data-error' })
|
|
1210
|
+
public accessor dataError = '';
|
|
1211
|
+
|
|
1212
|
+
@property({ type: Array })
|
|
1213
|
+
public accessor sessions: IIdpAdminSession[] = [];
|
|
1214
|
+
|
|
1215
|
+
@property({ type: Array })
|
|
1216
|
+
public accessor activities: IIdpAdminActivity[] = [];
|
|
1217
|
+
|
|
1218
|
+
@property({ type: Array })
|
|
1219
|
+
public accessor orgMembers: IIdpAdminMember[] = [];
|
|
1220
|
+
|
|
1221
|
+
@property({ type: Array })
|
|
1222
|
+
public accessor orgInvitations: IIdpAdminInvitation[] = [];
|
|
1223
|
+
|
|
1224
|
+
@property({ type: Array })
|
|
1225
|
+
public accessor orgRoleDefinitions: IIdpAdminOrgRoleDefinition[] = [];
|
|
1226
|
+
|
|
1227
|
+
@property({ type: Array })
|
|
1228
|
+
public accessor orgApps: IIdpAdminApp[] = [];
|
|
1229
|
+
|
|
1230
|
+
@property({ type: Array })
|
|
1231
|
+
public accessor accountApps: IIdpAdminApp[] = [];
|
|
1232
|
+
|
|
1233
|
+
@property({ type: Array })
|
|
1234
|
+
public accessor adminApps: IIdpAdminApp[] = [];
|
|
1235
|
+
|
|
1236
|
+
@property({ type: Array })
|
|
1237
|
+
public accessor passportDevices: IIdpAdminPassportDevice[] = [];
|
|
1238
|
+
|
|
1239
|
+
@property({ type: Object })
|
|
1240
|
+
public accessor passportEnrollment: IIdpAdminPassportEnrollment | null = null;
|
|
1241
|
+
|
|
1242
|
+
@property({ type: String, attribute: 'credential-message' })
|
|
1243
|
+
public accessor credentialMessage = '';
|
|
1244
|
+
|
|
1245
|
+
@state()
|
|
1246
|
+
private accessor orgMenuOpen = false;
|
|
1247
|
+
|
|
1248
|
+
@state()
|
|
1249
|
+
private accessor currentPassword = '';
|
|
1250
|
+
|
|
1251
|
+
@state()
|
|
1252
|
+
private accessor newPassword = '';
|
|
1253
|
+
|
|
1254
|
+
@state()
|
|
1255
|
+
private accessor confirmPassword = '';
|
|
1256
|
+
|
|
1257
|
+
@state()
|
|
1258
|
+
private accessor credentialError = '';
|
|
1259
|
+
|
|
1260
|
+
@state()
|
|
1261
|
+
private accessor orgSettingsOrgId = '';
|
|
1262
|
+
|
|
1263
|
+
@state()
|
|
1264
|
+
private accessor orgNameDraft = '';
|
|
1265
|
+
|
|
1266
|
+
@state()
|
|
1267
|
+
private accessor orgSlugDraft = '';
|
|
1268
|
+
|
|
1269
|
+
@state()
|
|
1270
|
+
private accessor orgSettingsConfirmation = '';
|
|
1271
|
+
|
|
1272
|
+
@state()
|
|
1273
|
+
private accessor transferOwnerId = '';
|
|
1274
|
+
|
|
1275
|
+
@state()
|
|
1276
|
+
private accessor transferConfirmation = '';
|
|
1277
|
+
|
|
1278
|
+
@state()
|
|
1279
|
+
private accessor deleteConfirmation = '';
|
|
1280
|
+
|
|
1281
|
+
@state()
|
|
1282
|
+
private accessor orgSettingsError = '';
|
|
1283
|
+
|
|
1284
|
+
@state()
|
|
1285
|
+
private accessor dialogMode: 'none' | 'role-upsert' | 'role-delete' | 'member-roles' | 'app-role-mappings' = 'none';
|
|
1286
|
+
|
|
1287
|
+
@state()
|
|
1288
|
+
private accessor dialogError = '';
|
|
1289
|
+
|
|
1290
|
+
@state()
|
|
1291
|
+
private accessor dialogRoleKey = '';
|
|
1292
|
+
|
|
1293
|
+
@state()
|
|
1294
|
+
private accessor dialogRoleName = '';
|
|
1295
|
+
|
|
1296
|
+
@state()
|
|
1297
|
+
private accessor dialogRoleDescription = '';
|
|
1298
|
+
|
|
1299
|
+
@state()
|
|
1300
|
+
private accessor dialogRoleDeleteConfirmation = '';
|
|
1301
|
+
|
|
1302
|
+
@state()
|
|
1303
|
+
private accessor dialogMember: IIdpAdminMember | null = null;
|
|
1304
|
+
|
|
1305
|
+
@state()
|
|
1306
|
+
private accessor dialogMemberRoles: string[] = [];
|
|
1307
|
+
|
|
1308
|
+
@state()
|
|
1309
|
+
private accessor dialogApp: IIdpAdminApp | null = null;
|
|
1310
|
+
|
|
1311
|
+
@state()
|
|
1312
|
+
private accessor dialogAppMappings: IIdpAdminAppRoleMapping[] = [];
|
|
1313
|
+
|
|
1314
|
+
private workspaceNav: TNavItem[] = [
|
|
1315
|
+
{ id: 'overview', label: 'Overview', icon: 'grid' },
|
|
1316
|
+
];
|
|
1317
|
+
|
|
1318
|
+
private accountNav: TNavItem[] = [
|
|
1319
|
+
{ id: 'profile', label: 'Profile', icon: 'user' },
|
|
1320
|
+
{ id: 'security', label: 'Security', icon: 'shield' },
|
|
1321
|
+
{ id: 'sessions', label: 'Sessions & Devices', icon: 'monitor' },
|
|
1322
|
+
{ id: 'apps', label: 'Connected Apps', icon: 'grid' },
|
|
1323
|
+
];
|
|
1324
|
+
|
|
1325
|
+
private orgNav: TNavItem[] = [
|
|
1326
|
+
{ id: 'org-general', label: 'General', icon: 'building' },
|
|
1327
|
+
{ id: 'org-settings', label: 'Settings', icon: 'settings' },
|
|
1328
|
+
{ id: 'org-members', label: 'Members', icon: 'users' },
|
|
1329
|
+
{ id: 'org-apps', label: 'OAuth Apps', icon: 'box' },
|
|
1330
|
+
];
|
|
1331
|
+
|
|
1332
|
+
private supportNav: TNavItem[] = [
|
|
1333
|
+
{ id: 'support', label: 'Support', icon: 'shield' },
|
|
1334
|
+
];
|
|
1335
|
+
|
|
1336
|
+
private adminNav: TNavItem[] = [
|
|
1337
|
+
{ id: 'ga-users', label: 'All Users', icon: 'users' },
|
|
1338
|
+
{ id: 'ga-orgs', label: 'All Organisations', icon: 'building' },
|
|
1339
|
+
{ id: 'ga-apps', label: 'Platform Apps', icon: 'globe' },
|
|
1340
|
+
];
|
|
1341
|
+
|
|
1342
|
+
private get safeOrgs(): IIdpAdminOrg[] {
|
|
1343
|
+
return Array.isArray(this.orgs) ? this.orgs.filter(Boolean) : [];
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
private get currentOrg(): IIdpAdminOrg {
|
|
1347
|
+
const orgs = this.safeOrgs;
|
|
1348
|
+
return orgs.find((orgArg) => orgArg.id === this.selectedOrgId) || orgs[0] || {
|
|
1349
|
+
id: '',
|
|
1350
|
+
name: 'No organisation',
|
|
1351
|
+
slug: '',
|
|
1352
|
+
myRole: '',
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
private get userInitials(): string {
|
|
1357
|
+
if (this.user?.initials) {
|
|
1358
|
+
return this.user.initials;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const source = this.user?.name || this.user?.email || '?';
|
|
1362
|
+
return source
|
|
1363
|
+
.split(/\s+|@/)
|
|
1364
|
+
.filter(Boolean)
|
|
1365
|
+
.map((partArg) => partArg[0])
|
|
1366
|
+
.slice(0, 2)
|
|
1367
|
+
.join('')
|
|
1368
|
+
.toUpperCase();
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
private getInitials(valueArg?: string): string {
|
|
1372
|
+
const source = valueArg || '?';
|
|
1373
|
+
return source
|
|
1374
|
+
.split(/\s+|@|-|\./)
|
|
1375
|
+
.filter(Boolean)
|
|
1376
|
+
.map((partArg) => partArg[0])
|
|
1377
|
+
.slice(0, 2)
|
|
1378
|
+
.join('')
|
|
1379
|
+
.toUpperCase();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
private setPasswordField(fieldArg: 'current' | 'new' | 'confirm', eventArg: Event) {
|
|
1383
|
+
const value = (eventArg.target as HTMLInputElement).value;
|
|
1384
|
+
if (fieldArg === 'current') this.currentPassword = value;
|
|
1385
|
+
if (fieldArg === 'new') this.newPassword = value;
|
|
1386
|
+
if (fieldArg === 'confirm') this.confirmPassword = value;
|
|
1387
|
+
this.credentialError = '';
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
private submitPasswordChange() {
|
|
1391
|
+
if (!this.currentPassword || !this.newPassword || !this.confirmPassword) {
|
|
1392
|
+
this.credentialError = 'Enter your current password and confirm the new password.';
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (this.newPassword.length < 12) {
|
|
1397
|
+
this.credentialError = 'Use at least 12 characters for the new password.';
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (this.newPassword !== this.confirmPassword) {
|
|
1402
|
+
this.credentialError = 'New password and confirmation do not match.';
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
this.dispatchShellEvent<IIdpAdminPasswordChangeEventDetail>('idp-admin-password-change', {
|
|
1407
|
+
currentPassword: this.currentPassword,
|
|
1408
|
+
newPassword: this.newPassword,
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
private syncOrgSettingsState() {
|
|
1413
|
+
const org = this.currentOrg;
|
|
1414
|
+
if (this.orgSettingsOrgId === org.id) {
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
this.orgSettingsOrgId = org.id;
|
|
1419
|
+
this.orgNameDraft = org.name || '';
|
|
1420
|
+
this.orgSlugDraft = org.slug || '';
|
|
1421
|
+
this.orgSettingsConfirmation = '';
|
|
1422
|
+
this.transferOwnerId = '';
|
|
1423
|
+
this.transferConfirmation = '';
|
|
1424
|
+
this.deleteConfirmation = '';
|
|
1425
|
+
this.orgSettingsError = '';
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
private setOrgSettingsField(
|
|
1429
|
+
fieldArg: 'name' | 'slug' | 'settingsConfirmation' | 'transferOwnerId' | 'transferConfirmation' | 'deleteConfirmation',
|
|
1430
|
+
eventArg: Event
|
|
1431
|
+
) {
|
|
1432
|
+
const value = (eventArg.target as HTMLInputElement | HTMLSelectElement).value;
|
|
1433
|
+
if (fieldArg === 'name') this.orgNameDraft = value;
|
|
1434
|
+
if (fieldArg === 'slug') this.orgSlugDraft = value.trim().toLowerCase();
|
|
1435
|
+
if (fieldArg === 'settingsConfirmation') this.orgSettingsConfirmation = value;
|
|
1436
|
+
if (fieldArg === 'transferOwnerId') this.transferOwnerId = value;
|
|
1437
|
+
if (fieldArg === 'transferConfirmation') this.transferConfirmation = value;
|
|
1438
|
+
if (fieldArg === 'deleteConfirmation') this.deleteConfirmation = value;
|
|
1439
|
+
this.orgSettingsError = '';
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
private submitOrgSettingsUpdate() {
|
|
1443
|
+
const org = this.currentOrg;
|
|
1444
|
+
const name = this.orgNameDraft.trim();
|
|
1445
|
+
const slug = this.orgSlugDraft.trim().toLowerCase();
|
|
1446
|
+
if (!org.id) {
|
|
1447
|
+
this.orgSettingsError = 'Select an organisation before updating settings.';
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if (!name || !slug) {
|
|
1451
|
+
this.orgSettingsError = 'Organisation name and slug are required.';
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (name === org.name && slug === org.slug) {
|
|
1455
|
+
this.orgSettingsError = 'Change the organisation name or slug before applying settings.';
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (this.orgSettingsConfirmation.trim() !== org.slug) {
|
|
1459
|
+
this.orgSettingsError = `Type ${org.slug} to confirm organisation settings changes.`;
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
this.dispatchShellEvent<IIdpAdminOrgUpdateEventDetail>('idp-admin-org-update', {
|
|
1464
|
+
organizationId: org.id,
|
|
1465
|
+
name,
|
|
1466
|
+
slug,
|
|
1467
|
+
confirmationText: this.orgSettingsConfirmation.trim(),
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
private submitOrgTransfer() {
|
|
1472
|
+
const org = this.currentOrg;
|
|
1473
|
+
const expectedText = `transfer ${org.slug}`;
|
|
1474
|
+
if (!org.id) {
|
|
1475
|
+
this.orgSettingsError = 'Select an organisation before transferring ownership.';
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (!this.transferOwnerId) {
|
|
1479
|
+
this.orgSettingsError = 'Select the member who should become the new owner.';
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (this.transferConfirmation.trim() !== expectedText) {
|
|
1483
|
+
this.orgSettingsError = `Type ${expectedText} to confirm ownership transfer.`;
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
this.dispatchShellEvent<IIdpAdminOrgTransferEventDetail>('idp-admin-org-transfer', {
|
|
1488
|
+
organizationId: org.id,
|
|
1489
|
+
newOwnerId: this.transferOwnerId,
|
|
1490
|
+
confirmationText: this.transferConfirmation.trim(),
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private submitOrgDelete() {
|
|
1495
|
+
const org = this.currentOrg;
|
|
1496
|
+
const expectedText = `delete ${org.slug}`;
|
|
1497
|
+
if (!org.id) {
|
|
1498
|
+
this.orgSettingsError = 'Select an organisation before deleting it.';
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (this.deleteConfirmation.trim() !== expectedText) {
|
|
1502
|
+
this.orgSettingsError = `Type ${expectedText} to confirm organisation deletion.`;
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
this.dispatchShellEvent<IIdpAdminOrgDeleteEventDetail>('idp-admin-org-delete', {
|
|
1507
|
+
organizationId: org.id,
|
|
1508
|
+
confirmationText: this.deleteConfirmation.trim(),
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
private requestPassportEnrollment() {
|
|
1513
|
+
const fallbackLabel = typeof navigator !== 'undefined'
|
|
1514
|
+
? navigator.userAgent.includes('Mobile') ? 'Mobile passport device' : 'Desktop passport device'
|
|
1515
|
+
: 'Passport device';
|
|
1516
|
+
const deviceLabel = globalThis.prompt?.('Device label', fallbackLabel)?.trim();
|
|
1517
|
+
if (!deviceLabel) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
this.dispatchShellEvent<IIdpAdminPassportEnrollmentEventDetail>('idp-admin-passport-enroll', {
|
|
1522
|
+
deviceLabel,
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
private setPage(pageArg: TIdpAdminPage) {
|
|
1527
|
+
this.page = pageArg;
|
|
1528
|
+
this.orgMenuOpen = false;
|
|
1529
|
+
this.dispatchEvent(new CustomEvent<IIdpAdminNavigateEventDetail>('idp-admin-navigate', {
|
|
1530
|
+
detail: { page: pageArg },
|
|
1531
|
+
bubbles: true,
|
|
1532
|
+
composed: true,
|
|
1533
|
+
}));
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
private selectOrg(orgIdArg: string) {
|
|
1537
|
+
this.selectedOrgId = orgIdArg;
|
|
1538
|
+
this.orgMenuOpen = false;
|
|
1539
|
+
this.page = 'org-general';
|
|
1540
|
+
this.dispatchEvent(new CustomEvent<IIdpAdminOrgSelectEventDetail>('idp-admin-org-select', {
|
|
1541
|
+
detail: {
|
|
1542
|
+
orgId: orgIdArg,
|
|
1543
|
+
org: this.orgs.find((orgArg) => orgArg.id === orgIdArg) || null,
|
|
1544
|
+
},
|
|
1545
|
+
bubbles: true,
|
|
1546
|
+
composed: true,
|
|
1547
|
+
}));
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
private requestOrgCreate() {
|
|
1551
|
+
this.orgMenuOpen = false;
|
|
1552
|
+
this.dispatchEvent(new CustomEvent('idp-admin-org-create', {
|
|
1553
|
+
bubbles: true,
|
|
1554
|
+
composed: true,
|
|
1555
|
+
}));
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
private dispatchShellEvent<TDetail>(eventNameArg: string, detailArg: TDetail) {
|
|
1559
|
+
this.dispatchEvent(new CustomEvent<TDetail>(eventNameArg, {
|
|
1560
|
+
detail: detailArg,
|
|
1561
|
+
bubbles: true,
|
|
1562
|
+
composed: true,
|
|
1563
|
+
}));
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
private formatTimeAgo(timestampArg?: number): string {
|
|
1567
|
+
if (!timestampArg) {
|
|
1568
|
+
return 'unknown';
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const diff = Date.now() - timestampArg;
|
|
1572
|
+
const minutes = Math.floor(diff / 60000);
|
|
1573
|
+
const hours = Math.floor(diff / 3600000);
|
|
1574
|
+
const days = Math.floor(diff / 86400000);
|
|
1575
|
+
|
|
1576
|
+
if (minutes < 1) return 'just now';
|
|
1577
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1578
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1579
|
+
if (days < 7) return `${days}d ago`;
|
|
1580
|
+
return new Date(timestampArg).toLocaleDateString();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
private renderStateCard(titleArg: string, descriptionArg: string, iconArg = 'box'): TemplateResult {
|
|
1584
|
+
return html`
|
|
1585
|
+
<div class="state-card">
|
|
1586
|
+
<span class="state-icon"><idp-icon name=${iconArg as any} size="17"></idp-icon></span>
|
|
1587
|
+
<div class="state-title">${titleArg}</div>
|
|
1588
|
+
<div class="state-description">${descriptionArg}</div>
|
|
1589
|
+
</div>
|
|
1590
|
+
`;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
private renderDataState(emptyTitleArg: string, emptyDescriptionArg: string, iconArg = 'box'): TemplateResult | null {
|
|
1594
|
+
if (this.dataLoading) {
|
|
1595
|
+
return this.renderStateCard('Loading data', 'Fetching the latest account and organisation data.', 'cloud');
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (this.dataError) {
|
|
1599
|
+
return this.renderStateCard('Data unavailable', this.dataError, 'alert');
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
return this.renderStateCard(emptyTitleArg, emptyDescriptionArg, iconArg);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
private roleVariant(roleArg: string): 'default' | 'accent' | 'ok' | 'warn' | 'error' | 'outline' {
|
|
1606
|
+
if (roleArg === 'owner') return 'accent';
|
|
1607
|
+
if (roleArg === 'admin') return 'warn';
|
|
1608
|
+
if (roleArg === 'editor') return 'ok';
|
|
1609
|
+
if (roleArg === 'outlaw') return 'error';
|
|
1610
|
+
return 'outline';
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
private get platformRoles(): IIdpAdminOrgRoleDefinition[] {
|
|
1614
|
+
return [
|
|
1615
|
+
{ key: 'owner', name: 'Owner', description: 'Protected idp.global owner role.' },
|
|
1616
|
+
{ key: 'admin', name: 'Admin', description: 'Protected idp.global admin role.' },
|
|
1617
|
+
{ key: 'editor', name: 'Editor', description: 'Standard organisation role.' },
|
|
1618
|
+
{ key: 'viewer', name: 'Viewer', description: 'Standard organisation role.' },
|
|
1619
|
+
{ key: 'guest', name: 'Guest', description: 'Standard organisation role.' },
|
|
1620
|
+
{ key: 'outlaw', name: 'Outlaw', description: 'Restricted organisation role.' },
|
|
1621
|
+
];
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
private get availableOrgRoles(): IIdpAdminOrgRoleDefinition[] {
|
|
1625
|
+
const customRoles = this.orgRoleDefinitions || [];
|
|
1626
|
+
const customRoleKeys = new Set(customRoles.map((roleArg) => roleArg.key));
|
|
1627
|
+
return [
|
|
1628
|
+
...this.platformRoles.filter((roleArg) => !customRoleKeys.has(roleArg.key)),
|
|
1629
|
+
...customRoles,
|
|
1630
|
+
];
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
private parseCsv(valueArg: string): string[] {
|
|
1634
|
+
return [...new Set(valueArg.split(',').map((entryArg) => entryArg.trim()).filter(Boolean))];
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
private normalizeRoleKey(valueArg: string): string {
|
|
1638
|
+
return valueArg.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
private closeDialog() {
|
|
1642
|
+
this.dialogMode = 'none';
|
|
1643
|
+
this.dialogError = '';
|
|
1644
|
+
this.dialogRoleKey = '';
|
|
1645
|
+
this.dialogRoleName = '';
|
|
1646
|
+
this.dialogRoleDescription = '';
|
|
1647
|
+
this.dialogRoleDeleteConfirmation = '';
|
|
1648
|
+
this.dialogMember = null;
|
|
1649
|
+
this.dialogMemberRoles = [];
|
|
1650
|
+
this.dialogApp = null;
|
|
1651
|
+
this.dialogAppMappings = [];
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
private openRoleUpsertDialog(roleArg?: IIdpAdminOrgRoleDefinition) {
|
|
1655
|
+
this.dialogMode = 'role-upsert';
|
|
1656
|
+
this.dialogError = '';
|
|
1657
|
+
this.dialogRoleKey = roleArg?.key || '';
|
|
1658
|
+
this.dialogRoleName = roleArg?.name || '';
|
|
1659
|
+
this.dialogRoleDescription = roleArg?.description || '';
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
private openRoleDeleteDialog(roleArg: IIdpAdminOrgRoleDefinition) {
|
|
1663
|
+
this.dialogMode = 'role-delete';
|
|
1664
|
+
this.dialogError = '';
|
|
1665
|
+
this.dialogRoleKey = roleArg.key;
|
|
1666
|
+
this.dialogRoleName = roleArg.name;
|
|
1667
|
+
this.dialogRoleDescription = roleArg.description || '';
|
|
1668
|
+
this.dialogRoleDeleteConfirmation = '';
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
private openMemberRolesDialog(memberArg: IIdpAdminMember) {
|
|
1672
|
+
this.dialogMode = 'member-roles';
|
|
1673
|
+
this.dialogError = '';
|
|
1674
|
+
this.dialogMember = memberArg;
|
|
1675
|
+
this.dialogMemberRoles = [...memberArg.roles];
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
private openAppRoleMappingsDialog(appArg: IIdpAdminApp) {
|
|
1679
|
+
this.dialogMode = 'app-role-mappings';
|
|
1680
|
+
this.dialogError = '';
|
|
1681
|
+
this.dialogApp = appArg;
|
|
1682
|
+
this.dialogAppMappings = this.availableOrgRoles.map((roleArg) => {
|
|
1683
|
+
const existingMapping = (appArg.roleMappings || []).find((mappingArg) => mappingArg.orgRoleKey === roleArg.key);
|
|
1684
|
+
return {
|
|
1685
|
+
orgRoleKey: roleArg.key,
|
|
1686
|
+
appRoles: [...(existingMapping?.appRoles || [])],
|
|
1687
|
+
permissions: [...(existingMapping?.permissions || [])],
|
|
1688
|
+
scopes: [...(existingMapping?.scopes || [])],
|
|
1689
|
+
};
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
private setDialogRoleField(fieldArg: 'key' | 'name' | 'description' | 'deleteConfirmation', eventArg: Event) {
|
|
1694
|
+
const value = (eventArg.target as HTMLInputElement).value;
|
|
1695
|
+
if (fieldArg === 'key') this.dialogRoleKey = this.normalizeRoleKey(value);
|
|
1696
|
+
if (fieldArg === 'name') {
|
|
1697
|
+
this.dialogRoleName = value;
|
|
1698
|
+
if (!this.dialogRoleKey) {
|
|
1699
|
+
this.dialogRoleKey = this.normalizeRoleKey(value);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (fieldArg === 'description') this.dialogRoleDescription = value;
|
|
1703
|
+
if (fieldArg === 'deleteConfirmation') this.dialogRoleDeleteConfirmation = value;
|
|
1704
|
+
this.dialogError = '';
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
private toggleDialogMemberRole(roleKeyArg: string, checkedArg: boolean) {
|
|
1708
|
+
const nextRoles = new Set(this.dialogMemberRoles);
|
|
1709
|
+
if (checkedArg) {
|
|
1710
|
+
nextRoles.add(roleKeyArg);
|
|
1711
|
+
} else {
|
|
1712
|
+
nextRoles.delete(roleKeyArg);
|
|
1713
|
+
}
|
|
1714
|
+
this.dialogMemberRoles = [...nextRoles];
|
|
1715
|
+
this.dialogError = '';
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
private setDialogMappingField(roleKeyArg: string, fieldArg: 'appRoles' | 'permissions' | 'scopes', eventArg: Event) {
|
|
1719
|
+
const value = (eventArg.target as HTMLInputElement).value;
|
|
1720
|
+
this.dialogAppMappings = this.dialogAppMappings.map((mappingArg) => mappingArg.orgRoleKey === roleKeyArg
|
|
1721
|
+
? { ...mappingArg, [fieldArg]: this.parseCsv(value) }
|
|
1722
|
+
: mappingArg
|
|
1723
|
+
);
|
|
1724
|
+
this.dialogError = '';
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
private submitRoleUpsertDialog() {
|
|
1728
|
+
const key = this.normalizeRoleKey(this.dialogRoleKey);
|
|
1729
|
+
const name = this.dialogRoleName.trim();
|
|
1730
|
+
if (!key || !name) {
|
|
1731
|
+
this.dialogError = 'Role key and name are required.';
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
this.dispatchShellEvent<IIdpAdminOrgRoleUpsertEventDetail>('idp-admin-org-role-upsert', {
|
|
1735
|
+
organizationId: this.currentOrg.id,
|
|
1736
|
+
roleDefinition: {
|
|
1737
|
+
key,
|
|
1738
|
+
name,
|
|
1739
|
+
description: this.dialogRoleDescription.trim(),
|
|
1740
|
+
},
|
|
1741
|
+
});
|
|
1742
|
+
this.closeDialog();
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
private submitRoleDeleteDialog() {
|
|
1746
|
+
const expectedText = `delete role ${this.dialogRoleKey}`;
|
|
1747
|
+
if (this.dialogRoleDeleteConfirmation.trim() !== expectedText) {
|
|
1748
|
+
this.dialogError = `Type ${expectedText} to confirm role deletion.`;
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
this.dispatchShellEvent<IIdpAdminOrgRoleDeleteEventDetail>('idp-admin-org-role-delete', {
|
|
1752
|
+
organizationId: this.currentOrg.id,
|
|
1753
|
+
roleKey: this.dialogRoleKey,
|
|
1754
|
+
confirmationText: this.dialogRoleDeleteConfirmation.trim(),
|
|
1755
|
+
});
|
|
1756
|
+
this.closeDialog();
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
private submitMemberRolesDialog() {
|
|
1760
|
+
if (!this.dialogMember) return;
|
|
1761
|
+
if (!this.dialogMemberRoles.length) {
|
|
1762
|
+
this.dialogError = 'At least one role is required.';
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
this.dispatchShellEvent<IIdpAdminMemberRolesEventDetail>('idp-admin-member-roles-update', {
|
|
1766
|
+
userId: this.dialogMember.userId,
|
|
1767
|
+
roles: this.dialogMemberRoles,
|
|
1768
|
+
});
|
|
1769
|
+
this.closeDialog();
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
private submitAppRoleMappingsDialog() {
|
|
1773
|
+
if (!this.dialogApp) return;
|
|
1774
|
+
const roleMappings = this.dialogAppMappings
|
|
1775
|
+
.map((mappingArg) => ({
|
|
1776
|
+
orgRoleKey: mappingArg.orgRoleKey,
|
|
1777
|
+
appRoles: [...mappingArg.appRoles],
|
|
1778
|
+
permissions: [...mappingArg.permissions],
|
|
1779
|
+
scopes: [...mappingArg.scopes],
|
|
1780
|
+
}))
|
|
1781
|
+
.filter((mappingArg) => mappingArg.appRoles.length || mappingArg.permissions.length || mappingArg.scopes.length);
|
|
1782
|
+
const allowedScopes = this.dialogApp.scopes || [];
|
|
1783
|
+
const invalidScopes = roleMappings.flatMap((mappingArg) => mappingArg.scopes).filter((scopeArg) => !allowedScopes.includes(scopeArg));
|
|
1784
|
+
if (invalidScopes.length) {
|
|
1785
|
+
this.dialogError = `Unsupported app scopes: ${[...new Set(invalidScopes)].join(', ')}.`;
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
this.dispatchShellEvent<IIdpAdminAppRoleMappingsEventDetail>('idp-admin-app-role-mappings-update', {
|
|
1789
|
+
organizationId: this.currentOrg.id,
|
|
1790
|
+
appId: this.dialogApp.id,
|
|
1791
|
+
roleMappings,
|
|
1792
|
+
});
|
|
1793
|
+
this.closeDialog();
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
private renderDialogError(): TemplateResult {
|
|
1797
|
+
return this.dialogError
|
|
1798
|
+
? html`<div class="notice-card" style="border-color:var(--idp-error-border);background:var(--idp-error-bg)"><idp-icon name="alert" size="16" style="color:var(--idp-error)"></idp-icon><div><div class="section-title" style="color:var(--idp-error)">Action blocked</div><div class="muted">${this.dialogError}</div></div></div>`
|
|
1799
|
+
: html``;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
private renderRoleUpsertDialog(): TemplateResult {
|
|
1803
|
+
const isEdit = this.orgRoleDefinitions.some((roleArg) => roleArg.key === this.dialogRoleKey);
|
|
1804
|
+
return html`
|
|
1805
|
+
<div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
1806
|
+
<div class="dialog-head">
|
|
1807
|
+
<div><div class="dialog-title">${isEdit ? 'Edit custom role' : 'Add custom role'}</div><div class="muted">Custom roles model organisation-specific business access.</div></div>
|
|
1808
|
+
<button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
|
|
1809
|
+
</div>
|
|
1810
|
+
<div class="dialog-body">
|
|
1811
|
+
${this.renderDialogError()}
|
|
1812
|
+
${this.renderFormRow('Role name', 'Readable label shown to admins.', html`<input class="input" .value=${this.dialogRoleName} @input=${(eventArg: Event) => this.setDialogRoleField('name', eventArg)} />`, true)}
|
|
1813
|
+
${this.renderFormRow('Role key', 'Lowercase stable identifier used in assignments and app mappings.', html`<input class="input" .value=${this.dialogRoleKey} @input=${(eventArg: Event) => this.setDialogRoleField('key', eventArg)} ?disabled=${isEdit} />`, true)}
|
|
1814
|
+
${this.renderFormRow('Description', 'Optional admin note describing when this role should be used.', html`<textarea class="textarea" .value=${this.dialogRoleDescription} @input=${(eventArg: Event) => this.setDialogRoleField('description', eventArg)}></textarea>`)}
|
|
1815
|
+
</div>
|
|
1816
|
+
<div class="dialog-actions">
|
|
1817
|
+
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
1818
|
+
<button class="plain-button primary" @click=${() => this.submitRoleUpsertDialog()}>Save role</button>
|
|
1819
|
+
</div>
|
|
1820
|
+
</div>
|
|
1821
|
+
`;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
private renderRoleDeleteDialog(): TemplateResult {
|
|
1825
|
+
const expectedText = `delete role ${this.dialogRoleKey}`;
|
|
1826
|
+
return html`
|
|
1827
|
+
<div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
1828
|
+
<div class="dialog-head">
|
|
1829
|
+
<div><div class="dialog-title">Delete custom role</div><div class="muted">This removes the role from member assignments and app mappings after backend confirmation.</div></div>
|
|
1830
|
+
<button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
|
|
1831
|
+
</div>
|
|
1832
|
+
<div class="dialog-body">
|
|
1833
|
+
${this.renderDialogError()}
|
|
1834
|
+
<div class="notice-card" style="border-color:var(--idp-error-border);background:var(--idp-error-bg)"><idp-icon name="alert" size="16" style="color:var(--idp-error)"></idp-icon><div><div class="section-title" style="color:var(--idp-error)">${this.dialogRoleName}</div><div class="muted">Type <span class="mono">${expectedText}</span> to confirm deletion.</div></div></div>
|
|
1835
|
+
${this.renderFormRow('Confirmation', `Type ${expectedText}`, html`<input class="input" .value=${this.dialogRoleDeleteConfirmation} @input=${(eventArg: Event) => this.setDialogRoleField('deleteConfirmation', eventArg)} />`, true)}
|
|
1836
|
+
</div>
|
|
1837
|
+
<div class="dialog-actions">
|
|
1838
|
+
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
1839
|
+
<button class="plain-button destructive" @click=${() => this.submitRoleDeleteDialog()}>Delete role</button>
|
|
1840
|
+
</div>
|
|
1841
|
+
</div>
|
|
1842
|
+
`;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
private renderMemberRolesDialog(): TemplateResult {
|
|
1846
|
+
const member = this.dialogMember;
|
|
1847
|
+
return html`
|
|
1848
|
+
<div class="dialog-card" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
1849
|
+
<div class="dialog-head">
|
|
1850
|
+
<div><div class="dialog-title">Edit member roles</div><div class="muted">${member?.name || member?.email || 'Member'} receives the selected organisation roles.</div></div>
|
|
1851
|
+
<button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
|
|
1852
|
+
</div>
|
|
1853
|
+
<div class="dialog-body">
|
|
1854
|
+
${this.renderDialogError()}
|
|
1855
|
+
<div class="role-grid">
|
|
1856
|
+
${this.availableOrgRoles.map((roleArg) => {
|
|
1857
|
+
const checked = this.dialogMemberRoles.includes(roleArg.key);
|
|
1858
|
+
return html`
|
|
1859
|
+
<label class="role-option">
|
|
1860
|
+
<input type="checkbox" .checked=${checked} @change=${(eventArg: Event) => this.toggleDialogMemberRole(roleArg.key, (eventArg.target as HTMLInputElement).checked)} />
|
|
1861
|
+
<span><span class="section-title">${roleArg.name}</span><span class="muted" style="display:block">${roleArg.description || roleArg.key}</span></span>
|
|
1862
|
+
</label>
|
|
1863
|
+
`;
|
|
1864
|
+
})}
|
|
1865
|
+
</div>
|
|
1866
|
+
</div>
|
|
1867
|
+
<div class="dialog-actions">
|
|
1868
|
+
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
1869
|
+
<button class="plain-button primary" @click=${() => this.submitMemberRolesDialog()}>Save roles</button>
|
|
1870
|
+
</div>
|
|
1871
|
+
</div>
|
|
1872
|
+
`;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
private renderAppRoleMappingsDialog(): TemplateResult {
|
|
1876
|
+
const app = this.dialogApp;
|
|
1877
|
+
return html`
|
|
1878
|
+
<div class="dialog-card wide" @click=${(eventArg: Event) => eventArg.stopPropagation()}>
|
|
1879
|
+
<div class="dialog-head">
|
|
1880
|
+
<div><div class="dialog-title">Map organisation roles</div><div class="muted">Map ${this.currentOrg.name} roles to app-specific roles, permissions, and OAuth scopes for ${app?.name || 'this app'}.</div></div>
|
|
1881
|
+
<button class="plain-button ghost" @click=${() => this.closeDialog()}>Close</button>
|
|
1882
|
+
</div>
|
|
1883
|
+
<div class="dialog-body">
|
|
1884
|
+
${this.renderDialogError()}
|
|
1885
|
+
${app?.scopes?.length ? html`<div class="muted">Available OAuth scopes: <span class="mono">${app.scopes.join(', ')}</span></div>` : html`<div class="muted">This app has no declared OAuth scopes. Role and permission mappings are still supported.</div>`}
|
|
1886
|
+
${this.dialogAppMappings.map((mappingArg) => {
|
|
1887
|
+
const role = this.availableOrgRoles.find((roleArg) => roleArg.key === mappingArg.orgRoleKey);
|
|
1888
|
+
return html`
|
|
1889
|
+
<div class="mapping-row">
|
|
1890
|
+
<div class="mapping-role">${role?.name || mappingArg.orgRoleKey}<span class="muted">${mappingArg.orgRoleKey}</span></div>
|
|
1891
|
+
<div>${this.renderFormRow('App roles', '', html`<input class="input" .value=${mappingArg.appRoles.join(', ')} @input=${(eventArg: Event) => this.setDialogMappingField(mappingArg.orgRoleKey, 'appRoles', eventArg)} />`)}</div>
|
|
1892
|
+
<div>${this.renderFormRow('Permissions', '', html`<input class="input" .value=${mappingArg.permissions.join(', ')} @input=${(eventArg: Event) => this.setDialogMappingField(mappingArg.orgRoleKey, 'permissions', eventArg)} />`)}</div>
|
|
1893
|
+
<div>${this.renderFormRow('Scopes', '', html`<input class="input" .value=${mappingArg.scopes.join(', ')} @input=${(eventArg: Event) => this.setDialogMappingField(mappingArg.orgRoleKey, 'scopes', eventArg)} />`)}</div>
|
|
1894
|
+
</div>
|
|
1895
|
+
`;
|
|
1896
|
+
})}
|
|
1897
|
+
</div>
|
|
1898
|
+
<div class="dialog-actions">
|
|
1899
|
+
<button class="plain-button outline" @click=${() => this.closeDialog()}>Cancel</button>
|
|
1900
|
+
<button class="plain-button primary" @click=${() => this.submitAppRoleMappingsDialog()}>Save mappings</button>
|
|
1901
|
+
</div>
|
|
1902
|
+
</div>
|
|
1903
|
+
`;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
private renderDialog(): TemplateResult {
|
|
1907
|
+
if (this.dialogMode === 'none') {
|
|
1908
|
+
return html``;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const dialog = this.dialogMode === 'role-upsert'
|
|
1912
|
+
? this.renderRoleUpsertDialog()
|
|
1913
|
+
: this.dialogMode === 'role-delete'
|
|
1914
|
+
? this.renderRoleDeleteDialog()
|
|
1915
|
+
: this.dialogMode === 'member-roles'
|
|
1916
|
+
? this.renderMemberRolesDialog()
|
|
1917
|
+
: this.renderAppRoleMappingsDialog();
|
|
1918
|
+
return html`<div class="dialog-backdrop" @click=${() => this.closeDialog()}>${dialog}</div>`;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
private renderNavGroup(items: TNavItem[], active = ''): TemplateResult {
|
|
1922
|
+
return html`
|
|
1923
|
+
<div class="nav-list">
|
|
1924
|
+
${items.map((item) => html`
|
|
1925
|
+
<button class="nav-item ${item.id === active ? 'active' : ''}" @click=${() => this.setPage(item.id)}>
|
|
1926
|
+
<idp-icon name=${item.icon as any} size="14"></idp-icon>
|
|
1927
|
+
${item.label}
|
|
1928
|
+
${item.badge ? html`<idp-badge variant="accent" style="margin-left:auto">${item.badge}</idp-badge>` : html``}
|
|
1929
|
+
</button>
|
|
1930
|
+
`)}
|
|
1931
|
+
</div>
|
|
1932
|
+
`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
private renderSidebar(): TemplateResult {
|
|
1936
|
+
const currentOrg = this.currentOrg;
|
|
1937
|
+
return html`
|
|
1938
|
+
<aside class="sidebar">
|
|
1939
|
+
<div class="logo-block">
|
|
1940
|
+
<div class="logo">
|
|
1941
|
+
<div class="logo-icon" aria-hidden="true">
|
|
1942
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1943
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
|
1944
|
+
</svg>
|
|
1945
|
+
</div>
|
|
1946
|
+
<span class="logo-text">idp.global</span>
|
|
1947
|
+
</div>
|
|
1948
|
+
</div>
|
|
1949
|
+
<div class="nav-wrap">
|
|
1950
|
+
<div class="nav-section"><div class="nav-title">Workspace</div>${this.renderNavGroup(this.workspaceNav, this.page)}</div>
|
|
1951
|
+
<div class="nav-section"><div class="nav-title">My Account</div>${this.renderNavGroup(this.accountNav, this.page)}</div>
|
|
1952
|
+
<div class="nav-section">
|
|
1953
|
+
<div class="nav-title">Organisation</div>
|
|
1954
|
+
<div class="org-switch-wrap">
|
|
1955
|
+
<button class="org-switch ${this.orgMenuOpen ? 'open' : ''}" @click=${() => { this.orgMenuOpen = !this.orgMenuOpen; }}>
|
|
1956
|
+
<span class="org-avatar">${currentOrg.name.slice(0, 2).toUpperCase()}</span>
|
|
1957
|
+
<span class="org-name">${currentOrg.name}</span>
|
|
1958
|
+
<idp-icon name="chevron-down" size="10"></idp-icon>
|
|
1959
|
+
</button>
|
|
1960
|
+
${this.orgMenuOpen ? html`
|
|
1961
|
+
<div class="org-menu">
|
|
1962
|
+
<div class="org-menu-title">Switch organisation</div>
|
|
1963
|
+
${this.safeOrgs.map((orgArg) => html`
|
|
1964
|
+
<button class="org-menu-item ${orgArg.id === this.selectedOrgId ? 'selected' : ''}" @click=${() => this.selectOrg(orgArg.id)}>
|
|
1965
|
+
<span class="org-avatar">${orgArg.name.slice(0, 2).toUpperCase()}</span>
|
|
1966
|
+
<span style="flex:1;min-width:0"><span class="org-menu-name">${orgArg.name}</span><span class="org-menu-role">${orgArg.myRole || 'member'}</span></span>
|
|
1967
|
+
${orgArg.id === this.selectedOrgId ? html`<idp-icon name="check" size="11"></idp-icon>` : html``}
|
|
1968
|
+
</button>
|
|
1969
|
+
`)}
|
|
1970
|
+
<div class="org-menu-divider"></div>
|
|
1971
|
+
<button class="org-create" @click=${() => this.requestOrgCreate()}><span class="org-create-icon"><idp-icon name="plus" size="9"></idp-icon></span>Create organisation</button>
|
|
1972
|
+
</div>
|
|
1973
|
+
` : html``}
|
|
1974
|
+
</div>
|
|
1975
|
+
${this.renderNavGroup(this.orgNav, this.page)}
|
|
1976
|
+
</div>
|
|
1977
|
+
<div class="nav-section"><div class="nav-title">Support</div>${this.renderNavGroup(this.supportNav, this.page)}</div>
|
|
1978
|
+
${this.globalAdmin ? html`<div class="nav-section"><div class="nav-title">Global Admin</div>${this.renderNavGroup(this.adminNav, this.page)}</div>` : html``}
|
|
1979
|
+
</div>
|
|
1980
|
+
<div class="user-footer">
|
|
1981
|
+
<span class="user-avatar">${this.userInitials}</span>
|
|
1982
|
+
<div class="user-meta"><div class="user-name">${this.user?.name || 'Unknown User'}</div><div class="user-email">${this.user?.email || 'No email'}</div></div>
|
|
1983
|
+
</div>
|
|
1984
|
+
</aside>
|
|
1985
|
+
`;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
private renderSparkline(data: number[], color: string): TemplateResult {
|
|
1989
|
+
const max = Math.max(...data);
|
|
1990
|
+
const min = Math.min(...data);
|
|
1991
|
+
const range = max - min || 1;
|
|
1992
|
+
const width = 100;
|
|
1993
|
+
const height = 22;
|
|
1994
|
+
const points = data.map((valueArg, indexArg) => {
|
|
1995
|
+
const x = (indexArg / (data.length - 1)) * width;
|
|
1996
|
+
const y = height - ((valueArg - min) / range) * (height - 4) - 2;
|
|
1997
|
+
return `${x},${y}`;
|
|
1998
|
+
}).join(' ');
|
|
1999
|
+
const area = `0,${height} ${points} ${width},${height}`;
|
|
2000
|
+
|
|
2001
|
+
return html`
|
|
2002
|
+
<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
|
|
2003
|
+
<polygon points=${area} fill=${color} opacity="0.12"></polygon>
|
|
2004
|
+
<polyline points=${points} fill="none" stroke=${color} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></polyline>
|
|
2005
|
+
</svg>
|
|
2006
|
+
`;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
private renderKpi(kpi: TKpi): TemplateResult {
|
|
2010
|
+
return html`
|
|
2011
|
+
<div class="kpi" style="--kpi-accent:${kpi.accent}">
|
|
2012
|
+
<div class="kpi-label">${kpi.label}</div>
|
|
2013
|
+
<div class="kpi-value">${kpi.value}${kpi.unit ? html`<span>${kpi.unit}</span>` : html``}</div>
|
|
2014
|
+
<div class="kpi-sub">${kpi.sub}</div>
|
|
2015
|
+
<div class="kpi-foot">
|
|
2016
|
+
<span class="delta">${kpi.deltaKind === 'live' ? html`<span class="live-dot"></span>` : html``}${kpi.delta}</span>
|
|
2017
|
+
<div class="sparkline">${this.renderSparkline(kpi.spark, kpi.sparkColor)}</div>
|
|
2018
|
+
</div>
|
|
2019
|
+
</div>
|
|
2020
|
+
`;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
private renderPageHeader(titleArg: string, descriptionArg: string, actionArg?: TemplateResult): TemplateResult {
|
|
2024
|
+
return html`
|
|
2025
|
+
<header class="page-head">
|
|
2026
|
+
<div><h1>${titleArg}</h1><div class="lead">${descriptionArg}</div></div>
|
|
2027
|
+
${actionArg ? html`<div class="page-actions">${actionArg}</div>` : html``}
|
|
2028
|
+
</header>
|
|
2029
|
+
`;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
private renderSectionCard(titleArg: string, descriptionArg: string, contentArg: TemplateResult, actionArg?: TemplateResult): TemplateResult {
|
|
2033
|
+
return html`
|
|
2034
|
+
<section class="section-card">
|
|
2035
|
+
${titleArg || actionArg ? html`
|
|
2036
|
+
<div class="section-headline">
|
|
2037
|
+
<div><div class="section-title">${titleArg}</div>${descriptionArg ? html`<div class="section-description">${descriptionArg}</div>` : html``}</div>
|
|
2038
|
+
${actionArg}
|
|
2039
|
+
</div>
|
|
2040
|
+
` : html``}
|
|
2041
|
+
${contentArg}
|
|
2042
|
+
</section>
|
|
2043
|
+
`;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private renderFormRow(labelArg: string, hintArg: string, contentArg: TemplateResult, requiredArg = false): TemplateResult {
|
|
2047
|
+
return html`
|
|
2048
|
+
<div class="form-row">
|
|
2049
|
+
<div><div class="form-label">${labelArg}${requiredArg ? html`<span style="color:var(--idp-error)">*</span>` : html``}</div>${hintArg ? html`<div class="form-hint">${hintArg}</div>` : html``}</div>
|
|
2050
|
+
<div>${contentArg}</div>
|
|
2051
|
+
</div>
|
|
2052
|
+
`;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
private renderCodeBlock(valueArg: string): TemplateResult {
|
|
2056
|
+
return html`<div class="code-block"><code>${valueArg}</code><idp-icon name="copy" size="13"></idp-icon></div>`;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
private renderOverview(): TemplateResult {
|
|
2060
|
+
const firstName = this.user?.name?.split(' ')[0] || 'there';
|
|
2061
|
+
const activeSessionCount = this.sessions.filter((sessionArg) => sessionArg.isCurrent).length || this.sessions.length;
|
|
2062
|
+
const activePassportCount = this.passportDevices.filter((deviceArg) => deviceArg.status === 'active').length;
|
|
2063
|
+
const kpis: TKpi[] = [
|
|
2064
|
+
{
|
|
2065
|
+
label: 'Organisations',
|
|
2066
|
+
value: String(this.orgs.length),
|
|
2067
|
+
delta: 'live',
|
|
2068
|
+
deltaKind: 'live',
|
|
2069
|
+
spark: [1, 1, 1, 1, 1, 1, 1, Math.max(this.orgs.length, 1)],
|
|
2070
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
2071
|
+
accent: 'var(--idp-chart-1)',
|
|
2072
|
+
sub: 'Memberships visible to this account',
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
label: 'Sessions',
|
|
2076
|
+
value: String(this.sessions.length),
|
|
2077
|
+
delta: `${activeSessionCount} active`,
|
|
2078
|
+
deltaKind: 'up',
|
|
2079
|
+
spark: [1, 2, 1, 2, 3, 2, Math.max(this.sessions.length, 1)],
|
|
2080
|
+
sparkColor: 'var(--idp-spark-up)',
|
|
2081
|
+
accent: 'var(--idp-chart-2)',
|
|
2082
|
+
sub: this.sessions.length ? `Latest ${this.formatTimeAgo(Math.max(...this.sessions.map((sessionArg) => sessionArg.lastActive)))}` : 'No active sessions loaded',
|
|
2083
|
+
},
|
|
2084
|
+
{
|
|
2085
|
+
label: 'Passport devices',
|
|
2086
|
+
value: String(this.passportDevices.length),
|
|
2087
|
+
delta: `${activePassportCount} active`,
|
|
2088
|
+
deltaKind: 'live',
|
|
2089
|
+
spark: [1, 1, 1, Math.max(this.passportDevices.length, 1)],
|
|
2090
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
2091
|
+
accent: 'var(--idp-chart-5)',
|
|
2092
|
+
sub: 'Cryptographic credentials',
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
label: 'Activity',
|
|
2096
|
+
value: String(this.activities.length),
|
|
2097
|
+
delta: 'live',
|
|
2098
|
+
deltaKind: 'live',
|
|
2099
|
+
spark: [1, 2, 2, 3, Math.max(this.activities.length, 1)],
|
|
2100
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
2101
|
+
accent: 'var(--idp-info)',
|
|
2102
|
+
sub: 'Recent account events',
|
|
2103
|
+
},
|
|
2104
|
+
];
|
|
2105
|
+
return html`
|
|
2106
|
+
<header class="page-head">
|
|
2107
|
+
<div>
|
|
2108
|
+
<div class="eyebrow-row"><span class="eyebrow">Account - Overview</span><span class="live-pill"><span class="live-dot"></span>live</span></div>
|
|
2109
|
+
<h1>Good morning, ${firstName}.</h1>
|
|
2110
|
+
<div class="lead">Account-wide operational snapshot using live identity data.</div>
|
|
2111
|
+
</div>
|
|
2112
|
+
<div class="page-actions"><button class="plain-button outline" @click=${() => this.setPage('sessions')}>Review sessions</button><button class="plain-button primary" @click=${() => this.setPage('security')}>Manage security</button></div>
|
|
2113
|
+
</header>
|
|
2114
|
+
<div class="body">
|
|
2115
|
+
<div class="kpis">${kpis.map((kpiArg) => this.renderKpi(kpiArg))}</div>
|
|
2116
|
+
${this.dataLoading || this.dataError ? this.renderDataState('No overview data', 'The console has not received live account data yet.', 'activity') : html``}
|
|
2117
|
+
<div class="primary-grid">
|
|
2118
|
+
<div class="card">
|
|
2119
|
+
<div class="card-head"><span class="card-title">Recent activity</span><idp-badge>${this.activities.length} events</idp-badge></div>
|
|
2120
|
+
${this.activities.length ? this.activities.slice(0, 8).map((activityArg) => html`
|
|
2121
|
+
<div class="feed-item"><span class="feed-dot" style="--dot-color:var(--idp-info)"></span><div class="feed-text"><strong>${activityArg.action.replace(/_/g, ' ')}</strong> - ${activityArg.description}</div><span class="feed-meta">${this.formatTimeAgo(activityArg.timestamp)}</span></div>
|
|
2122
|
+
`) : this.renderStateCard('No activity yet', 'Activity events will appear here after logins, app changes, and organisation updates.', 'activity')}
|
|
2123
|
+
</div>
|
|
2124
|
+
<div class="card">
|
|
2125
|
+
<div class="card-head"><span class="card-title">Current sessions</span><idp-badge>${this.sessions.length} total</idp-badge></div>
|
|
2126
|
+
${this.sessions.length ? this.sessions.slice(0, 5).map((sessionArg) => html`
|
|
2127
|
+
<div class="feed-item"><span class="feed-dot" style="--dot-color:${sessionArg.isCurrent ? 'var(--idp-ok)' : 'var(--idp-muted-fg)'}"></span><div class="feed-text"><strong>${sessionArg.deviceName || 'Unknown device'}</strong> - <span class="mono">${sessionArg.browser} ${sessionArg.os}</span></div><span class="feed-meta">${this.formatTimeAgo(sessionArg.lastActive)}</span></div>
|
|
2128
|
+
`) : this.renderStateCard('No sessions loaded', 'Active session telemetry is unavailable or there are no sessions.', 'monitor')}
|
|
2129
|
+
</div>
|
|
2130
|
+
</div>
|
|
2131
|
+
</div>
|
|
2132
|
+
`;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
private renderProfile(): TemplateResult {
|
|
2136
|
+
const username = this.user?.username || this.user?.email?.split('@')[0] || '';
|
|
2137
|
+
const status = this.user?.status || 'active';
|
|
2138
|
+
return html`
|
|
2139
|
+
${this.renderPageHeader('Profile', 'Your personal identity details visible to connected apps.')}
|
|
2140
|
+
<div class="body narrow-body">
|
|
2141
|
+
${this.renderSectionCard('Avatar', 'Shown to apps that request your profile.', html`<div class="split-row" style="justify-content:flex-start"><span class="avatar" style="width:56px;height:56px;font-size:20px">${this.userInitials}</span><div><div class="section-title">${this.user?.name || 'Unknown User'}</div><div class="muted" style="margin-top:5px">Profile update endpoints are not exposed in this console yet.</div></div></div>`)}
|
|
2142
|
+
${this.renderSectionCard('Personal information', '', html`
|
|
2143
|
+
${this.renderFormRow('Full name', '', html`<input class="input" .value=${this.user?.name || ''} disabled />`, true)}
|
|
2144
|
+
${this.renderFormRow('Username', 'Used in your public profile URL', html`<div class="input-group"><span class="input-prefix">idp.global/user/</span><input class="input" .value=${username} disabled /></div>`)}
|
|
2145
|
+
${this.renderFormRow('Email', 'Primary address for login and notifications', html`<input class="input" .value=${this.user?.email || ''} disabled />`)}
|
|
2146
|
+
${this.renderFormRow('Mobile number', 'Used for SMS verification', html`<input class="input" .value=${this.user?.mobileNumber || ''} disabled />`)}
|
|
2147
|
+
`)}
|
|
2148
|
+
${this.renderSectionCard('Account status', '', html`<div class="split-row"><div><div class="section-title">Status</div><div class="muted">Your account is currently ${status}.</div></div><idp-badge variant=${status === 'active' ? 'ok' : status === 'suspended' ? 'error' : 'warn'}>${status}</idp-badge></div>${this.globalAdmin ? html`<div class="divider"></div><div class="split-row"><div><div class="section-title">Global admin</div><div class="muted">You have platform-wide administrative access.</div></div><idp-badge variant="accent">Admin</idp-badge></div>` : html``}`)}
|
|
2149
|
+
</div>
|
|
2150
|
+
`;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
private renderSecurity(): TemplateResult {
|
|
2154
|
+
const passportRows = this.passportDevices.map((deviceArg) => ({
|
|
2155
|
+
cells: [
|
|
2156
|
+
html`
|
|
2157
|
+
<div class="identity-cell">
|
|
2158
|
+
<span class="identity-avatar">${this.getInitials(deviceArg.label)}</span>
|
|
2159
|
+
<div>
|
|
2160
|
+
<div class="identity-primary">${deviceArg.label}</div>
|
|
2161
|
+
<div class="identity-secondary">${deviceArg.platform}${deviceArg.appVersion ? ` - ${deviceArg.appVersion}` : ''}</div>
|
|
2162
|
+
</div>
|
|
2163
|
+
</div>
|
|
2164
|
+
`,
|
|
2165
|
+
html`<idp-badge variant=${deviceArg.status === 'active' ? 'ok' : 'error'}>${deviceArg.status}</idp-badge>`,
|
|
2166
|
+
[
|
|
2167
|
+
deviceArg.capabilities?.push ? 'push' : '',
|
|
2168
|
+
deviceArg.capabilities?.nfc ? 'nfc' : '',
|
|
2169
|
+
deviceArg.capabilities?.gps ? 'gps' : '',
|
|
2170
|
+
].filter(Boolean).join(', ') || '-',
|
|
2171
|
+
deviceArg.lastSeenAt ? this.formatTimeAgo(deviceArg.lastSeenAt) : 'never',
|
|
2172
|
+
html`<div class="cell-actions"><button class="table-action destructive" @click=${() => this.dispatchShellEvent<IIdpAdminPassportDeviceEventDetail>('idp-admin-passport-revoke', { deviceId: deviceArg.id })}>Revoke</button></div>`,
|
|
2173
|
+
],
|
|
2174
|
+
}));
|
|
2175
|
+
|
|
2176
|
+
return html`
|
|
2177
|
+
${this.renderPageHeader('Security', 'Manage how you authenticate and protect your account.')}
|
|
2178
|
+
<div class="body wide-body">
|
|
2179
|
+
${this.credentialMessage ? html`<div class="notice-card"><idp-icon name="shield" size="16"></idp-icon><div><div class="section-title">Credential update</div><div class="muted">${this.credentialMessage}</div></div></div>` : html``}
|
|
2180
|
+
${this.credentialError ? html`<div class="notice-card" style="border-color:var(--idp-error-border);background:var(--idp-error-bg)"><idp-icon name="alert" size="16" style="color:var(--idp-error)"></idp-icon><div><div class="section-title" style="color:var(--idp-error)">Credential error</div><div class="muted">${this.credentialError}</div></div></div>` : html``}
|
|
2181
|
+
${this.renderSectionCard('Session security', 'Live session data is available and sessions can be revoked from the device list.', html`
|
|
2182
|
+
<div class="split-row"><div><div class="section-title">Active sessions</div><div class="muted">Review and revoke sessions from the Sessions & Devices page.</div></div><idp-badge variant="accent">${this.sessions.length}</idp-badge></div>
|
|
2183
|
+
<div class="divider"></div>
|
|
2184
|
+
<button class="plain-button outline" @click=${() => this.setPage('sessions')}>Open sessions</button>
|
|
2185
|
+
`)}
|
|
2186
|
+
${this.renderSectionCard('Password', 'Change your password using the existing production password endpoint. All fields are required.', html`
|
|
2187
|
+
${this.renderFormRow('Current password', '', html`<input class="input" type="password" autocomplete="current-password" .value=${this.currentPassword} @input=${(eventArg: Event) => this.setPasswordField('current', eventArg)} />`, true)}
|
|
2188
|
+
${this.renderFormRow('New password', 'Minimum 12 characters.', html`<input class="input" type="password" autocomplete="new-password" .value=${this.newPassword} @input=${(eventArg: Event) => this.setPasswordField('new', eventArg)} />`, true)}
|
|
2189
|
+
${this.renderFormRow('Confirm password', '', html`<input class="input" type="password" autocomplete="new-password" .value=${this.confirmPassword} @input=${(eventArg: Event) => this.setPasswordField('confirm', eventArg)} />`, true)}
|
|
2190
|
+
<div class="divider"></div>
|
|
2191
|
+
<button class="plain-button primary" @click=${() => this.submitPasswordChange()}>Update password</button>
|
|
2192
|
+
`)}
|
|
2193
|
+
<idp-data-table
|
|
2194
|
+
title="Passport devices"
|
|
2195
|
+
subtitle="Cryptographic IDP Passport devices registered for this account."
|
|
2196
|
+
badge=${`${this.passportDevices.length} total`}
|
|
2197
|
+
empty-title="No passport devices"
|
|
2198
|
+
empty-description="Passport devices will appear here after enrollment from the mobile passport app."
|
|
2199
|
+
.columns=${[
|
|
2200
|
+
{ label: 'Device' },
|
|
2201
|
+
{ label: 'Status' },
|
|
2202
|
+
{ label: 'Capabilities', hideBelow: 'mobile' },
|
|
2203
|
+
{ label: 'Last seen', mono: true, hideBelow: 'mobile' },
|
|
2204
|
+
{ label: 'Action', align: 'right' },
|
|
2205
|
+
]}
|
|
2206
|
+
.rows=${passportRows}
|
|
2207
|
+
></idp-data-table>
|
|
2208
|
+
<div class="section-card">
|
|
2209
|
+
<div class="section-headline">
|
|
2210
|
+
<div><div class="section-title">Enroll passport device</div><div class="section-description">Creates a signed enrollment challenge for the IDP Passport device flow.</div></div>
|
|
2211
|
+
<button class="plain-button outline" @click=${() => this.requestPassportEnrollment()}>Create challenge</button>
|
|
2212
|
+
</div>
|
|
2213
|
+
${this.passportEnrollment ? html`
|
|
2214
|
+
<div class="notice-card" style="margin-bottom:14px"><idp-icon name="key" size="16"></idp-icon><div><div class="section-title">Enrollment challenge ready</div><div class="muted">Expires ${new Date(this.passportEnrollment.expiresAt).toLocaleString()}. Use the pairing token or payload in a passport client to complete enrollment.</div></div></div>
|
|
2215
|
+
${this.renderFormRow('Pairing token', '', this.renderCodeBlock(this.passportEnrollment.pairingToken))}
|
|
2216
|
+
${this.renderFormRow('Pairing payload', '', this.renderCodeBlock(this.passportEnrollment.pairingPayload))}
|
|
2217
|
+
${this.renderFormRow('Signing payload', '', this.renderCodeBlock(this.passportEnrollment.signingPayload))}
|
|
2218
|
+
` : html`<div class="muted">No active enrollment challenge.</div>`}
|
|
2219
|
+
</div>
|
|
2220
|
+
<div class="primary-grid">
|
|
2221
|
+
${this.renderStateCard('TOTP controls not connected', 'No TOTP secret, enrollment, or verification endpoints exist in this backend yet, so no fake TOTP toggle is shown.', 'lock')}
|
|
2222
|
+
${this.renderStateCard('WebAuthn passkeys not connected', 'No WebAuthn passkey credential model or assertion endpoints exist yet. Passport devices are the available cryptographic credential path.', 'key')}
|
|
2223
|
+
</div>
|
|
2224
|
+
</div>
|
|
2225
|
+
`;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
private renderSessions(): TemplateResult {
|
|
2229
|
+
return html`
|
|
2230
|
+
${this.renderPageHeader('Sessions & Devices', 'Active login sessions across all your devices.')}
|
|
2231
|
+
<div class="body narrow-body"><div class="list-stack">
|
|
2232
|
+
${this.sessions.length ? this.sessions.map((sessionArg) => html`
|
|
2233
|
+
<div class="row-card">
|
|
2234
|
+
<span class="icon-tile"><idp-icon name="monitor" size="17"></idp-icon></span>
|
|
2235
|
+
<div style="flex:1">
|
|
2236
|
+
<div style="display:flex;gap:8px;align-items:center"><span class="section-title">${sessionArg.deviceName || 'Unknown device'}</span>${sessionArg.isCurrent ? html`<idp-badge variant="ok">Current session</idp-badge>` : html``}</div>
|
|
2237
|
+
<div class="muted">${sessionArg.browser || 'Unknown browser'} - ${sessionArg.os || 'Unknown OS'} - IP: <span class="mono">${sessionArg.ip || 'unknown'}</span></div>
|
|
2238
|
+
<div class="muted">Started ${this.formatTimeAgo(sessionArg.createdAt)} - Active ${this.formatTimeAgo(sessionArg.lastActive)}</div>
|
|
2239
|
+
</div>
|
|
2240
|
+
${sessionArg.isCurrent ? html`` : html`<button class="plain-button ghost" style="color:var(--idp-error)" @click=${() => this.dispatchShellEvent<IIdpAdminSessionEventDetail>('idp-admin-session-revoke', { sessionId: sessionArg.id })}>Revoke</button>`}
|
|
2241
|
+
</div>
|
|
2242
|
+
`) : this.renderDataState('No sessions', 'There are no active sessions for this account or session telemetry is unavailable.', 'monitor')}
|
|
2243
|
+
</div></div>
|
|
2244
|
+
`;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
private renderAccountApps(): TemplateResult {
|
|
2248
|
+
const apps = this.accountApps;
|
|
2249
|
+
return html`
|
|
2250
|
+
${this.renderPageHeader('Connected Apps', 'Third-party apps and services that have OAuth access to your account.')}
|
|
2251
|
+
<div class="body narrow-body"><div class="list-stack">
|
|
2252
|
+
${apps.length ? apps.map((appArg) => html`<div class="row-card"><span class="app-avatar">${appArg.name.slice(0, 2).toUpperCase()}</span><div style="flex:1"><div class="section-title" style="margin-bottom:4px">${appArg.name}</div><div class="chip-row">${(appArg.scopes || []).map((scopeArg) => html`<idp-badge variant="outline">${scopeArg}</idp-badge>`)}</div><div class="muted" style="margin-top:6px">${appArg.description || appArg.appUrl || 'Connected application'}</div></div><button class="plain-button ghost" style="color:var(--idp-error)" @click=${() => this.dispatchShellEvent<IIdpAdminAppToggleEventDetail>('idp-admin-app-toggle', { appId: appArg.id, connected: false })}>Revoke</button></div>`) : this.renderDataState('Account app grants not connected', 'No account-level OAuth grant endpoint is wired into this app yet. Organisation app connections are managed under Organisation > OAuth Apps.', 'grid')}
|
|
2253
|
+
</div></div>
|
|
2254
|
+
`;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
private renderOrgGeneral(): TemplateResult {
|
|
2258
|
+
const org = this.currentOrg;
|
|
2259
|
+
const connectedAppCount = this.orgApps.filter((appArg) => appArg.isConnected).length;
|
|
2260
|
+
const orgActivities = this.activities
|
|
2261
|
+
.filter((activityArg) => {
|
|
2262
|
+
const searchableText = `${activityArg.targetType || ''} ${activityArg.description || ''}`.toLowerCase();
|
|
2263
|
+
return Boolean(org.slug && searchableText.includes(org.slug.toLowerCase()))
|
|
2264
|
+
|| Boolean(org.name && searchableText.includes(org.name.toLowerCase()))
|
|
2265
|
+
|| searchableText.includes('org')
|
|
2266
|
+
|| searchableText.includes('organization')
|
|
2267
|
+
|| searchableText.includes('organisation');
|
|
2268
|
+
})
|
|
2269
|
+
.slice(0, 5);
|
|
2270
|
+
const kpis: TKpi[] = [
|
|
2271
|
+
{
|
|
2272
|
+
label: 'Members',
|
|
2273
|
+
value: String(this.orgMembers.length),
|
|
2274
|
+
delta: `${this.orgInvitations.length} pending`,
|
|
2275
|
+
deltaKind: 'up',
|
|
2276
|
+
spark: [1, 1, 2, Math.max(this.orgMembers.length, 1)],
|
|
2277
|
+
sparkColor: 'var(--idp-spark-up)',
|
|
2278
|
+
accent: 'var(--idp-chart-5)',
|
|
2279
|
+
sub: org.name,
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
label: 'Connected apps',
|
|
2283
|
+
value: String(connectedAppCount),
|
|
2284
|
+
delta: `${this.orgApps.length} available`,
|
|
2285
|
+
deltaKind: 'live',
|
|
2286
|
+
spark: [1, 2, 2, Math.max(connectedAppCount, 1)],
|
|
2287
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
2288
|
+
accent: 'var(--idp-info)',
|
|
2289
|
+
sub: 'Organisation OAuth catalogue',
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
label: 'Role',
|
|
2293
|
+
value: org.myRole || 'member',
|
|
2294
|
+
delta: 'live',
|
|
2295
|
+
deltaKind: 'live',
|
|
2296
|
+
spark: [1, 1, 1, 1],
|
|
2297
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
2298
|
+
accent: 'var(--idp-chart-1)',
|
|
2299
|
+
sub: 'Your access level',
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
label: 'Activity',
|
|
2303
|
+
value: String(orgActivities.length),
|
|
2304
|
+
delta: `${this.activities.length} account events`,
|
|
2305
|
+
deltaKind: 'up',
|
|
2306
|
+
spark: [1, 1, Math.max(orgActivities.length, 1)],
|
|
2307
|
+
sparkColor: 'var(--idp-spark-up)',
|
|
2308
|
+
accent: 'var(--idp-chart-2)',
|
|
2309
|
+
sub: 'Recent org-related events',
|
|
2310
|
+
},
|
|
2311
|
+
];
|
|
2312
|
+
|
|
2313
|
+
return html`
|
|
2314
|
+
<header class="page-head">
|
|
2315
|
+
<div>
|
|
2316
|
+
<div class="eyebrow-row"><span class="eyebrow">Organisation - General</span><span class="live-pill"><span class="live-dot"></span>live</span></div>
|
|
2317
|
+
<h1>${org.name}</h1>
|
|
2318
|
+
<div class="lead">Selected organisation dashboard for <code>${org.slug ? `@${org.slug}` : org.id || 'no organisation selected'}</code>.</div>
|
|
2319
|
+
</div>
|
|
2320
|
+
<div class="page-actions"><button class="plain-button outline" @click=${() => this.setPage('org-members')}>Manage members</button><button class="plain-button primary" @click=${() => this.setPage('org-apps')}>Manage apps</button></div>
|
|
2321
|
+
</header>
|
|
2322
|
+
<div class="body">
|
|
2323
|
+
<div class="kpis">${kpis.map((kpiArg) => this.renderKpi(kpiArg))}</div>
|
|
2324
|
+
${this.dataLoading || this.dataError ? this.renderDataState('No organisation data', 'The console has not received live organisation data yet.', 'building') : html``}
|
|
2325
|
+
<div class="primary-grid">
|
|
2326
|
+
<div class="card">
|
|
2327
|
+
<div class="card-head"><span class="card-title">Organisation profile</span><idp-badge>${org.myRole || 'member'}</idp-badge></div>
|
|
2328
|
+
<div style="padding:16px">
|
|
2329
|
+
<div style="display:flex;align-items:center;gap:14px;margin-bottom:16px"><span class="org-avatar" style="width:48px;height:48px;font-size:17px">${org.name.slice(0, 2).toUpperCase()}</span><div><div style="font-size:16px;font-weight:600;color:var(--idp-fg)">${org.name}</div><div class="mono">idp.global/org/${org.slug || 'unassigned'}</div></div></div>
|
|
2330
|
+
${this.renderCodeBlock(org.id || 'No organisation selected')}
|
|
2331
|
+
<div class="muted" style="margin-top:10px">Rename, slug updates, ownership transfer, and deletion are available from Settings with server-side audited confirmation.</div>
|
|
2332
|
+
</div>
|
|
2333
|
+
</div>
|
|
2334
|
+
<div class="card">
|
|
2335
|
+
<div class="card-head"><span class="card-title">Recent organisation activity</span><idp-badge>${orgActivities.length} events</idp-badge></div>
|
|
2336
|
+
${orgActivities.length ? orgActivities.map((activityArg) => html`
|
|
2337
|
+
<div class="feed-item"><span class="feed-dot" style="--dot-color:var(--idp-info)"></span><div class="feed-text"><strong>${activityArg.action.replace(/_/g, ' ')}</strong> - ${activityArg.description}</div><span class="feed-meta">${this.formatTimeAgo(activityArg.timestamp)}</span></div>
|
|
2338
|
+
`) : this.renderStateCard('No org activity yet', 'Organisation-related activity will appear here when the backend reports matching activity events.', 'activity')}
|
|
2339
|
+
</div>
|
|
2340
|
+
</div>
|
|
2341
|
+
</div>
|
|
2342
|
+
`;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
private renderOrgSettings(): TemplateResult {
|
|
2346
|
+
const org = this.currentOrg;
|
|
2347
|
+
this.syncOrgSettingsState();
|
|
2348
|
+
const transferCandidates = this.orgMembers.filter((memberArg) => !memberArg.isCurrentUser);
|
|
2349
|
+
const transferConfirmText = `transfer ${org.slug}`;
|
|
2350
|
+
const deleteConfirmText = `delete ${org.slug}`;
|
|
2351
|
+
const identityChanged = this.orgNameDraft.trim() !== org.name || this.orgSlugDraft.trim().toLowerCase() !== org.slug;
|
|
2352
|
+
return html`
|
|
2353
|
+
${this.renderPageHeader('Organisation Settings', 'Audited configuration and owner-controlled destructive operations.')}
|
|
2354
|
+
<div class="body narrow-body">
|
|
2355
|
+
${this.orgSettingsError ? html`<div class="notice-card" style="border-color:var(--idp-error-border);background:var(--idp-error-bg)"><idp-icon name="alert" size="16" style="color:var(--idp-error)"></idp-icon><div><div class="section-title" style="color:var(--idp-error)">Settings action blocked</div><div class="muted">${this.orgSettingsError}</div></div></div>` : html``}
|
|
2356
|
+
${this.renderSectionCard('Organisation identity', `Type ${org.slug} to confirm name or slug changes. The backend verifies this confirmation before saving.`, html`
|
|
2357
|
+
<div style="display:flex;align-items:center;gap:14px;margin-bottom:20px"><span class="org-avatar" style="width:48px;height:48px;font-size:17px">${org.name.slice(0, 2).toUpperCase()}</span><div><div style="font-size:16px;font-weight:600;color:var(--idp-fg)">${org.name}</div><div class="mono">idp.global/org/${org.slug}</div></div></div>
|
|
2358
|
+
${this.renderFormRow('Organisation name', '', html`<input class="input" .value=${this.orgNameDraft} @input=${(eventArg: Event) => this.setOrgSettingsField('name', eventArg)} />`, true)}
|
|
2359
|
+
${this.renderFormRow('URL slug', "Used in your org's public URLs.", html`<div class="input-group"><span class="input-prefix">idp.global/org/</span><input class="input" .value=${this.orgSlugDraft} @input=${(eventArg: Event) => this.setOrgSettingsField('slug', eventArg)} /></div>`)}
|
|
2360
|
+
${this.renderFormRow('Confirmation', `Type ${org.slug}`, html`<input class="input" .value=${this.orgSettingsConfirmation} @input=${(eventArg: Event) => this.setOrgSettingsField('settingsConfirmation', eventArg)} />`)}
|
|
2361
|
+
<div class="divider"></div>
|
|
2362
|
+
<button class="plain-button primary" ?disabled=${!identityChanged} @click=${() => this.submitOrgSettingsUpdate()}>Apply identity changes</button>
|
|
2363
|
+
`)}
|
|
2364
|
+
${this.renderSectionCard('Organisation ID', 'Use this identifier when making API calls.', this.renderCodeBlock(org.id))}
|
|
2365
|
+
${this.renderSectionCard('Transfer ownership', `Owner-only operation. Type ${transferConfirmText} to confirm.`, html`
|
|
2366
|
+
${transferCandidates.length ? html`
|
|
2367
|
+
${this.renderFormRow('New owner', 'The target user must already be an organisation member.', html`
|
|
2368
|
+
<select class="select" .value=${this.transferOwnerId} @change=${(eventArg: Event) => this.setOrgSettingsField('transferOwnerId', eventArg)}>
|
|
2369
|
+
<option value="">Select a member</option>
|
|
2370
|
+
${transferCandidates.map((memberArg) => html`<option value=${memberArg.userId}>${memberArg.name || memberArg.email} (${memberArg.email})</option>`)}
|
|
2371
|
+
</select>
|
|
2372
|
+
`, true)}
|
|
2373
|
+
${this.renderFormRow('Confirmation', `Type ${transferConfirmText}`, html`<input class="input" .value=${this.transferConfirmation} @input=${(eventArg: Event) => this.setOrgSettingsField('transferConfirmation', eventArg)} />`)}
|
|
2374
|
+
<div class="divider"></div>
|
|
2375
|
+
<button class="plain-button outline" @click=${() => this.submitOrgTransfer()}>Transfer ownership</button>
|
|
2376
|
+
` : this.renderStateCard('No transfer candidates loaded', 'Load organisation members before transferring ownership.', 'users')}
|
|
2377
|
+
`)}
|
|
2378
|
+
${this.renderSectionCard('Delete organisation', `Owner-only destructive operation. Type ${deleteConfirmText} to permanently remove this organisation, its memberships, pending invitations, billing records, and app connections.`, html`
|
|
2379
|
+
${this.renderFormRow('Confirmation', `Type ${deleteConfirmText}`, html`<input class="input" .value=${this.deleteConfirmation} @input=${(eventArg: Event) => this.setOrgSettingsField('deleteConfirmation', eventArg)} />`)}
|
|
2380
|
+
<div class="divider"></div>
|
|
2381
|
+
<button class="plain-button destructive" @click=${() => this.submitOrgDelete()}>Delete organisation</button>
|
|
2382
|
+
`)}
|
|
2383
|
+
</div>
|
|
2384
|
+
`;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
private renderOrgMembers(): TemplateResult {
|
|
2388
|
+
const customRoleRows = this.orgRoleDefinitions.map((roleArg) => ({
|
|
2389
|
+
cells: [
|
|
2390
|
+
html`
|
|
2391
|
+
<div class="identity-cell">
|
|
2392
|
+
<span class="identity-avatar">${this.getInitials(roleArg.name)}</span>
|
|
2393
|
+
<div>
|
|
2394
|
+
<div class="identity-primary">${roleArg.name}</div>
|
|
2395
|
+
<div class="identity-secondary">${roleArg.description || 'Custom organisation role'}</div>
|
|
2396
|
+
</div>
|
|
2397
|
+
</div>
|
|
2398
|
+
`,
|
|
2399
|
+
roleArg.key,
|
|
2400
|
+
html`
|
|
2401
|
+
<div class="cell-actions">
|
|
2402
|
+
<button class="table-action" @click=${() => this.openRoleUpsertDialog(roleArg)}>Edit</button>
|
|
2403
|
+
<button class="table-action destructive" @click=${() => this.openRoleDeleteDialog(roleArg)}>Delete</button>
|
|
2404
|
+
</div>
|
|
2405
|
+
`,
|
|
2406
|
+
],
|
|
2407
|
+
}));
|
|
2408
|
+
const memberRows = this.orgMembers.map((memberArg) => ({
|
|
2409
|
+
cells: [
|
|
2410
|
+
html`
|
|
2411
|
+
<div class="identity-cell">
|
|
2412
|
+
<span class="identity-avatar">${this.getInitials(memberArg.name || memberArg.email)}</span>
|
|
2413
|
+
<div>
|
|
2414
|
+
<div class="identity-primary">${memberArg.name || memberArg.email}</div>
|
|
2415
|
+
<div class="identity-secondary">${memberArg.email}</div>
|
|
2416
|
+
</div>
|
|
2417
|
+
</div>
|
|
2418
|
+
`,
|
|
2419
|
+
html`<div class="chip-row">${memberArg.roles.map((roleArg) => html`<idp-badge variant=${this.roleVariant(roleArg)}>${roleArg}</idp-badge>`)}</div>`,
|
|
2420
|
+
memberArg.isCurrentUser ? html`<idp-badge variant="accent">You</idp-badge>` : html`<idp-badge variant="outline">Member</idp-badge>`,
|
|
2421
|
+
memberArg.roles.includes('owner') || memberArg.isCurrentUser
|
|
2422
|
+
? html`<div class="cell-actions"><button class="table-action" @click=${() => this.openMemberRolesDialog(memberArg)}>Edit roles</button></div>`
|
|
2423
|
+
: html`<div class="cell-actions"><button class="table-action" @click=${() => this.openMemberRolesDialog(memberArg)}>Edit roles</button><button class="table-action destructive" @click=${() => this.dispatchShellEvent<IIdpAdminMemberEventDetail>('idp-admin-member-remove', { userId: memberArg.userId })}>Remove</button></div>`,
|
|
2424
|
+
],
|
|
2425
|
+
}));
|
|
2426
|
+
const invitationRows = this.orgInvitations.map((inviteArg) => ({
|
|
2427
|
+
cells: [
|
|
2428
|
+
html`
|
|
2429
|
+
<div class="identity-cell">
|
|
2430
|
+
<span class="identity-avatar">${this.getInitials(inviteArg.email)}</span>
|
|
2431
|
+
<div>
|
|
2432
|
+
<div class="identity-primary">${inviteArg.email}</div>
|
|
2433
|
+
<div class="identity-secondary">Invited ${this.formatTimeAgo(inviteArg.invitedAt)}</div>
|
|
2434
|
+
</div>
|
|
2435
|
+
</div>
|
|
2436
|
+
`,
|
|
2437
|
+
html`<div class="chip-row">${inviteArg.roles.map((roleArg) => html`<idp-badge variant=${this.roleVariant(roleArg)}>${roleArg}</idp-badge>`)}</div>`,
|
|
2438
|
+
new Date(inviteArg.expiresAt).toLocaleDateString(),
|
|
2439
|
+
html`
|
|
2440
|
+
<div class="cell-actions">
|
|
2441
|
+
<button class="table-action" @click=${() => this.dispatchShellEvent<IIdpAdminInvitationEventDetail>('idp-admin-invitation-resend', { invitationId: inviteArg.id })}>Resend</button>
|
|
2442
|
+
<button class="table-action destructive" @click=${() => this.dispatchShellEvent<IIdpAdminInvitationEventDetail>('idp-admin-invitation-cancel', { invitationId: inviteArg.id })}>Cancel</button>
|
|
2443
|
+
</div>
|
|
2444
|
+
`,
|
|
2445
|
+
],
|
|
2446
|
+
}));
|
|
2447
|
+
|
|
2448
|
+
return html`
|
|
2449
|
+
${this.renderPageHeader('Members', `${this.orgMembers.length} members - ${this.orgInvitations.length} pending invitations`, html`<button class="plain-button primary" @click=${() => this.dispatchShellEvent('idp-admin-member-invite', { orgId: this.currentOrg.id })}><idp-icon name="plus" size="12"></idp-icon>Invite member</button>`)}
|
|
2450
|
+
<div class="body wide-body">
|
|
2451
|
+
${this.orgSettingsError ? html`<div class="notice-card" style="border-color:var(--idp-error-border);background:var(--idp-error-bg)"><idp-icon name="alert" size="16" style="color:var(--idp-error)"></idp-icon><div><div class="section-title" style="color:var(--idp-error)">Role action blocked</div><div class="muted">${this.orgSettingsError}</div></div></div>` : html``}
|
|
2452
|
+
<idp-data-table
|
|
2453
|
+
title="Custom organisation roles"
|
|
2454
|
+
subtitle="Business roles defined by this organisation. Platform roles like owner/admin remain protected."
|
|
2455
|
+
badge=${`${this.orgRoleDefinitions.length} custom`}
|
|
2456
|
+
empty-title="No custom roles"
|
|
2457
|
+
empty-description="Create custom roles such as Finance, Support Lead, or Contractor, then assign them to members."
|
|
2458
|
+
.columns=${[
|
|
2459
|
+
{ label: 'Role' },
|
|
2460
|
+
{ label: 'Key', mono: true },
|
|
2461
|
+
{ label: 'Action', align: 'right' },
|
|
2462
|
+
]}
|
|
2463
|
+
.rows=${customRoleRows}
|
|
2464
|
+
></idp-data-table>
|
|
2465
|
+
<div><button class="plain-button outline" @click=${() => this.openRoleUpsertDialog()}><idp-icon name="plus" size="12"></idp-icon>Add custom role</button></div>
|
|
2466
|
+
<idp-data-table
|
|
2467
|
+
title="Members"
|
|
2468
|
+
badge=${`${this.orgMembers.length} total`}
|
|
2469
|
+
empty-title="No members loaded"
|
|
2470
|
+
empty-description="Members will appear here after organisation membership data is loaded."
|
|
2471
|
+
.columns=${[
|
|
2472
|
+
{ label: 'User' },
|
|
2473
|
+
{ label: 'Roles', hideBelow: 'mobile' },
|
|
2474
|
+
{ label: 'Status' },
|
|
2475
|
+
{ label: 'Action', align: 'right' },
|
|
2476
|
+
]}
|
|
2477
|
+
.rows=${memberRows}
|
|
2478
|
+
></idp-data-table>
|
|
2479
|
+
${this.orgInvitations.length ? html`
|
|
2480
|
+
<idp-data-table
|
|
2481
|
+
title="Pending invitations"
|
|
2482
|
+
badge=${`${this.orgInvitations.length} total`}
|
|
2483
|
+
.columns=${[
|
|
2484
|
+
{ label: 'Invitee' },
|
|
2485
|
+
{ label: 'Roles', hideBelow: 'mobile' },
|
|
2486
|
+
{ label: 'Expires', mono: true },
|
|
2487
|
+
{ label: 'Action', align: 'right' },
|
|
2488
|
+
]}
|
|
2489
|
+
.rows=${invitationRows}
|
|
2490
|
+
></idp-data-table>
|
|
2491
|
+
` : html``}
|
|
2492
|
+
</div>
|
|
2493
|
+
`;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
private renderOrgApps(): TemplateResult {
|
|
2497
|
+
const appRows = this.orgApps.map((appArg) => ({
|
|
2498
|
+
cells: [
|
|
2499
|
+
html`
|
|
2500
|
+
<div class="identity-cell">
|
|
2501
|
+
<span class="identity-avatar">${this.getInitials(appArg.name)}</span>
|
|
2502
|
+
<div>
|
|
2503
|
+
<div class="identity-primary">${appArg.name}</div>
|
|
2504
|
+
<div class="identity-secondary">${appArg.description || appArg.appUrl || 'Global app'}</div>
|
|
2505
|
+
</div>
|
|
2506
|
+
</div>
|
|
2507
|
+
`,
|
|
2508
|
+
appArg.clientId || '-',
|
|
2509
|
+
html`<div class="chip-row">${(appArg.scopes || []).slice(0, 4).map((scopeArg) => html`<idp-badge variant="outline">${scopeArg}</idp-badge>`)}</div>`,
|
|
2510
|
+
html`<idp-badge variant=${appArg.isConnected ? 'ok' : 'outline'}>${appArg.isConnected ? 'connected' : 'available'}</idp-badge>`,
|
|
2511
|
+
html`<idp-badge variant=${appArg.roleMappings?.length ? 'accent' : 'outline'}>${appArg.roleMappings?.length || 0} mappings</idp-badge>`,
|
|
2512
|
+
html`<div class="cell-actions">${appArg.isConnected ? html`<button class="table-action" @click=${() => this.openAppRoleMappingsDialog(appArg)}>Map roles</button>` : html``}<button class="table-action ${appArg.isConnected ? '' : 'primary'}" @click=${() => this.dispatchShellEvent<IIdpAdminAppToggleEventDetail>('idp-admin-app-toggle', { appId: appArg.id, connected: !appArg.isConnected })}>${appArg.isConnected ? 'Disconnect' : 'Connect'}</button></div>`,
|
|
2513
|
+
],
|
|
2514
|
+
}));
|
|
2515
|
+
|
|
2516
|
+
return html`
|
|
2517
|
+
${this.renderPageHeader('Apps', "Global apps connected to this organisation.")}
|
|
2518
|
+
<div class="body wide-body">
|
|
2519
|
+
${this.orgSettingsError ? html`<div class="notice-card" style="border-color:var(--idp-error-border);background:var(--idp-error-bg)"><idp-icon name="alert" size="16" style="color:var(--idp-error)"></idp-icon><div><div class="section-title" style="color:var(--idp-error)">App mapping blocked</div><div class="muted">${this.orgSettingsError}</div></div></div>` : html``}
|
|
2520
|
+
<idp-data-table
|
|
2521
|
+
title="OAuth apps"
|
|
2522
|
+
badge=${`${this.orgApps.length} total`}
|
|
2523
|
+
empty-title="No apps available"
|
|
2524
|
+
empty-description="Global apps will appear here after app catalogue data is loaded."
|
|
2525
|
+
.columns=${[
|
|
2526
|
+
{ label: 'App' },
|
|
2527
|
+
{ label: 'Client ID', mono: true, hideBelow: 'tablet' },
|
|
2528
|
+
{ label: 'Scopes', hideBelow: 'mobile' },
|
|
2529
|
+
{ label: 'Status' },
|
|
2530
|
+
{ label: 'Role mappings' },
|
|
2531
|
+
{ label: 'Action', align: 'right' },
|
|
2532
|
+
]}
|
|
2533
|
+
.rows=${appRows}
|
|
2534
|
+
></idp-data-table>
|
|
2535
|
+
</div>
|
|
2536
|
+
`;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
private renderSupport(): TemplateResult {
|
|
2540
|
+
const services = [
|
|
2541
|
+
['Account Recovery', 'Lost access to your account or locked out of your organisation? Our team will verify your identity and restore access securely.', 'EUR149', 'per incident', 'key', '1-2 business days'],
|
|
2542
|
+
['Organisation Recovery', 'All owners have lost access to your organisation. We can verify ownership and restore admin access.', 'EUR249', 'per incident', 'building', '2-3 business days'],
|
|
2543
|
+
['Data Export & Migration', 'Full export of your org data - users, sessions, app connections - for migration or compliance.', 'EUR199', 'per request', 'box', '3-5 business days'],
|
|
2544
|
+
['Identity & SSO Consulting', 'Architecture review, OIDC guidance, and custom SSO setup for your organisation stack.', 'EUR190', 'per hour', 'globe', 'Scheduled session'],
|
|
2545
|
+
['Security Review', 'Audit of connected apps, active sessions, passkey policies, and role assignments.', 'EUR390', 'per review', 'shield', '5-7 business days'],
|
|
2546
|
+
];
|
|
2547
|
+
return html`
|
|
2548
|
+
${this.renderPageHeader('Support', 'idp.global is free for everyone. Paid options cover hands-on recovery and consulting work.')}
|
|
2549
|
+
<div class="body narrow-body"><div class="row-card" style="background:var(--idp-accent-soft);border-color:var(--idp-info-border)"><idp-icon name="shield" size="16" style="color:var(--idp-accent)"></idp-icon><div><div class="section-title" style="color:var(--idp-accent)">idp.global is free, forever</div><div class="muted">All platform features - authentication, passkeys, OIDC apps, team management - are included at no cost.</div></div></div><div class="list-stack">${services.map((serviceArg) => html`<div class="row-card"><span class="icon-tile"><idp-icon name=${serviceArg[4] as any} size="16"></idp-icon></span><div style="flex:1"><div style="display:flex;gap:8px;align-items:baseline"><span class="section-title">${serviceArg[0]}</span><span class="muted">- ${serviceArg[5]}</span></div><div class="muted">${serviceArg[1]}</div><div style="display:flex;align-items:center;justify-content:space-between;margin-top:10px"><div><span style="font-size:16px;font-weight:700;color:var(--idp-fg)">${serviceArg[2]}</span> <span class="muted">${serviceArg[3]}</span></div><button class="plain-button outline" disabled>Request flow pending</button></div></div></div>`)}</div></div>
|
|
2550
|
+
`;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
private renderGAUsers(): TemplateResult {
|
|
2554
|
+
return html`${this.renderPageHeader('All Users', 'Platform-wide user administration.')}<div class="body wide-body">${this.renderDataState('User directory not connected', 'The shell is ready for global user data, but no platform user-list endpoint is wired into this app yet. No demo users are shown in production mode.', 'users')}</div>`;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
private renderGAOrgs(): TemplateResult {
|
|
2558
|
+
const orgRows = this.orgs.map((orgArg) => ({
|
|
2559
|
+
cells: [
|
|
2560
|
+
html`
|
|
2561
|
+
<div class="identity-cell">
|
|
2562
|
+
<span class="identity-avatar">${this.getInitials(orgArg.name)}</span>
|
|
2563
|
+
<div>
|
|
2564
|
+
<div class="identity-primary">${orgArg.name}</div>
|
|
2565
|
+
<div class="identity-secondary">${orgArg.slug ? `idp.global/org/${orgArg.slug}` : 'No slug'}</div>
|
|
2566
|
+
</div>
|
|
2567
|
+
</div>
|
|
2568
|
+
`,
|
|
2569
|
+
orgArg.slug || '-',
|
|
2570
|
+
html`<idp-badge variant=${this.roleVariant(orgArg.myRole || 'member')}>${orgArg.myRole || 'member'}</idp-badge>`,
|
|
2571
|
+
orgArg.id,
|
|
2572
|
+
],
|
|
2573
|
+
}));
|
|
2574
|
+
|
|
2575
|
+
return html`
|
|
2576
|
+
${this.renderPageHeader('All Organisations', `${this.orgs.length} organisations visible to this admin session`)}
|
|
2577
|
+
<div class="body wide-body">
|
|
2578
|
+
<idp-data-table
|
|
2579
|
+
title="Organisations"
|
|
2580
|
+
badge=${`${this.orgs.length} total`}
|
|
2581
|
+
empty-title="No organisations"
|
|
2582
|
+
empty-description="No organisations are visible to this admin session."
|
|
2583
|
+
.columns=${[
|
|
2584
|
+
{ label: 'Organisation' },
|
|
2585
|
+
{ label: 'Slug', mono: true, hideBelow: 'mobile' },
|
|
2586
|
+
{ label: 'Role' },
|
|
2587
|
+
{ label: 'Identifier', mono: true, hideBelow: 'tablet' },
|
|
2588
|
+
]}
|
|
2589
|
+
.rows=${orgRows}
|
|
2590
|
+
></idp-data-table>
|
|
2591
|
+
</div>
|
|
2592
|
+
`;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
private renderGAApps(): TemplateResult {
|
|
2596
|
+
const apps = this.adminApps.length ? this.adminApps : this.orgApps;
|
|
2597
|
+
const appRows = apps.map((appArg) => ({
|
|
2598
|
+
cells: [
|
|
2599
|
+
html`
|
|
2600
|
+
<div class="identity-cell">
|
|
2601
|
+
<span class="identity-avatar">${this.getInitials(appArg.name)}</span>
|
|
2602
|
+
<div>
|
|
2603
|
+
<div class="identity-primary">${appArg.name}</div>
|
|
2604
|
+
<div class="identity-secondary">${appArg.description || appArg.appUrl || 'Platform app'}</div>
|
|
2605
|
+
</div>
|
|
2606
|
+
</div>
|
|
2607
|
+
`,
|
|
2608
|
+
html`<idp-badge variant=${appArg.type === 'global' ? 'accent' : 'outline'}>${appArg.type || 'global'}</idp-badge>`,
|
|
2609
|
+
appArg.category || '-',
|
|
2610
|
+
appArg.connectionCount ?? '-',
|
|
2611
|
+
html`<idp-badge variant=${appArg.status === 'suspended' ? 'error' : appArg.status === 'pending_review' ? 'warn' : 'ok'}>${appArg.status || 'active'}</idp-badge>`,
|
|
2612
|
+
],
|
|
2613
|
+
}));
|
|
2614
|
+
|
|
2615
|
+
return html`
|
|
2616
|
+
${this.renderPageHeader('Platform Apps', 'Global and partner apps across the platform.')}
|
|
2617
|
+
<div class="body wide-body">
|
|
2618
|
+
<idp-data-table
|
|
2619
|
+
title="Platform apps"
|
|
2620
|
+
badge=${`${apps.length} total`}
|
|
2621
|
+
empty-title="No platform apps"
|
|
2622
|
+
empty-description="Global app administration data is not available for this session."
|
|
2623
|
+
.columns=${[
|
|
2624
|
+
{ label: 'App' },
|
|
2625
|
+
{ label: 'Type' },
|
|
2626
|
+
{ label: 'Category', hideBelow: 'mobile' },
|
|
2627
|
+
{ label: 'Connections', align: 'right', mono: true, hideBelow: 'mobile' },
|
|
2628
|
+
{ label: 'Status' },
|
|
2629
|
+
]}
|
|
2630
|
+
.rows=${appRows}
|
|
2631
|
+
></idp-data-table>
|
|
2632
|
+
</div>
|
|
2633
|
+
`;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
private renderMainContent(): TemplateResult {
|
|
2637
|
+
const renderers: Record<TIdpAdminPage, () => TemplateResult> = {
|
|
2638
|
+
overview: () => this.renderOverview(),
|
|
2639
|
+
profile: () => this.renderProfile(),
|
|
2640
|
+
security: () => this.renderSecurity(),
|
|
2641
|
+
sessions: () => this.renderSessions(),
|
|
2642
|
+
apps: () => this.renderAccountApps(),
|
|
2643
|
+
'org-general': () => this.renderOrgGeneral(),
|
|
2644
|
+
'org-settings': () => this.renderOrgSettings(),
|
|
2645
|
+
'org-members': () => this.renderOrgMembers(),
|
|
2646
|
+
'org-apps': () => this.renderOrgApps(),
|
|
2647
|
+
support: () => this.renderSupport(),
|
|
2648
|
+
'ga-users': () => this.renderGAUsers(),
|
|
2649
|
+
'ga-orgs': () => this.renderGAOrgs(),
|
|
2650
|
+
'ga-apps': () => this.renderGAApps(),
|
|
2651
|
+
};
|
|
2652
|
+
return (renderers[this.page] || renderers.overview)();
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
public render(): TemplateResult {
|
|
2656
|
+
return html`
|
|
2657
|
+
<div class="shell">
|
|
2658
|
+
${this.renderSidebar()}
|
|
2659
|
+
<main>${this.renderMainContent()}</main>
|
|
2660
|
+
</div>
|
|
2661
|
+
${this.renderDialog()}
|
|
2662
|
+
`;
|
|
2663
|
+
}
|
|
2664
|
+
}
|