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` Users
({ ID: html`${user.id}`, Email: user.email || user.username, Name: user.name || '', Role: this.renderRoleBadge(user.role), Status: user.status || 'active', Auth: (user.authSources || []).join(', ') || 'bootstrap', Session: user.id === currentUserId ? html`current` : '', })} .dataActions=${[ { name: 'Create User', iconName: 'lucide:userPlus', type: ['header'], actionFunc: async () => await this.showCreateUserDialog(), }, { name: 'Delete', iconName: 'lucide:trash2', type: ['inRow', 'contextmenu'] as any, actionFunc: async (actionData: any) => { await this.showDeleteUserDialog(actionData.item as appstate.IUser); }, }, ]} >
`; } private renderRoleBadge(role: string): TemplateResult { const cls = role === 'admin' ? 'admin' : 'user'; return html`${role}`; } private async showCreateUserDialog(): Promise { const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: 'Create User', content: html` `, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, { name: 'Create', iconName: 'lucide:userPlus', action: async (modalArg: any) => { const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form') as any; if (!form) return; const data = await form.collectFormData(); const email = String(data.email || '').trim(); const name = String(data.name || '').trim(); const password = String(data.password || ''); const passwordConfirm = String(data.passwordConfirm || ''); const roleValue = String(data.role?.key ?? data.role ?? 'user'); if (!email || !password) { form.setStatus?.('error', 'Email and password are required.'); return; } if (password !== passwordConfirm) { form.setStatus?.('error', 'Passwords do not match.'); return; } form.setStatus?.('pending', 'Creating user...'); await appstate.usersStatePart.dispatchAction(appstate.createUserAction, { email, name, role: roleValue === 'admin' ? 'admin' : 'user', password, enableIdpGlobalAuth: Boolean(data.enableIdpGlobalAuth), }); const state = appstate.usersStatePart.getState(); if (state?.error) { form.setStatus?.('error', state.error); return; } DeesToast.show({ message: `User created for ${email}`, type: 'success', duration: 3000 }); await modalArg.destroy(); }, }, ], }); } private async showDeleteUserDialog(userArg: appstate.IUser): Promise { const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog'); const currentUserId = this.loginState.identity?.userId; if (userArg.id === currentUserId) { DeesToast.show({ message: 'You cannot delete the current user.', type: 'error', duration: 4000 }); return; } await DeesModal.createAndShow({ heading: 'Delete User', content: html`

Delete ${userArg.email || userArg.username}?

This removes the local dcrouter account and cannot be undone.

`, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => await modalArg.destroy(), }, { name: 'Delete', iconName: 'lucide:trash2', action: async (modalArg: any) => { await appstate.usersStatePart.dispatchAction(appstate.deleteUserAction, userArg.id); const state = appstate.usersStatePart.getState(); if (state?.error) { DeesToast.show({ message: state.error, type: 'error', duration: 4000 }); return; } DeesToast.show({ message: 'User deleted.', type: 'success', duration: 3000 }); await modalArg.destroy(); }, }, ], }); } async firstUpdated() { if (this.loginState.isLoggedIn) { await appstate.usersStatePart.dispatchAction(appstate.fetchUsersAction, null); } } }