dashboard-nanobot/frontend/src/modules/dashboard/components/DashboardEnvParamsModal.tsx

286 lines
11 KiB
TypeScript

import { useMemo, useState } from 'react';
import { Plus, Save, Trash2, X } from 'lucide-react';
import { DrawerShell } from '../../../components/DrawerShell';
import { PasswordInput } from '../../../components/PasswordInput';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import './DashboardManagementModals.css';
import './DashboardSupportModals.css';
interface CommonModalLabels {
cancel: string;
close: string;
save: string;
}
interface EnvParamsModalLabels extends CommonModalLabels {
addEnvParam: string;
envDraftPlaceholderKey: string;
envDraftPlaceholderValue: string;
envParams: string;
envParamsDesc: string;
envParamsHint: string;
envValue: string;
hideEnvValue: string;
noEnvParams: string;
removeEnvParam: string;
showEnvValue: string;
}
interface EnvParamsModalProps {
open: boolean;
envEntries: Array<[string, string]>;
envDraftKey: string;
envDraftValue: string;
labels: EnvParamsModalLabels;
onClose: () => void;
onCreateEnvParam: (key: string, value: string) => Promise<boolean> | boolean;
onDeleteEnvParam: (key: string) => Promise<boolean> | boolean;
onEnvDraftKeyChange: (value: string) => void;
onEnvDraftValueChange: (value: string) => void;
onSaveEnvParam: (originalKey: string, nextKey: string, nextValue: string) => Promise<boolean> | boolean;
}
export function EnvParamsModal({
open,
envEntries,
envDraftKey,
envDraftValue,
labels,
onClose,
onCreateEnvParam,
onDeleteEnvParam,
onEnvDraftKeyChange,
onEnvDraftValueChange,
onSaveEnvParam,
}: EnvParamsModalProps) {
const [createPanelOpen, setCreatePanelOpen] = useState(false);
const [envEditDrafts, setEnvEditDrafts] = useState<Record<string, { key: string; value: string }>>({});
const resetLocalState = () => {
setCreatePanelOpen(false);
setEnvEditDrafts({});
};
const mergedEnvDrafts = useMemo(() => {
const nextDrafts: Record<string, { key: string; value: string }> = {};
envEntries.forEach(([key, value]) => {
nextDrafts[key] = envEditDrafts[key] || { key, value };
});
return nextDrafts;
}, [envEditDrafts, envEntries]);
if (!open) return null;
return (
<DrawerShell
open={open}
onClose={() => {
resetLocalState();
onClose();
}}
title={labels.envParams}
size="standard"
bodyClassName="ops-config-drawer-body"
closeLabel={labels.close}
footer={(
!createPanelOpen ? (
<div className="drawer-shell-footer-content">
<span className="drawer-shell-footer-main field-label">{labels.envParamsHint}</span>
<button className="btn btn-primary" onClick={() => setCreatePanelOpen(true)}>
<Plus size={14} />
<span style={{ marginLeft: 6 }}>{labels.addEnvParam}</span>
</button>
</div>
) : undefined
)}
>
<div className="ops-config-modal">
<div className="wizard-channel-list ops-config-list-scroll">
{envEntries.length === 0 ? (
<div className="ops-empty-inline">{labels.noEnvParams}</div>
) : (
envEntries.map(([key, value]) => {
const draft = mergedEnvDrafts[key] || { key, value };
return (
<div key={key} className="card wizard-channel-card wizard-channel-compact">
<div className="ops-config-card-header">
<div className="ops-config-card-main">
<strong className="mono">{draft.key || key}</strong>
<div className="ops-config-collapsed-meta">{labels.envValue}</div>
</div>
<div className="ops-config-card-actions">
<LucentIconButton
className="btn btn-danger btn-sm wizard-icon-btn"
onClick={async () => {
const deleted = await onDeleteEnvParam(key);
if (!deleted) return;
setEnvEditDrafts((prev) => {
if (!(key in prev)) return prev;
const next = { ...prev };
delete next[key];
return next;
});
}}
tooltip={labels.removeEnvParam}
aria-label={labels.removeEnvParam}
>
<Trash2 size={14} />
</LucentIconButton>
</div>
</div>
<div className="ops-topic-grid">
<div className="ops-config-field">
<label className="field-label">{labels.envDraftPlaceholderKey}</label>
<input
className="input mono"
value={draft.key}
onChange={(e) => {
const nextKey = e.target.value.toUpperCase();
setEnvEditDrafts((prev) => ({
...prev,
[key]: {
...(prev[key] || { key, value }),
key: nextKey,
},
}));
}}
placeholder={labels.envDraftPlaceholderKey}
autoComplete="off"
/>
</div>
<div className="ops-config-field">
<label className="field-label">{labels.envValue}</label>
<PasswordInput
className="input"
value={draft.value}
onChange={(e) => {
const nextValue = e.target.value;
setEnvEditDrafts((prev) => ({
...prev,
[key]: {
...(prev[key] || { key, value }),
value: nextValue,
},
}));
}}
placeholder={labels.envValue}
autoComplete="off"
wrapperClassName="is-inline"
toggleLabels={{
show: labels.showEnvValue,
hide: labels.hideEnvValue,
}}
/>
</div>
</div>
<div className="row-between ops-config-footer">
<span className="field-label">{labels.envParamsHint}</span>
<button
className="btn btn-primary btn-sm"
onClick={async () => {
const saved = await onSaveEnvParam(key, draft.key, draft.value);
if (!saved) return;
setEnvEditDrafts((prev) => {
if (!(key in prev)) return prev;
const next = { ...prev };
delete next[key];
return next;
});
}}
>
<Save size={14} />
<span style={{ marginLeft: 6 }}>{labels.save}</span>
</button>
</div>
</div>
);
})
)}
</div>
{createPanelOpen ? (
<div className="card wizard-channel-card wizard-channel-compact ops-config-new-card">
<div className="ops-config-card-header">
<div className="ops-config-card-main">
<strong>{labels.addEnvParam}</strong>
<div className="ops-config-collapsed-meta">{labels.envParamsHint}</div>
</div>
<div className="ops-config-card-actions">
<LucentIconButton
className="ops-plain-icon-btn"
onClick={() => {
resetLocalState();
onEnvDraftKeyChange('');
onEnvDraftValueChange('');
}}
tooltip={labels.cancel}
aria-label={labels.cancel}
>
<X size={15} />
</LucentIconButton>
</div>
</div>
<div className="ops-topic-grid">
<div className="ops-config-field">
<label className="field-label">{labels.envDraftPlaceholderKey}</label>
<input
className="input mono"
value={envDraftKey}
onChange={(e) => onEnvDraftKeyChange(e.target.value.toUpperCase())}
placeholder={labels.envDraftPlaceholderKey}
autoComplete="off"
/>
</div>
<div className="ops-config-field">
<label className="field-label">{labels.envDraftPlaceholderValue}</label>
<PasswordInput
className="input"
value={envDraftValue}
onChange={(e) => onEnvDraftValueChange(e.target.value)}
placeholder={labels.envDraftPlaceholderValue}
autoComplete="off"
wrapperClassName="is-inline"
toggleLabels={{
show: labels.showEnvValue,
hide: labels.hideEnvValue,
}}
/>
</div>
</div>
<div className="row-between ops-config-footer">
<span className="field-label">{labels.envParamsHint}</span>
<div className="ops-inline-actions ops-inline-actions-wrap ops-inline-actions-end">
<button
className="btn btn-secondary btn-sm"
onClick={() => {
setCreatePanelOpen(false);
onEnvDraftKeyChange('');
onEnvDraftValueChange('');
}}
>
{labels.cancel}
</button>
<button
className="btn btn-primary btn-sm"
onClick={async () => {
const key = String(envDraftKey || '').trim().toUpperCase();
if (!key) return;
const saved = await onCreateEnvParam(key, envDraftValue);
if (!saved) return;
onEnvDraftKeyChange('');
onEnvDraftValueChange('');
setCreatePanelOpen(false);
}}
>
<Save size={14} />
<span style={{ marginLeft: 6 }}>{labels.save}</span>
</button>
</div>
</div>
</div>
) : null}
</div>
</DrawerShell>
);
}