feat(opsserver-access): add admin user listing to the access dashboard

This commit is contained in:
2026-04-08 09:01:08 +00:00
parent 6099563acd
commit 7971bd249e
21 changed files with 291 additions and 11 deletions

View File

@@ -1 +1,2 @@
export * from './ops-view-apitokens.js';
export * from './ops-view-users.js';

View File

@@ -100,7 +100,7 @@ export class OpsViewApiTokens extends DeesElement {
const { apiTokens } = this.routeState;
return html`
<dees-heading level="2">API Tokens</dees-heading>
<dees-heading level="hr">API Tokens</dees-heading>
<div class="apiTokensContainer">
<dees-table

View File

@@ -0,0 +1,140 @@
import * as appstate from '../../appstate.js';
import { viewHostCss } from '../shared/css.js';
import {
DeesElement,
css,
cssManager,
customElement,
html,
state,
type TemplateResult,
} from '@design.estate/dees-element';
@customElement('ops-view-users')
export class OpsViewUsers extends DeesElement {
@state() accessor usersState: appstate.IUsersState = {
users: [],
isLoading: false,
error: null,
lastUpdated: 0,
};
@state() accessor loginState: appstate.ILoginState = {
identity: null,
isLoggedIn: false,
};
constructor() {
super();
const usersSub = appstate.usersStatePart
.select((s) => s)
.subscribe((usersState) => {
this.usersState = usersState;
});
this.rxSubscriptions.push(usersSub);
const loginSub = appstate.loginStatePart
.select((s) => s)
.subscribe((loginState) => {
this.loginState = loginState;
// Re-fetch users when user logs in (fixes race condition where
// the view is created before authentication completes)
if (loginState.isLoggedIn) {
appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
}
});
this.rxSubscriptions.push(loginSub);
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
.usersContainer {
display: flex;
flex-direction: column;
gap: 24px;
}
.roleBadge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.roleBadge.admin {
background: ${cssManager.bdTheme('#fef3c7', '#451a03')};
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
}
.roleBadge.user {
background: ${cssManager.bdTheme('#e0f2fe', '#0c4a6e')};
color: ${cssManager.bdTheme('#075985', '#7dd3fc')};
}
.sessionBadge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
color: ${cssManager.bdTheme('#166534', '#4ade80')};
}
.userIdCell {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
font-size: 11px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
}
`,
];
public render(): TemplateResult {
const { users } = this.usersState;
const currentUserId = this.loginState.identity?.userId;
return html`
<dees-heading level="2">Users</dees-heading>
<div class="usersContainer">
<dees-table
.heading1=${'Users'}
.heading2=${'OpsServer user accounts'}
.data=${users}
.dataName=${'user'}
.searchable=${true}
.showColumnFilters=${true}
.displayFunction=${(user: appstate.IUser) => ({
ID: html`<span class="userIdCell">${user.id}</span>`,
Username: user.username,
Role: this.renderRoleBadge(user.role),
Session: user.id === currentUserId
? html`<span class="sessionBadge">current</span>`
: '',
})}
></dees-table>
</div>
`;
}
private renderRoleBadge(role: string): TemplateResult {
const cls = role === 'admin' ? 'admin' : 'user';
return html`<span class="roleBadge ${cls}">${role}</span>`;
}
async firstUpdated() {
if (this.loginState.isLoggedIn) {
await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null);
}
}
}

View File

@@ -60,7 +60,7 @@ export class OpsViewEmails extends DeesElement {
public render() {
return html`
<dees-heading level="2">Email Operations</dees-heading>
<dees-heading level="hr">Email Log</dees-heading>
<div class="viewContainer">
${this.currentView === 'detail' && this.selectedEmail
? html`

View File

@@ -347,6 +347,7 @@ export class OpsViewNetworkActivity extends DeesElement {
heading1="Recent Network Activity"
heading2="Recent network requests"
searchable
.showColumnFilters=${true}
.pagination=${true}
.paginationSize=${50}
dataName="request"
@@ -606,6 +607,8 @@ export class OpsViewNetworkActivity extends DeesElement {
}}
heading1="Top Connected IPs"
heading2="IPs with most active connections and bandwidth"
searchable
.showColumnFilters=${true}
.pagination=${false}
dataName="ip"
></dees-table>
@@ -656,6 +659,7 @@ export class OpsViewNetworkActivity extends DeesElement {
heading1="Backend Protocols"
heading2="Auto-detected backend protocols and connection pool health"
searchable
.showColumnFilters=${true}
.pagination=${false}
dataName="backend"
></dees-table>

View File

@@ -36,6 +36,7 @@ import { OpsViewEmailSecurity } from './email/ops-view-email-security.js';
// Access group
import { OpsViewApiTokens } from './access/ops-view-apitokens.js';
import { OpsViewUsers } from './access/ops-view-users.js';
// Security group
import { OpsViewSecurityOverview } from './security/ops-view-security-overview.js';
@@ -114,6 +115,7 @@ export class OpsDashboard extends DeesElement {
iconName: 'lucide:keyRound',
subViews: [
{ slug: 'apitokens', name: 'API Tokens', iconName: 'lucide:key', element: OpsViewApiTokens },
{ slug: 'users', name: 'Users', iconName: 'lucide:users', element: OpsViewUsers },
],
},
{

View File

@@ -159,7 +159,7 @@ export class OpsViewCertificates extends DeesElement {
const { summary } = this.certState;
return html`
<dees-heading level="2">Certificates</dees-heading>
<dees-heading level="hr">Certificates</dees-heading>
<div class="certificatesContainer">
${this.renderStatsTiles(summary)}

View File

@@ -39,7 +39,7 @@ export class OpsViewLogs extends DeesElement {
public render() {
return html`
<dees-heading level="2">Logs</dees-heading>
<dees-heading level="hr">Logs</dees-heading>
<dees-chart-log
.label=${'Application Logs'}

View File

@@ -57,7 +57,7 @@ export class OpsViewConfig extends DeesElement {
public render() {
return html`
<dees-heading level="2">Configuration</dees-heading>
<dees-heading level="hr">Configuration</dees-heading>
${this.configState.isLoading
? html`

View File

@@ -94,7 +94,7 @@ export class OpsViewOverview extends DeesElement {
public render() {
return html`
<dees-heading level="2">Overview</dees-heading>
<dees-heading level="hr">Stats</dees-heading>
${this.statsState.isLoading ? html`
<div class="loadingMessage">