234 lines
6.9 KiB
JavaScript
234 lines
6.9 KiB
JavaScript
import { Component } from 'react';
|
|
import Alert from '@mui/material/Alert';
|
|
import Box from '@mui/material/Box';
|
|
import CircularProgress from '@mui/material/CircularProgress';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import Stack from '@mui/material/Stack';
|
|
import Table from '@mui/material/Table';
|
|
import TableBody from '@mui/material/TableBody';
|
|
import TableCell from '@mui/material/TableCell';
|
|
import TableContainer from '@mui/material/TableContainer';
|
|
import TableRow from '@mui/material/TableRow';
|
|
import TextField from '@mui/material/TextField';
|
|
import Typography from '@mui/material/Typography';
|
|
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
|
import { enqueueSnackbar } from 'notistack';
|
|
import BackendError from '../BackendError';
|
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
|
import { backendErrorFrom } from '../../utils/backendError';
|
|
import { notifyError } from '../../utils/notify';
|
|
|
|
/**
|
|
* Settings that may be set via PUT /api/company/setting/{key}.
|
|
* Only these keys are accepted by the backend.
|
|
*/
|
|
const SETTABLE_KEYS = [
|
|
'ui.theme',
|
|
'keys.tavily',
|
|
'keys.openrouter',
|
|
'keys.serper',
|
|
];
|
|
|
|
const KEY_LABELS = {
|
|
'ui.theme': 'UI-Theme',
|
|
'keys.tavily': 'Tavily API-Key',
|
|
'keys.openrouter': 'OpenRouter API-Key',
|
|
'keys.serper': 'Serper API-Key',
|
|
};
|
|
|
|
function keyLabel(key) {
|
|
return KEY_LABELS[key] ?? key;
|
|
}
|
|
|
|
export default class SettingsKeysPanel extends Component {
|
|
static contextType = StorageManagerContext;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
status: 'loading',
|
|
error: null,
|
|
settings: [],
|
|
// key → { value, busy, saveError }
|
|
drafts: {},
|
|
};
|
|
this.handleRetry = this.handleRetry.bind(this);
|
|
this.handleSave = this.handleSave.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
void this.load();
|
|
}
|
|
|
|
async load() {
|
|
this.setState({ status: 'loading', error: null });
|
|
try {
|
|
const data = await this.context.setting.list();
|
|
requireObject(data, 'GET /api/company/setting');
|
|
const settings = requireArray(data.settings, 'settings');
|
|
const drafts = Object.fromEntries(
|
|
settings.map((s) => [
|
|
s.key,
|
|
{
|
|
value: s.value ?? '',
|
|
busy: false,
|
|
saveError: null,
|
|
},
|
|
]),
|
|
);
|
|
this.setState({ status: 'ready', settings, drafts });
|
|
} catch (err) {
|
|
notifyError(err);
|
|
this.setState({ status: 'failed', error: backendErrorFrom(err) });
|
|
}
|
|
}
|
|
|
|
handleRetry() {
|
|
void this.load();
|
|
}
|
|
|
|
handleChange(key, field, value) {
|
|
this.setState((prev) => ({
|
|
drafts: {
|
|
...prev.drafts,
|
|
[key]: { ...prev.drafts[key], [field]: value, saveError: null },
|
|
},
|
|
}));
|
|
}
|
|
|
|
async handleSave(key) {
|
|
const draft = this.state.drafts[key];
|
|
if (!draft || draft.busy) return;
|
|
const row = this.state.settings.find((s) => s.key === key);
|
|
if (!row) return;
|
|
|
|
const body = {};
|
|
if (draft.value !== (row.value ?? '')) {
|
|
body.value = draft.value;
|
|
}
|
|
|
|
if (Object.keys(body).length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.setState((prev) => ({
|
|
drafts: {
|
|
...prev.drafts,
|
|
[key]: { ...prev.drafts[key], busy: true, saveError: null },
|
|
},
|
|
}));
|
|
try {
|
|
const data = await this.context.setting.upsert(key, body);
|
|
requireObject(data, 'PUT /api/company/setting/:key');
|
|
requireObject(data.setting, 'setting');
|
|
const updated = requireDefined(data.setting, 'setting');
|
|
|
|
const settings = this.state.settings.map((s) =>
|
|
s.key === key ? { ...s, ...body } : s,
|
|
);
|
|
this.setState({
|
|
settings,
|
|
drafts: {
|
|
...this.state.drafts,
|
|
[key]: {
|
|
value: updated.value ?? '',
|
|
busy: false,
|
|
saveError: null,
|
|
},
|
|
},
|
|
});
|
|
enqueueSnackbar('Einstellung gespeichert.', { variant: 'success' });
|
|
} catch (err) {
|
|
if (this._unmounted) return;
|
|
this.setState((prev) => ({
|
|
drafts: {
|
|
...prev.drafts,
|
|
[key]: { ...prev.drafts[key], busy: false, saveError: backendErrorFrom(err) },
|
|
},
|
|
}));
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._unmounted = true;
|
|
}
|
|
|
|
render() {
|
|
const { status, error, settings, drafts } = this.state;
|
|
|
|
if (status === 'loading') {
|
|
return (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
);
|
|
}
|
|
if (status === 'failed') {
|
|
return <BackendError error={error} onRetry={this.handleRetry} />;
|
|
}
|
|
if (settings.length === 0) {
|
|
return (
|
|
<Typography variant="body2" color="text.secondary">
|
|
Keine Einstellungen.
|
|
</Typography>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableBody>
|
|
{SETTABLE_KEYS.map((key) => {
|
|
const row = settings.find((s) => s.key === key);
|
|
const draft = row ? drafts[row.key] : null;
|
|
if (!row || !draft) return null;
|
|
return (
|
|
<TableRow key={row.key} hover>
|
|
<TableCell>
|
|
<Typography variant="body2">{keyLabel(row.key)}</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TextField
|
|
size="small"
|
|
value={draft.value}
|
|
disabled={draft.busy}
|
|
onChange={(e) => this.handleChange(row.key, 'value', e.target.value)}
|
|
autoComplete="off"
|
|
/>
|
|
</TableCell>
|
|
<TableCell align="right">
|
|
<Stack
|
|
direction="row"
|
|
spacing={0}
|
|
sx={{ justifyContent: 'flex-end', alignItems: 'center' }}
|
|
>
|
|
{draft.saveError ? (
|
|
<Box sx={{ maxWidth: 220, mr: 1 }}>
|
|
<Alert severity="error" sx={{ py: 0 }}>
|
|
{typeof draft.saveError === 'string'
|
|
? draft.saveError
|
|
: draft.saveError.message}
|
|
</Alert>
|
|
</Box>
|
|
) : null}
|
|
<IconButton
|
|
size="small"
|
|
aria-label="Speichern"
|
|
color="primary"
|
|
disabled={draft.busy}
|
|
onClick={() => void this.handleSave(row.key)}
|
|
>
|
|
<SaveOutlinedIcon fontSize="small" />
|
|
</IconButton>
|
|
</Stack>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
);
|
|
}
|
|
} |