@idp.global/catalog 1.2.1 → 2.1.0
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/changelog.md +21 -0
- package/dist_bundle/bundle.js +4428 -1496
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/idp-admin-overview.d.ts +28 -0
- package/dist_ts_web/elements/idp-admin-overview.js +460 -0
- package/dist_ts_web/elements/idp-admin-shared.d.ts +18 -0
- package/dist_ts_web/elements/idp-admin-shared.js +404 -0
- package/dist_ts_web/elements/idp-admin-shell.d.ts +96 -11
- package/dist_ts_web/elements/idp-admin-shell.js +442 -816
- package/dist_ts_web/elements/idp-admin-sidebar.d.ts +42 -0
- package/dist_ts_web/elements/idp-admin-sidebar.js +700 -0
- package/dist_ts_web/elements/idp-app-pair.d.ts +19 -0
- package/dist_ts_web/elements/idp-app-pair.js +187 -0
- package/dist_ts_web/elements/idp-auth-card.d.ts +17 -0
- package/dist_ts_web/elements/idp-auth-card.js +180 -0
- package/dist_ts_web/elements/idp-avatar.d.ts +17 -0
- package/dist_ts_web/elements/idp-avatar.js +151 -0
- package/dist_ts_web/elements/idp-badge.d.ts +2 -0
- package/dist_ts_web/elements/idp-badge.js +12 -3
- package/dist_ts_web/elements/idp-brand-mark.d.ts +16 -0
- package/dist_ts_web/elements/idp-brand-mark.js +139 -0
- package/dist_ts_web/elements/idp-button.d.ts +3 -0
- package/dist_ts_web/elements/idp-button.js +47 -3
- package/dist_ts_web/elements/idp-consent-card.d.ts +34 -0
- package/dist_ts_web/elements/idp-consent-card.js +217 -0
- package/dist_ts_web/elements/idp-delegation-section.d.ts +59 -0
- package/dist_ts_web/elements/idp-delegation-section.js +609 -0
- package/dist_ts_web/elements/idp-form-submit.d.ts +2 -1
- package/dist_ts_web/elements/idp-form-submit.js +11 -2
- package/dist_ts_web/elements/idp-form.js +2 -2
- package/dist_ts_web/elements/idp-input.d.ts +4 -0
- package/dist_ts_web/elements/idp-input.js +68 -15
- package/dist_ts_web/elements/idp-option-row.d.ts +25 -0
- package/dist_ts_web/elements/idp-option-row.js +267 -0
- package/dist_ts_web/elements/idp-otp-input.d.ts +32 -0
- package/dist_ts_web/elements/idp-otp-input.js +274 -0
- package/dist_ts_web/elements/idp-progress-bar.d.ts +14 -0
- package/dist_ts_web/elements/idp-progress-bar.js +127 -0
- package/dist_ts_web/elements/idp-signed-in-pill.d.ts +15 -0
- package/dist_ts_web/elements/idp-signed-in-pill.js +113 -0
- package/dist_ts_web/elements/idp-spinner.d.ts +13 -0
- package/dist_ts_web/elements/idp-spinner.js +109 -0
- package/dist_ts_web/elements/idp-status-icon.d.ts +18 -0
- package/dist_ts_web/elements/idp-status-icon.js +136 -0
- package/dist_ts_web/elements/index.d.ts +15 -0
- package/dist_ts_web/elements/index.js +16 -1
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/idp-admin-overview.ts +388 -0
- package/ts_web/elements/idp-admin-shared.ts +419 -0
- package/ts_web/elements/idp-admin-shell.ts +514 -871
- package/ts_web/elements/idp-admin-sidebar.ts +646 -0
- package/ts_web/elements/idp-app-pair.ts +120 -0
- package/ts_web/elements/idp-auth-card.ts +113 -0
- package/ts_web/elements/idp-avatar.ts +84 -0
- package/ts_web/elements/idp-badge.ts +5 -1
- package/ts_web/elements/idp-brand-mark.ts +76 -0
- package/ts_web/elements/idp-button.ts +36 -1
- package/ts_web/elements/idp-consent-card.ts +146 -0
- package/ts_web/elements/idp-delegation-section.ts +576 -0
- package/ts_web/elements/idp-form-submit.ts +5 -1
- package/ts_web/elements/idp-form.ts +1 -1
- package/ts_web/elements/idp-input.ts +58 -13
- package/ts_web/elements/idp-option-row.ts +184 -0
- package/ts_web/elements/idp-otp-input.ts +204 -0
- package/ts_web/elements/idp-progress-bar.ts +69 -0
- package/ts_web/elements/idp-signed-in-pill.ts +55 -0
- package/ts_web/elements/idp-spinner.ts +56 -0
- package/ts_web/elements/idp-status-icon.ts +76 -0
- package/ts_web/elements/index.ts +15 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
+
import { idpElementStyles } from './tokens.js';
|
|
3
|
+
import { idpAdminSharedStyles, renderAdminKpi, renderAdminStateCard, formatAdminTimeAgo, type TIdpAdminKpi } from './idp-admin-shared.js';
|
|
4
|
+
import './idp-icon.js';
|
|
5
|
+
import './idp-badge.js';
|
|
6
|
+
import '@design.estate/dees-catalog/dist_ts_web/elements/00group-chart/dees-chart-area/index.js';
|
|
7
|
+
import type {
|
|
8
|
+
TIdpAdminPage,
|
|
9
|
+
IIdpAdminUser,
|
|
10
|
+
IIdpAdminOrg,
|
|
11
|
+
IIdpAdminSession,
|
|
12
|
+
IIdpAdminActivity,
|
|
13
|
+
IIdpAdminPassportDevice,
|
|
14
|
+
IIdpAdminOverviewData,
|
|
15
|
+
IIdpAdminNavigateEventDetail,
|
|
16
|
+
} from './idp-admin-shell.js';
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLElementTagNameMap {
|
|
20
|
+
'idp-admin-overview': IdpAdminOverview;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@customElement('idp-admin-overview')
|
|
25
|
+
export class IdpAdminOverview extends DeesElement {
|
|
26
|
+
public static demo = () => {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const hours = Array.from({ length: 24 }, (unused, indexArg) => indexArg);
|
|
29
|
+
return html`
|
|
30
|
+
<idp-admin-overview
|
|
31
|
+
.user=${{ name: 'Alex Mercer', email: 'alex@lossless.com' }}
|
|
32
|
+
.orgs=${[
|
|
33
|
+
{ id: 'org_foss', name: 'Lossless GmbH', slug: 'lossless', myRole: 'owner' },
|
|
34
|
+
{ id: 'org_task', name: 'Task VC', slug: 'task', myRole: 'admin' },
|
|
35
|
+
]}
|
|
36
|
+
.sessions=${[
|
|
37
|
+
{ id: 's1', deviceName: 'MacBook Pro', browser: 'Safari 18', os: 'macOS', ip: '84.132.10.4', lastActive: now - 120000, createdAt: now - 86400000 * 3, isCurrent: true },
|
|
38
|
+
{ id: 's2', deviceName: 'iPhone 15 Pro', browser: 'Safari', os: 'iOS 18', ip: '84.132.10.4', lastActive: now - 3600000, createdAt: now - 86400000 * 10, isCurrent: false },
|
|
39
|
+
]}
|
|
40
|
+
.activities=${[
|
|
41
|
+
{ id: 'a1', action: 'login_success', description: 'Signed in with passkey', timestamp: now - 300000 },
|
|
42
|
+
{ id: 'a2', action: 'app_authorized', description: 'Granted access to foss.global', timestamp: now - 7200000 },
|
|
43
|
+
]}
|
|
44
|
+
.overviewData=${{
|
|
45
|
+
chartLabel: 'Approval activity',
|
|
46
|
+
chartSeries: [
|
|
47
|
+
{ name: 'Approvals', data: hours.map((hourArg) => ({ x: now - (23 - hourArg) * 3600000, y: [22, 18, 14, 10, 8, 7, 9, 14, 22, 38, 52, 64, 71, 74, 70, 65, 58, 52, 45, 38, 32, 28, 25, 23][hourArg] })) },
|
|
48
|
+
{ name: 'OAuth grants', data: hours.map((hourArg) => ({ x: now - (23 - hourArg) * 3600000, y: [16, 12, 9, 7, 6, 5, 7, 11, 18, 30, 42, 50, 56, 58, 56, 52, 47, 41, 36, 30, 26, 23, 20, 18][hourArg] })) },
|
|
49
|
+
],
|
|
50
|
+
threats: [
|
|
51
|
+
{ severity: 'error', title: 'Repeated denied logins', meta: '5 attempts · Lagos, NG · last 1h', action: 'Block IP' },
|
|
52
|
+
{ severity: 'warning', title: 'Stale device key', meta: 'sam@ · Apple Watch · 9 mo old', action: 'Rotate' },
|
|
53
|
+
],
|
|
54
|
+
regions: [
|
|
55
|
+
{ name: 'Berlin, DE', code: 'DE', count: 1284, share: 1, kind: 'home' },
|
|
56
|
+
{ name: 'Amsterdam, NL', code: 'NL', count: 642, share: 0.5 },
|
|
57
|
+
{ name: 'Lagos, NG', code: 'NG', count: 12, share: 0.01, kind: 'risk' },
|
|
58
|
+
],
|
|
59
|
+
feed: [
|
|
60
|
+
{ tone: 'info', text: 'Identity created · did:idp:0x9b12…f034', meta: 'block #9 841 222' },
|
|
61
|
+
{ tone: 'ok', text: 'Anchor confirmed · 12 blocks deep', meta: '2m' },
|
|
62
|
+
],
|
|
63
|
+
}}
|
|
64
|
+
></idp-admin-overview>
|
|
65
|
+
`;
|
|
66
|
+
};
|
|
67
|
+
public static demoGroups = ['idp.global v5 composed surfaces'];
|
|
68
|
+
|
|
69
|
+
public static styles = [
|
|
70
|
+
...idpElementStyles,
|
|
71
|
+
idpAdminSharedStyles,
|
|
72
|
+
css`
|
|
73
|
+
:host {
|
|
74
|
+
display: block;
|
|
75
|
+
background: var(--idp-bg);
|
|
76
|
+
color: var(--idp-fg);
|
|
77
|
+
}
|
|
78
|
+
.chart-card dees-chart-area {
|
|
79
|
+
display: block;
|
|
80
|
+
min-height: 240px;
|
|
81
|
+
}
|
|
82
|
+
.region-row {
|
|
83
|
+
display: grid;
|
|
84
|
+
grid-template-columns: 28px 1fr auto;
|
|
85
|
+
gap: 12px;
|
|
86
|
+
align-items: center;
|
|
87
|
+
padding: 8px 0;
|
|
88
|
+
}
|
|
89
|
+
.region-code {
|
|
90
|
+
font-family: var(--idp-mono);
|
|
91
|
+
font-size: 10px;
|
|
92
|
+
font-weight: 700;
|
|
93
|
+
text-align: center;
|
|
94
|
+
padding: 2px 0;
|
|
95
|
+
border: 1px solid var(--idp-border);
|
|
96
|
+
border-radius: 3px;
|
|
97
|
+
color: var(--idp-fg-3);
|
|
98
|
+
}
|
|
99
|
+
.region-code.home {
|
|
100
|
+
color: var(--idp-accent);
|
|
101
|
+
border-color: var(--sh-accent-ring);
|
|
102
|
+
background: var(--idp-accent-soft);
|
|
103
|
+
}
|
|
104
|
+
.region-code.risk {
|
|
105
|
+
color: var(--idp-error);
|
|
106
|
+
border-color: var(--idp-error-border);
|
|
107
|
+
background: var(--idp-error-bg);
|
|
108
|
+
}
|
|
109
|
+
.region-bar {
|
|
110
|
+
height: 4px;
|
|
111
|
+
margin-top: 4px;
|
|
112
|
+
border-radius: 2px;
|
|
113
|
+
background: var(--idp-muted);
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
}
|
|
116
|
+
.region-bar-fill {
|
|
117
|
+
height: 100%;
|
|
118
|
+
background: var(--idp-accent);
|
|
119
|
+
opacity: 0.85;
|
|
120
|
+
}
|
|
121
|
+
.region-bar-fill.risk {
|
|
122
|
+
background: var(--idp-error);
|
|
123
|
+
opacity: 0.8;
|
|
124
|
+
}
|
|
125
|
+
.region-count {
|
|
126
|
+
font-family: var(--idp-mono);
|
|
127
|
+
font-size: 11.5px;
|
|
128
|
+
color: var(--idp-fg);
|
|
129
|
+
min-width: 44px;
|
|
130
|
+
text-align: right;
|
|
131
|
+
}
|
|
132
|
+
.region-list {
|
|
133
|
+
padding: 4px 16px 8px;
|
|
134
|
+
}
|
|
135
|
+
.threat-row {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: 6px 1fr auto;
|
|
138
|
+
gap: 12px;
|
|
139
|
+
align-items: center;
|
|
140
|
+
padding: 12px 0;
|
|
141
|
+
border-bottom: 1px solid var(--idp-border-soft);
|
|
142
|
+
}
|
|
143
|
+
.threat-row:last-child {
|
|
144
|
+
border-bottom: none;
|
|
145
|
+
}
|
|
146
|
+
.threat-list {
|
|
147
|
+
padding: 0 16px;
|
|
148
|
+
}
|
|
149
|
+
.threat-dot {
|
|
150
|
+
width: 6px;
|
|
151
|
+
height: 6px;
|
|
152
|
+
border-radius: 3px;
|
|
153
|
+
}
|
|
154
|
+
.threat-title {
|
|
155
|
+
font-size: 12.5px;
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
color: var(--idp-fg);
|
|
158
|
+
}
|
|
159
|
+
.threat-meta {
|
|
160
|
+
font-family: var(--idp-mono);
|
|
161
|
+
font-size: 10.5px;
|
|
162
|
+
color: var(--idp-muted-fg);
|
|
163
|
+
margin-top: 2px;
|
|
164
|
+
}
|
|
165
|
+
.threat-action {
|
|
166
|
+
padding: 4px 10px;
|
|
167
|
+
border: 1px solid var(--idp-border);
|
|
168
|
+
border-radius: 6px;
|
|
169
|
+
background: var(--idp-bg);
|
|
170
|
+
color: var(--idp-fg);
|
|
171
|
+
font-family: var(--idp-font);
|
|
172
|
+
font-size: 11px;
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
white-space: nowrap;
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
}
|
|
177
|
+
`,
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
@property({ type: Object })
|
|
181
|
+
public accessor user: IIdpAdminUser = {
|
|
182
|
+
name: '',
|
|
183
|
+
email: '',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
@property({ type: Array })
|
|
187
|
+
public accessor orgs: IIdpAdminOrg[] = [];
|
|
188
|
+
|
|
189
|
+
@property({ type: Array })
|
|
190
|
+
public accessor sessions: IIdpAdminSession[] = [];
|
|
191
|
+
|
|
192
|
+
@property({ type: Array })
|
|
193
|
+
public accessor activities: IIdpAdminActivity[] = [];
|
|
194
|
+
|
|
195
|
+
@property({ type: Array })
|
|
196
|
+
public accessor passportDevices: IIdpAdminPassportDevice[] = [];
|
|
197
|
+
|
|
198
|
+
@property({ type: Boolean, attribute: 'data-loading', reflect: true })
|
|
199
|
+
public accessor dataLoading = false;
|
|
200
|
+
|
|
201
|
+
@property({ type: String, attribute: 'data-error' })
|
|
202
|
+
public accessor dataError = '';
|
|
203
|
+
|
|
204
|
+
@property({ type: Object })
|
|
205
|
+
public accessor overviewData: IIdpAdminOverviewData | null = null;
|
|
206
|
+
|
|
207
|
+
private navigate(pageArg: TIdpAdminPage) {
|
|
208
|
+
this.dispatchEvent(new CustomEvent<IIdpAdminNavigateEventDetail>('idp-admin-navigate', {
|
|
209
|
+
detail: { page: pageArg },
|
|
210
|
+
bubbles: true,
|
|
211
|
+
composed: true,
|
|
212
|
+
}));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private renderDataState(emptyTitleArg: string, emptyDescriptionArg: string, iconArg = 'box'): TemplateResult {
|
|
216
|
+
if (this.dataLoading) {
|
|
217
|
+
return renderAdminStateCard('Loading data', 'Fetching the latest account and organisation data.', 'cloud');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (this.dataError) {
|
|
221
|
+
return renderAdminStateCard('Data unavailable', this.dataError, 'alert');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return renderAdminStateCard(emptyTitleArg, emptyDescriptionArg, iconArg);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private renderInsights(): TemplateResult {
|
|
228
|
+
const data = this.overviewData;
|
|
229
|
+
if (!data?.chartSeries?.length && !data?.threats?.length) {
|
|
230
|
+
return html``;
|
|
231
|
+
}
|
|
232
|
+
const toneColor = (severityArg: string) => severityArg === 'error'
|
|
233
|
+
? 'var(--idp-error)'
|
|
234
|
+
: severityArg === 'warning' ? 'var(--idp-warn)' : 'var(--idp-info)';
|
|
235
|
+
return html`
|
|
236
|
+
<div class="primary-grid">
|
|
237
|
+
${data?.chartSeries?.length ? html`
|
|
238
|
+
<div class="card chart-card">
|
|
239
|
+
<div class="card-head"><span class="card-title">${data.chartLabel || 'Approval activity'}</span></div>
|
|
240
|
+
<dees-chart-area
|
|
241
|
+
.label=${data.chartLabel || 'Approval activity'}
|
|
242
|
+
.yAxisFormatter=${(valueArg: number) => `${Math.round(valueArg)}`}
|
|
243
|
+
.series=${data.chartSeries.map((seriesArg) => ({
|
|
244
|
+
name: seriesArg.name,
|
|
245
|
+
data: seriesArg.data.map((pointArg) => ({ x: pointArg.x, y: pointArg.y })),
|
|
246
|
+
}))}
|
|
247
|
+
></dees-chart-area>
|
|
248
|
+
</div>
|
|
249
|
+
` : html``}
|
|
250
|
+
${data?.threats?.length ? html`
|
|
251
|
+
<div class="card">
|
|
252
|
+
<div class="card-head"><span class="card-title">Needs attention</span><idp-badge variant="error">${data.threats.length} open</idp-badge></div>
|
|
253
|
+
<div class="threat-list">
|
|
254
|
+
${data.threats.map((threatArg) => html`
|
|
255
|
+
<div class="threat-row">
|
|
256
|
+
<span class="threat-dot" style="background:${toneColor(threatArg.severity)}"></span>
|
|
257
|
+
<div><div class="threat-title">${threatArg.title}</div><div class="threat-meta">${threatArg.meta}</div></div>
|
|
258
|
+
${threatArg.action ? html`<button class="threat-action">${threatArg.action}</button>` : html``}
|
|
259
|
+
</div>
|
|
260
|
+
`)}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
` : html``}
|
|
264
|
+
</div>
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private renderFooterRows(): TemplateResult {
|
|
269
|
+
const data = this.overviewData;
|
|
270
|
+
if (!data?.feed?.length && !data?.regions?.length) {
|
|
271
|
+
return html``;
|
|
272
|
+
}
|
|
273
|
+
const feedColor = (toneArg: string) => toneArg === 'ok'
|
|
274
|
+
? 'var(--idp-ok)'
|
|
275
|
+
: toneArg === 'warn' ? 'var(--idp-warn)' : toneArg === 'error' ? 'var(--idp-error)' : 'var(--idp-info)';
|
|
276
|
+
return html`
|
|
277
|
+
<div class="primary-grid">
|
|
278
|
+
${data?.feed?.length ? html`
|
|
279
|
+
<div class="card">
|
|
280
|
+
<div class="card-head"><span class="card-title">Cardano feed</span><idp-badge variant="accent">live</idp-badge></div>
|
|
281
|
+
${data.feed.map((itemArg) => html`
|
|
282
|
+
<div class="feed-item"><span class="feed-dot" style="--dot-color:${feedColor(itemArg.tone)}"></span><div class="feed-text">${itemArg.text}</div><span class="feed-meta">${itemArg.meta}</span></div>
|
|
283
|
+
`)}
|
|
284
|
+
</div>
|
|
285
|
+
` : html``}
|
|
286
|
+
${data?.regions?.length ? html`
|
|
287
|
+
<div class="card">
|
|
288
|
+
<div class="card-head"><span class="card-title">Sign-ins by region</span><span class="feed-meta">last 24h</span></div>
|
|
289
|
+
<div class="region-list">
|
|
290
|
+
${data.regions.map((regionArg) => html`
|
|
291
|
+
<div class="region-row">
|
|
292
|
+
<span class="region-code ${regionArg.kind || ''}">${regionArg.code}</span>
|
|
293
|
+
<div>
|
|
294
|
+
<div style="display:flex;align-items:center;gap:6px;font-size:12.5px;color:var(--idp-fg)">
|
|
295
|
+
${regionArg.name}
|
|
296
|
+
${regionArg.kind === 'risk' ? html`<idp-badge variant="error">flagged</idp-badge>` : html``}
|
|
297
|
+
${regionArg.kind === 'home' ? html`<idp-badge variant="accent">primary</idp-badge>` : html``}
|
|
298
|
+
</div>
|
|
299
|
+
<div class="region-bar"><div class="region-bar-fill ${regionArg.kind === 'risk' ? 'risk' : ''}" style="width:${Math.round(Math.min(1, Math.max(0, regionArg.share)) * 100)}%"></div></div>
|
|
300
|
+
</div>
|
|
301
|
+
<span class="region-count">${regionArg.count.toLocaleString()}</span>
|
|
302
|
+
</div>
|
|
303
|
+
`)}
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
` : html``}
|
|
307
|
+
</div>
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
public render(): TemplateResult {
|
|
312
|
+
const firstName = this.user?.name?.split(' ')[0] || 'there';
|
|
313
|
+
const activeSessionCount = this.sessions.filter((sessionArg) => sessionArg.isCurrent).length || this.sessions.length;
|
|
314
|
+
const activePassportCount = this.passportDevices.filter((deviceArg) => deviceArg.status === 'active').length;
|
|
315
|
+
const kpis: TIdpAdminKpi[] = [
|
|
316
|
+
{
|
|
317
|
+
label: 'Organisations',
|
|
318
|
+
value: String(this.orgs.length),
|
|
319
|
+
delta: 'live',
|
|
320
|
+
deltaKind: 'live',
|
|
321
|
+
spark: [1, 1, 1, 1, 1, 1, 1, Math.max(this.orgs.length, 1)],
|
|
322
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
323
|
+
accent: 'var(--idp-chart-1)',
|
|
324
|
+
sub: 'Memberships visible to this account',
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
label: 'Sessions',
|
|
328
|
+
value: String(this.sessions.length),
|
|
329
|
+
delta: `${activeSessionCount} active`,
|
|
330
|
+
deltaKind: 'up',
|
|
331
|
+
spark: [1, 2, 1, 2, 3, 2, Math.max(this.sessions.length, 1)],
|
|
332
|
+
sparkColor: 'var(--idp-spark-up)',
|
|
333
|
+
accent: 'var(--idp-chart-2)',
|
|
334
|
+
sub: this.sessions.length ? `Latest ${formatAdminTimeAgo(Math.max(...this.sessions.map((sessionArg) => sessionArg.lastActive)))}` : 'No active sessions loaded',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
label: 'Passport devices',
|
|
338
|
+
value: String(this.passportDevices.length),
|
|
339
|
+
delta: `${activePassportCount} active`,
|
|
340
|
+
deltaKind: 'live',
|
|
341
|
+
spark: [1, 1, 1, Math.max(this.passportDevices.length, 1)],
|
|
342
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
343
|
+
accent: 'var(--idp-chart-5)',
|
|
344
|
+
sub: 'Cryptographic credentials',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
label: 'Activity',
|
|
348
|
+
value: String(this.activities.length),
|
|
349
|
+
delta: 'live',
|
|
350
|
+
deltaKind: 'live',
|
|
351
|
+
spark: [1, 2, 2, 3, Math.max(this.activities.length, 1)],
|
|
352
|
+
sparkColor: 'var(--idp-spark-info)',
|
|
353
|
+
accent: 'var(--idp-info)',
|
|
354
|
+
sub: 'Recent account events',
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
return html`
|
|
358
|
+
<header class="page-head">
|
|
359
|
+
<div>
|
|
360
|
+
<div class="eyebrow-row"><span class="eyebrow">Account - Overview</span><span class="live-pill"><span class="live-dot"></span>live</span></div>
|
|
361
|
+
<h1>Good morning, ${firstName}.</h1>
|
|
362
|
+
<div class="lead">Account-wide operational snapshot using live identity data.</div>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="page-actions"><button class="plain-button outline" @click=${() => this.navigate('sessions')}>Review sessions</button><button class="plain-button primary" @click=${() => this.navigate('security')}>Manage security</button></div>
|
|
365
|
+
</header>
|
|
366
|
+
<div class="body">
|
|
367
|
+
<div class="kpis">${kpis.map((kpiArg) => renderAdminKpi(kpiArg))}</div>
|
|
368
|
+
${this.dataLoading || this.dataError ? this.renderDataState('No overview data', 'The console has not received live account data yet.', 'activity') : html``}
|
|
369
|
+
${this.renderInsights()}
|
|
370
|
+
<div class="primary-grid">
|
|
371
|
+
<div class="card">
|
|
372
|
+
<div class="card-head"><span class="card-title">Recent activity</span><idp-badge>${this.activities.length} events</idp-badge></div>
|
|
373
|
+
${this.activities.length ? this.activities.slice(0, 8).map((activityArg) => html`
|
|
374
|
+
<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">${formatAdminTimeAgo(activityArg.timestamp)}</span></div>
|
|
375
|
+
`) : renderAdminStateCard('No activity yet', 'Activity events will appear here after logins, app changes, and organisation updates.', 'activity')}
|
|
376
|
+
</div>
|
|
377
|
+
<div class="card">
|
|
378
|
+
<div class="card-head"><span class="card-title">Current sessions</span><idp-badge>${this.sessions.length} total</idp-badge></div>
|
|
379
|
+
${this.sessions.length ? this.sessions.slice(0, 5).map((sessionArg) => html`
|
|
380
|
+
<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">${formatAdminTimeAgo(sessionArg.lastActive)}</span></div>
|
|
381
|
+
`) : renderAdminStateCard('No sessions loaded', 'Active session telemetry is unavailable or there are no sessions.', 'monitor')}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
${this.renderFooterRows()}
|
|
385
|
+
</div>
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
}
|