286 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|