ui-nyla/src/pages/admin/AdminDemoConfigPage.tsx
ValueOn AG fc2cce8732 fixes
2026-04-23 23:09:54 +02:00

271 lines
9.8 KiB
TypeScript

/**
* AdminDemoConfigPage
*
* SysAdmin page for managing demo configurations.
* Lists available demo configs with Load / Remove actions.
*/
import React, { useState, useEffect, useCallback } from 'react';
import { FaPlay, FaTrash, FaSync, FaCubes, FaCopy, FaKey } from 'react-icons/fa';
import api from '../../api';
import styles from './Admin.module.css';
import demoStyles from './AdminDemoConfigPage.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { useConfirm } from '../../hooks/useConfirm';
interface _DemoCredential {
role?: string;
username?: string;
email?: string;
password?: string;
}
interface _DemoConfig {
code: string;
label: string;
description: string;
credentials?: _DemoCredential[];
}
interface _ActionResult {
code: string;
action: 'load' | 'remove';
status: 'ok' | 'error';
summary?: Record<string, unknown>;
credentials?: _DemoCredential[];
error?: string;
}
export const AdminDemoConfigPage: React.FC = () => {
const { t } = useLanguage();
const { confirm, ConfirmDialog } = useConfirm();
const [configs, setConfigs] = useState<_DemoConfig[]>([]);
const [loading, setLoading] = useState(false);
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
const [lastResult, setLastResult] = useState<_ActionResult | null>(null);
const [error, setError] = useState<string | null>(null);
const _fetchConfigs = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await api.get('/api/admin/demo-config');
setConfigs(response.data.configs || []);
} catch (err: any) {
setError(err.response?.data?.detail || t('Error loading demo configs'));
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
_fetchConfigs();
}, [_fetchConfigs]);
const _handleLoad = async (code: string) => {
if (actionInProgress) return;
setActionInProgress(code);
setLastResult(null);
try {
const response = await api.post(`/api/admin/demo-config/${code}/load`);
const summary = (response.data?.summary || {}) as Record<string, unknown>;
const credsFromSummary = Array.isArray(summary.credentials)
? (summary.credentials as _DemoCredential[])
: undefined;
const credsFromConfig = configs.find((c) => c.code === code)?.credentials;
setLastResult({
code,
action: 'load',
status: 'ok',
summary,
credentials: credsFromSummary ?? credsFromConfig,
});
} catch (err: any) {
setLastResult({ code, action: 'load', status: 'error', error: err.response?.data?.detail || String(err) });
} finally {
setActionInProgress(null);
}
};
const _handleRemove = async (code: string) => {
if (actionInProgress) return;
const ok = await confirm(
t('Alle Demo-Daten für diese Konfiguration wirklich entfernen?'),
{ confirmLabel: t('Entfernen'), cancelLabel: t('Abbrechen'), variant: 'danger' },
);
if (!ok) return;
setActionInProgress(code);
setLastResult(null);
try {
const response = await api.post(`/api/admin/demo-config/${code}/remove`);
setLastResult({ code, action: 'remove', status: 'ok', summary: response.data.summary });
} catch (err: any) {
setLastResult({ code, action: 'remove', status: 'error', error: err.response?.data?.detail || String(err) });
} finally {
setActionInProgress(null);
}
};
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('Demo-Konfigurationen')}</h1>
<p className={styles.pageSubtitle}>{t('Demo-Umgebungen für Präsentationen und Tests laden oder entfernen.')}</p>
</div>
<div className={styles.headerActions}>
<button className={styles.secondaryButton} onClick={_fetchConfigs} disabled={loading}>
<FaSync /> {t('Aktualisieren')}
</button>
</div>
</div>
{error && <div className={demoStyles.errorBanner}>{error}</div>}
{lastResult && (
<div className={lastResult.status === 'ok' ? demoStyles.successBanner : demoStyles.errorBanner}>
<strong>{lastResult.action === 'load' ? t('Geladen') : t('Entfernt')}:</strong>{' '}
{lastResult.status === 'ok' ? (
<_SummaryDisplay summary={lastResult.summary} />
) : (
<span>{lastResult.error}</span>
)}
{lastResult.status === 'ok' && lastResult.action === 'load' && lastResult.credentials && lastResult.credentials.length > 0 && (
<_CredentialsBox credentials={lastResult.credentials} />
)}
</div>
)}
{loading && configs.length === 0 ? (
<div className={demoStyles.loadingState}>{t('Lade…')}</div>
) : configs.length === 0 ? (
<div className={demoStyles.emptyState}>{t('Keine Demo-Konfigurationen gefunden.')}</div>
) : (
<div className={demoStyles.configGrid}>
{configs.map((cfg) => (
<div key={cfg.code} className={demoStyles.configCard}>
<div className={demoStyles.cardIcon}><FaCubes /></div>
<div className={demoStyles.cardContent}>
<h3 className={demoStyles.cardTitle}>{cfg.label}</h3>
<p className={demoStyles.cardDescription}>{cfg.description}</p>
<span className={demoStyles.cardCode}>{cfg.code}</span>
{cfg.credentials && cfg.credentials.length > 0 && (
<_CredentialsBox credentials={cfg.credentials} compact />
)}
</div>
<div className={demoStyles.cardActions}>
<button
className={demoStyles.loadButton}
onClick={() => _handleLoad(cfg.code)}
disabled={actionInProgress !== null}
>
{actionInProgress === cfg.code ? <FaSync className={demoStyles.spin} /> : <FaPlay />}
{t('Laden')}
</button>
<button
className={demoStyles.removeButton}
onClick={() => _handleRemove(cfg.code)}
disabled={actionInProgress !== null}
>
<FaTrash />
{t('Entfernen')}
</button>
</div>
</div>
))}
</div>
)}
<ConfirmDialog />
</div>
);
};
const _SummaryDisplay: React.FC<{ summary?: Record<string, unknown> }> = ({ summary }) => {
const { t } = useLanguage();
if (!summary) return null;
// Skip the credentials block here -- it gets its own copyable widget below.
const sections = Object.entries(summary)
.filter(([key]) => key !== 'credentials')
.filter(([, v]) => Array.isArray(v) && (v as unknown[]).length > 0);
if (sections.length === 0) return <span>{t('Abgeschlossen (keine Änderungen)')}</span>;
return (
<span>
{sections.map(([key, items]) => (
<span key={key} style={{ marginRight: 12 }}>
<strong>{key}:</strong> {(items as string[]).length}
</span>
))}
</span>
);
};
const _CredentialsBox: React.FC<{ credentials: _DemoCredential[]; compact?: boolean }> = ({ credentials, compact }) => {
const { t } = useLanguage();
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const _copy = async (key: string, value: string) => {
try {
await navigator.clipboard.writeText(value);
setCopiedKey(key);
window.setTimeout(() => setCopiedKey((prev) => (prev === key ? null : prev)), 1500);
} catch {
// ignore clipboard failures (no permission, http, ...)
}
};
return (
<div className={compact ? demoStyles.credentialsBoxCompact : demoStyles.credentialsBox}>
<div className={demoStyles.credentialsHeader}>
<FaKey />
<span>{t('Login-Daten')}</span>
</div>
{credentials.map((cred, idx) => {
// Login uses the USERNAME (the email is just informational metadata
// about the demo user -- the auth flow keys off `username`).
const loginValue = cred.username || '';
const pwd = cred.password || '';
const rowKey = `${idx}-${loginValue}`;
return (
<div key={rowKey} className={demoStyles.credentialsRow}>
{cred.role && <div className={demoStyles.credentialsRole}>{cred.role}</div>}
<div className={demoStyles.credentialsField}>
<span className={demoStyles.credentialsLabel}>{t('Login')}:</span>
<code>{loginValue}</code>
<button
type="button"
className={demoStyles.copyButton}
onClick={() => _copy(`${rowKey}-login`, loginValue)}
disabled={!loginValue}
title={t('Login kopieren')}
>
<FaCopy />
{copiedKey === `${rowKey}-login` ? ` ${t('kopiert')}` : ''}
</button>
</div>
<div className={demoStyles.credentialsField}>
<span className={demoStyles.credentialsLabel}>{t('Passwort')}:</span>
<code>{pwd}</code>
<button
type="button"
className={demoStyles.copyButton}
onClick={() => _copy(`${rowKey}-pwd`, pwd)}
disabled={!pwd}
title={t('Passwort kopieren')}
>
<FaCopy />
{copiedKey === `${rowKey}-pwd` ? ` ${t('kopiert')}` : ''}
</button>
</div>
{cred.email && (
<div className={demoStyles.credentialsField}>
<span className={demoStyles.credentialsLabel}>{t('E-Mail')}:</span>
<code>{cred.email}</code>
</div>
)}
</div>
);
})}
</div>
);
};