Use managed secret controls for Files connectors

This commit is contained in:
2026-07-21 12:13:00 +02:00
parent f2dfb6c90e
commit 8826cf2890
5 changed files with 27 additions and 78 deletions

View File

@@ -15,6 +15,8 @@ import {
LoadingFrame,
mergeDeltaRows,
SegmentedControl,
StatusBadge,
TableActionGroup,
ToggleSwitch,
useDeltaWatermarks,
useUnsavedDraftGuard,
@@ -65,8 +67,6 @@ type ConnectorProfileDraft = {
username: string;
password: string;
token: string;
passwordEnv: string;
tokenEnv: string;
secretRef: string;
canBrowse: boolean;
canImport: boolean;
@@ -91,8 +91,6 @@ type ConnectorCredentialDraft = {
username: string;
password: string;
token: string;
passwordEnv: string;
tokenEnv: string;
secretRef: string;
policyMode: PolicyMode;
allowedPaths: string;
@@ -491,8 +489,6 @@ export default function FileConnectorSettingsPanel({
username: cleanOrNull(credentialDraft.username),
password: cleanOrUndefined(credentialDraft.password),
token: cleanOrUndefined(credentialDraft.token),
password_env: cleanOrNull(credentialDraft.passwordEnv),
token_env: cleanOrNull(credentialDraft.tokenEnv),
secret_ref: cleanOrNull(credentialDraft.secretRef)
};
const sharedPayload = {
@@ -633,8 +629,6 @@ export default function FileConnectorSettingsPanel({
username: cleanOrNull(draft.username),
password: cleanOrUndefined(draft.password),
token: cleanOrUndefined(draft.token),
password_env: cleanOrNull(draft.passwordEnv),
token_env: cleanOrNull(draft.tokenEnv),
secret_ref: cleanOrNull(draft.secretRef)
},
metadata: metadataFromDraft(draft)
@@ -680,8 +674,6 @@ export default function FileConnectorSettingsPanel({
username: cleanOrNull(credentialDraft.username),
password: cleanOrUndefined(credentialDraft.password),
token: cleanOrUndefined(credentialDraft.token),
password_env: cleanOrNull(credentialDraft.passwordEnv),
token_env: cleanOrNull(credentialDraft.tokenEnv),
secret_ref: cleanOrNull(credentialDraft.secretRef)
},
metadata: profile.metadata ?? {},
@@ -756,7 +748,7 @@ export default function FileConnectorSettingsPanel({
width: "120px",
render: (row) => {
const enabled = row.kind === "profile" ? row.profile.enabled : row.credential.enabled;
return <span className={`status-badge ${enabled ? "success" : "neutral"}`}>{enabled ? "i18n:govoplan-files.enabled.df174a3f" : "i18n:govoplan-files.disabled.f4f4473d"}</span>;
return <StatusBadge status={enabled ? "success" : "inactive"} label={enabled ? "i18n:govoplan-files.enabled.df174a3f" : "i18n:govoplan-files.disabled.f4f4473d"} />;
}
}];
@@ -771,21 +763,19 @@ export default function FileConnectorSettingsPanel({
function renderConnectorActions(row: ConnectorTreeRow) {
if (row.kind === "credential") {
const readOnly = row.credential.source_kind !== "database";
return (
<div className="admin-icon-actions">
<Button type="button" className="admin-icon-button" title={i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.credential.label })} aria-label={i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.credential.label })} onClick={() => startCredentialEdit(row.credential)} disabled={!canWrite || readOnly || saving}><Edit3 size={15} /></Button>
<Button type="button" variant="danger" className="admin-icon-button" title={i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.credential.label })} aria-label={i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.credential.label })} onClick={() => setPendingCredentialDeactivate(row.credential)} disabled={!canWrite || readOnly || saving || !row.credential.enabled}><Trash2 size={15} /></Button>
</div>);
return <TableActionGroup actions={[
{ id: "edit", label: i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.credential.label }), icon: <Edit3 size={15} />, disabled: !canWrite || readOnly || saving, onClick: () => startCredentialEdit(row.credential) },
{ id: "disable", label: i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.credential.label }), icon: <Trash2 size={15} />, variant: "danger", applicable: row.credential.enabled, disabled: !canWrite || readOnly || saving, onClick: () => setPendingCredentialDeactivate(row.credential) }
]} />;
}
const readOnly = row.profile.source_kind !== "database";
return (
<div className="admin-icon-actions">
<Button type="button" className="admin-icon-button" title={i18nMessage("i18n:govoplan-files.test_value.6a5d10a5", { value0: row.profile.label })} aria-label={i18nMessage("i18n:govoplan-files.test_value.6a5d10a5", { value0: row.profile.label })} onClick={() => void testBrowse(row.profile)} disabled={saving || testingProfileId === row.profile.id || !row.profile.enabled}><RefreshCw size={15} /></Button>
<Button type="button" variant="primary" className="admin-icon-button" title={i18nMessage("i18n:govoplan-files.add_credential_for_value.0fa9c1fe", { value0: row.profile.label })} aria-label={i18nMessage("i18n:govoplan-files.add_credential_for_value.0fa9c1fe", { value0: row.profile.label })} onClick={() => startCredentialCreate(row.profile)} disabled={!canWrite || saving}><UserRoundKey size={15} /></Button>
<Button type="button" className="admin-icon-button" title={i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.profile.label })} aria-label={i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.profile.label })} onClick={() => startEdit(row.profile)} disabled={!canWrite || readOnly || saving}><Edit3 size={15} /></Button>
<Button type="button" variant="danger" className="admin-icon-button" title={i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.profile.label })} aria-label={i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.profile.label })} onClick={() => setPendingDeactivate(row.profile)} disabled={!canWrite || readOnly || saving || !row.profile.enabled}><Trash2 size={15} /></Button>
</div>);
return <TableActionGroup actions={[
{ id: "test", label: i18nMessage("i18n:govoplan-files.test_value.6a5d10a5", { value0: row.profile.label }), icon: <RefreshCw size={15} />, applicable: row.profile.enabled, disabled: saving || testingProfileId === row.profile.id, onClick: () => void testBrowse(row.profile) },
{ id: "add-credential", label: i18nMessage("i18n:govoplan-files.add_credential_for_value.0fa9c1fe", { value0: row.profile.label }), icon: <UserRoundKey size={15} />, variant: "primary", disabled: !canWrite || saving, onClick: () => startCredentialCreate(row.profile) },
{ id: "edit", label: i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.profile.label }), icon: <Edit3 size={15} />, disabled: !canWrite || readOnly || saving, onClick: () => startEdit(row.profile) },
{ id: "disable", label: i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.profile.label }), icon: <Trash2 size={15} />, variant: "danger", applicable: row.profile.enabled, disabled: !canWrite || readOnly || saving, onClick: () => setPendingDeactivate(row.profile) }
]} />;
}
@@ -865,7 +855,7 @@ export default function FileConnectorSettingsPanel({
<LoadingFrame loading={loading} label="i18n:govoplan-files.loading_connector_policy.f12ab285">
<>
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.allowed_connection_ids.0df854c8">
<textarea value={policyDraft.allowedConnectors} disabled={saving || !canWrite} onChange={(event) => patchPolicyDraft({ allowedConnectors: event.target.value })} rows={3} placeholder={"tenant-webdav\nseafile-*"} />
</FormField>
@@ -940,7 +930,7 @@ export default function FileConnectorSettingsPanel({
<h3>Credential</h3>
<p>Choose where this credential can be used and how it signs in.</p>
</header>
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.credential_id.9432a6e1">
<input className={!editingCredentialId && !credentialDraft.id.trim() ? "field-input-missing" : undefined} aria-invalid={!editingCredentialId && !credentialDraft.id.trim() || undefined} value={credentialDraft.id} disabled={Boolean(editingCredentialId) || saving} onChange={(event) => patchCredentialDraft({ id: event.target.value })} placeholder={`${scopeType}-webdav-credentials`} />
</FormField>
@@ -1018,7 +1008,7 @@ export default function FileConnectorSettingsPanel({
onChange={patchCredentialValues}
disabled={saving}
showPassword={false} />
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.secret_reference.04ed2221">
<input value={credentialDraft.secretRef} disabled={saving} onChange={(event) => patchCredentialDraft({ secretRef: event.target.value })} />
</FormField>
@@ -1049,16 +1039,6 @@ export default function FileConnectorSettingsPanel({
{credentialLoginResult.message}
</DismissibleAlert>
}
<AdvancedOptionsPanel title="Environment and fallback secrets" summary="Use these only when the deployment injects secrets outside the database.">
<div className="form-grid two-column-form-grid">
<FormField label="i18n:govoplan-files.password_environment_variable.0e77673f">
<input value={credentialDraft.passwordEnv} disabled={saving} onChange={(event) => patchCredentialDraft({ passwordEnv: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-files.token_environment_variable.5af4a79e">
<input value={credentialDraft.tokenEnv} disabled={saving} onChange={(event) => patchCredentialDraft({ tokenEnv: event.target.value })} />
</FormField>
</div>
</AdvancedOptionsPanel>
</section>
<section className="adaptive-config-section">
@@ -1066,7 +1046,7 @@ export default function FileConnectorSettingsPanel({
<h3>Policy</h3>
<p>Limit where lower levels may use this credential.</p>
</header>
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.policy.bb9cf141">
<select value={credentialDraft.policyMode} disabled={saving} onChange={(event) => patchCredentialDraft({ policyMode: event.target.value as PolicyMode })}>
<option value="allow-provider">i18n:govoplan-files.can_use_this_credential.f4a41971</option>
@@ -1118,7 +1098,7 @@ export default function FileConnectorSettingsPanel({
<h3>Connection</h3>
<p>Name the file server connection and choose the provider.</p>
</header>
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.profile_id.25961d68">
<input className={!editingProfileId && !draft.id.trim() ? "field-input-missing" : undefined} aria-invalid={!editingProfileId && !draft.id.trim() || undefined} value={draft.id} disabled={Boolean(editingProfileId) || saving} onChange={(event) => patchDraft({ id: event.target.value })} placeholder={`${scopeType}-webdav`} />
</FormField>
@@ -1151,7 +1131,7 @@ export default function FileConnectorSettingsPanel({
<h3>Location and credentials</h3>
<p>Only fields relevant for the selected provider and credential mode are shown.</p>
</header>
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.endpoint_url.65aaaa45">
<input value={draft.endpointUrl} disabled={saving} onChange={(event) => patchDraft({ endpointUrl: event.target.value })} placeholder={ENDPOINT_PLACEHOLDERS[draft.provider]} />
</FormField>
@@ -1188,7 +1168,7 @@ export default function FileConnectorSettingsPanel({
</DismissibleAlert>
}
<AdvancedOptionsPanel title="Protocol and compatibility options" summary="Change these only when the provider or network requires an override.">
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.browse_protocol.d1f27c90">
<select value={draft.browseProtocol} disabled={saving} onChange={(event) => patchDraft({ browseProtocol: event.target.value })}>
<option value="">i18n:govoplan-files.native_default.ba32f7b6</option>
@@ -1222,7 +1202,7 @@ export default function FileConnectorSettingsPanel({
<ToggleSwitch label="i18n:govoplan-files.import.d6fbc9d2" checked={draft.canImport} disabled={saving} onChange={(canImport) => patchDraft({ canImport })} />
<ToggleSwitch label="i18n:govoplan-files.sync.905f6309" checked={draft.canSync} disabled={saving} onChange={(canSync) => patchDraft({ canSync })} />
</div>
<div className="form-grid two-column-form-grid">
<div className="form-grid two">
<FormField label="i18n:govoplan-files.policy.bb9cf141">
<select value={draft.policyMode} disabled={saving} onChange={(event) => patchDraft({ policyMode: event.target.value as PolicyMode })}>
<option value="allow-provider">i18n:govoplan-files.can_use_this_connection.5091a74c</option>
@@ -1373,8 +1353,6 @@ function emptyDraft(scopeType: ConnectorScope): ConnectorProfileDraft {
username: "",
password: "",
token: "",
passwordEnv: "",
tokenEnv: "",
secretRef: "",
canBrowse: true,
canImport: true,
@@ -1437,8 +1415,6 @@ function emptyCredentialDraft(scopeType: ConnectorScope): ConnectorCredentialDra
username: "",
password: "",
token: "",
passwordEnv: "",
tokenEnv: "",
secretRef: "",
policyMode: "allow-provider",
allowedPaths: "",