import { useEffect, useMemo, useState } from "react"; import { Edit3, Plus, RefreshCw, Save, ShieldCheck, Trash2, UserRoundKey } from "lucide-react"; import { Button, Card, ConnectionTree, ConfirmDialog, DismissibleAlert, Dialog, FormField, ActionBlockerHint, AdvancedOptionsPanel, LoadingFrame, mergeDeltaRows, SegmentedControl, ToggleSwitch, useDeltaWatermarks, useUnsavedDraftGuard, type ApiSettings, type ConnectionTreeColumn, type FileConnectorScope, type FileConnectorTargetOption, i18nMessage } from "@govoplan/core-webui"; import { browseFileConnectorProfile, createFileConnectorCredential, createFileConnectorProfile, deactivateFileConnectorCredential, deactivateFileConnectorProfile, discoverFileConnectorEndpoint, fetchFileConnectorSettingsDelta, updateFileConnectorPolicy, updateFileConnectorCredential, updateFileConnectorProfile, type FileConnectorCredential, type FileConnectorCredentialPayload, type FileConnectorCredentialUpdatePayload, type FileConnectorPolicyResponse, type FileConnectorProfile, type FileConnectorProfilePayload, type FileConnectorProfileUpdatePayload } from "../../api/files"; type ConnectorScope = FileConnectorScope; type Provider = FileConnectorProfilePayload["provider"]; type CredentialMode = NonNullable; type PolicyMode = "allow-provider" | "allowed-paths" | "deny-provider"; type ConnectorTreeRow = {kind: "profile";id: string;profile: FileConnectorProfile;} | {kind: "credential";id: string;credential: FileConnectorCredential;profile: FileConnectorProfile;}; type ConnectorProfileDraft = { id: string; label: string; provider: Provider; endpointUrl: string; basePath: string; enabled: boolean; credentialProfileId: string; credentialMode: CredentialMode; username: string; password: string; token: string; passwordEnv: string; tokenEnv: string; secretRef: string; canBrowse: boolean; canImport: boolean; canSync: boolean; policyMode: PolicyMode; allowedPaths: string; deniedPaths: string; requireSigning: boolean; authProtocol: string; browseProtocol: string; metadataJson: string; clearPassword: boolean; clearToken: boolean; }; type ConnectorCredentialDraft = { id: string; label: string; provider: Provider | ""; enabled: boolean; credentialMode: CredentialMode; username: string; password: string; token: string; passwordEnv: string; tokenEnv: string; secretRef: string; policyMode: PolicyMode; allowedPaths: string; deniedPaths: string; metadataJson: string; clearPassword: boolean; clearToken: boolean; }; type CentralConnectorPolicyDraft = { allowedConnectors: string; allowedCredentials: string; allowedProviders: string; allowedPaths: string; allowedUrls: string; deniedConnectors: string; deniedCredentials: string; deniedProviders: string; deniedPaths: string; deniedUrls: string; allowLowerConnectionLimits: boolean; allowLowerCredentialLimits: boolean; allowLowerProviderLimits: boolean; allowLowerPathLimits: boolean; allowLowerUrlLimits: boolean; }; const PROVIDERS: Array<{id: Provider;label: string;}> = [ { id: "webdav", label: "i18n:govoplan-files.webdav.521b4c8b" }, { id: "nextcloud", label: "i18n:govoplan-files.nextcloud.aab73a3d" }, { id: "seafile", label: "i18n:govoplan-files.seafile.600192cc" }, { id: "smb", label: "i18n:govoplan-files.smb.515d2f88" }, { id: "nfs", label: "i18n:govoplan-files.nfs.db121865" }, { id: "dms", label: "i18n:govoplan-files.dms.477e5652" }, { id: "generic", label: "i18n:govoplan-files.generic.ff7613e5" }]; const ENDPOINT_PLACEHOLDERS: Record = { webdav: "http://127.0.0.1:9083/", nextcloud: "http://127.0.0.1:9081/remote.php/dav/files/admin/", seafile: "http://127.0.0.1:9082/", smb: "smb://127.0.0.1:1445/files", nfs: "nfs://127.0.0.1/export", dms: "https://dms.example.test", generic: "https://files.example.test" }; export default function FileConnectorSettingsPanel({ settings, scopeType, scopeId = null, targetOptions = [], targetLabel = "i18n:govoplan-files.target.61ad50a9", title, canWrite }: {settings: ApiSettings;scopeType: ConnectorScope;scopeId?: string | null;targetOptions?: FileConnectorTargetOption[];targetLabel?: string;title?: string;canWrite: boolean;}) { const [profiles, setProfiles] = useState([]); const [credentials, setCredentials] = useState([]); const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || ""); const [policyResponse, setPolicyResponse] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [draft, setDraft] = useState(null); const [savedDraftKey, setSavedDraftKey] = useState(""); const [credentialDraft, setCredentialDraft] = useState(null); const [savedCredentialDraftKey, setSavedCredentialDraftKey] = useState(""); const [credentialAttachProfileId, setCredentialAttachProfileId] = useState(null); const [policyDraft, setPolicyDraft] = useState(emptyCentralPolicyDraft()); const [savedPolicyDraftKey, setSavedPolicyDraftKey] = useState(centralPolicyDraftKey(emptyCentralPolicyDraft())); const [editingProfileId, setEditingProfileId] = useState(null); const [editingCredentialId, setEditingCredentialId] = useState(null); const [pendingDeactivate, setPendingDeactivate] = useState(null); const [pendingCredentialDeactivate, setPendingCredentialDeactivate] = useState(null); const [testingProfileId, setTestingProfileId] = useState(null); const [testResults, setTestResults] = useState>({}); const [discoveringProfile, setDiscoveringProfile] = useState(false); const [profileDiscoveryResult, setProfileDiscoveryResult] = useState<{ok: boolean;message: string;} | null>(null); const [testingCredentialLogin, setTestingCredentialLogin] = useState(false); const [credentialLoginResult, setCredentialLoginResult] = useState<{ok: boolean;message: string;} | null>(null); const [message, setMessage] = useState(""); const [error, setError] = useState(""); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const requiresTarget = scopeType === "user" || scopeType === "group"; const targetSelectionRequired = requiresTarget && !scopeId; const hasSelectableTarget = targetOptions.length > 0; const activeScopeId = scopeId || selectedTargetId || null; const deltaKey = `files:connectors:${scopeType}:${activeScopeId ?? ""}`; const scopeReady = !requiresTarget || Boolean(activeScopeId); const targetEmptyText = i18nMessage("i18n:govoplan-files.no_value_available", { value0: targetPluralLabel(scopeType, targetLabel) }); const profileDirty = Boolean(draft) && connectorProfileDraftKey(draft) !== savedDraftKey; const credentialDirty = Boolean(credentialDraft) && connectorCredentialDraftKey(credentialDraft) !== savedCredentialDraftKey; const policyDirty = scopeReady && centralPolicyDraftKey(policyDraft) !== savedPolicyDraftKey; useUnsavedDraftGuard({ dirty: profileDirty || credentialDirty || policyDirty, onSave: saveDirtyDrafts, onDiscard: discardDirtyDrafts }); useEffect(() => { if (scopeId) { setSelectedTargetId(scopeId); return; } if (targetOptions.length > 0 && !targetOptions.some((option) => option.id === selectedTargetId)) { setSelectedTargetId(targetOptions[0].id); return; } if (targetOptions.length === 0 && selectedTargetId) setSelectedTargetId(""); }, [scopeId, selectedTargetId, targetOptions]); const scopedProfiles = useMemo( () => profiles.filter((profile) => connectorBelongsToEditableScope(profile, scopeType, activeScopeId)), [activeScopeId, profiles, scopeType] ); const scopedCredentials = useMemo( () => credentials.filter((credential) => connectorBelongsToEditableScope(credential, scopeType, activeScopeId)), [activeScopeId, credentials, scopeType] ); useEffect(() => { resetDeltaWatermark(deltaKey); void loadProfiles(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, scopeType, activeScopeId, scopeReady, deltaKey, resetDeltaWatermark]); async function loadProfiles(forceFull = false) { setLoading(true); setError(""); if (!scopeReady) { setProfiles([]); setCredentials([]); setPolicyResponse(null); setPolicyDraft(emptyCentralPolicyDraft()); setSavedPolicyDraftKey(centralPolicyDraftKey(emptyCentralPolicyDraft())); resetDeltaWatermark(deltaKey); setLoading(false); return; } try { let nextProfiles = forceFull ? [] : profiles; let nextCredentials = forceFull ? [] : credentials; let nextPolicyResponse = forceFull ? null : policyResponse; let nextWatermark = forceFull ? null : getDeltaWatermark(deltaKey); let hasMore = false; do { const response = await fetchFileConnectorSettingsDelta(settings, { scope_type: scopeType, scope_id: activeScopeId, include_disabled: true, since: nextWatermark }); nextProfiles = response.full ? response.profiles : mergeDeltaRows(nextProfiles, response.profiles, response.deleted, (profile) => profile.id, { deletedResourceType: "file_connector_profile", sort: sortConnectorProfiles }); nextCredentials = response.full ? response.credentials : mergeDeltaRows(nextCredentials, response.credentials, response.deleted, (credential) => credential.id, { deletedResourceType: "file_connector_credential", sort: sortConnectorCredentials }); if (response.policy) nextPolicyResponse = response.policy; nextWatermark = response.watermark ?? null; hasMore = response.has_more; } while (hasMore); setProfiles(nextProfiles); setCredentials(nextCredentials); setPolicyResponse(nextPolicyResponse); if (nextPolicyResponse) { const nextPolicyDraft = centralPolicyDraftFromPolicy(nextPolicyResponse.policy); setPolicyDraft(nextPolicyDraft); setSavedPolicyDraftKey(centralPolicyDraftKey(nextPolicyDraft)); } setDeltaWatermark(deltaKey, nextWatermark); } catch (err) { resetDeltaWatermark(deltaKey); setError(errorMessage(err)); } finally { setLoading(false); } } function startCreate() { setEditingProfileId(null); const nextDraft = emptyDraft(scopeType); setDraft(nextDraft); setSavedDraftKey(connectorProfileDraftKey(nextDraft)); setProfileDiscoveryResult(null); setMessage(""); setError(""); } function startCredentialCreate(profile: FileConnectorProfile) { setEditingCredentialId(null); setCredentialAttachProfileId(profile.id); const next = emptyCredentialDraft(scopeType); next.id = `${profile.id}-credentials`; next.label = `${profile.label} credentials`; next.provider = knownProvider(profile.provider); next.credentialMode = knownCredentialMode(profile.credential_mode); setCredentialDraft(next); setSavedCredentialDraftKey(connectorCredentialDraftKey(next)); setCredentialLoginResult(null); setMessage(""); setError(""); } function startEdit(profile: FileConnectorProfile) { setEditingProfileId(profile.id); const nextDraft = draftFromProfile(profile, scopeType); setDraft(nextDraft); setSavedDraftKey(connectorProfileDraftKey(nextDraft)); setProfileDiscoveryResult(null); setMessage(""); setError(""); } function startCredentialEdit(credential: FileConnectorCredential) { setEditingCredentialId(credential.id); setCredentialAttachProfileId(null); const nextDraft = credentialDraftFromCredential(credential, scopeType); setCredentialDraft(nextDraft); setSavedCredentialDraftKey(connectorCredentialDraftKey(nextDraft)); setCredentialLoginResult(null); setMessage(""); setError(""); } function patchDraft(patch: Partial) { setDraft((current) => current ? { ...current, ...patch } : current); } function patchCredentialDraft(patch: Partial) { setCredentialLoginResult(null); setCredentialDraft((current) => current ? { ...current, ...patch } : current); } function patchPolicyDraft(patch: Partial) { setPolicyDraft((current) => ({ ...current, ...patch })); } function closeProfileDialog() { if (saving) return; setDraft(null); setEditingProfileId(null); setProfileDiscoveryResult(null); } function closeCredentialDialog() { if (saving) return; setCredentialDraft(null); setEditingCredentialId(null); setCredentialAttachProfileId(null); setCredentialLoginResult(null); } async function savePolicyDraft(): Promise { if (!canWrite || !scopeReady) return false; setSaving(true); setError(""); try { const response = await updateFileConnectorPolicy(settings, scopeType, centralPolicyFromDraft(policyDraft), activeScopeId); setPolicyResponse(response); const savedPolicyDraft = centralPolicyDraftFromPolicy(response.policy); setPolicyDraft(savedPolicyDraft); setSavedPolicyDraftKey(centralPolicyDraftKey(savedPolicyDraft)); setMessage(i18nMessage("i18n:govoplan-files.value_connector_policy_saved.430d4eca", { value0: scopeLabel(scopeType) })); return true; } catch (err) { setError(errorMessage(err)); return false; } finally { setSaving(false); } } async function saveDraft(): Promise { if (!draft || !canWrite || !scopeReady) return false; const label = draft.label.trim(); const id = draft.id.trim(); if (!label || !editingProfileId && !id) { setError("i18n:govoplan-files.profile_id_and_label_are_required.6ad85df1"); return false; } let metadata: Record; try { metadata = metadataFromDraft(draft); } catch (err) { setError(errorMessage(err)); return false; } const capabilities = [ draft.canBrowse ? "browse" : "", draft.canImport ? "import" : "", draft.canSync ? "sync" : ""]. filter(Boolean); const sharedPayload = { label, provider: draft.provider, endpoint_url: cleanOrNull(draft.endpointUrl), base_path: cleanOrNull(draft.basePath), enabled: draft.enabled, credential_profile_id: cleanOrNull(credentialProfileIdForDraft(draft, scopedCredentials)), credential_mode: draft.credentialMode, capabilities, policy: policyFromDraft(draft), metadata }; setSaving(true); setError(""); try { if (editingProfileId) { const payload: FileConnectorProfileUpdatePayload = { ...sharedPayload, clear_password: draft.clearPassword, clear_token: draft.clearToken }; await updateFileConnectorProfile(settings, editingProfileId, payload); } else { const payload: FileConnectorProfilePayload = { id, scope_type: scopeType, scope_id: activeScopeId, ...sharedPayload }; await createFileConnectorProfile(settings, payload); } setMessage(editingProfileId ? "i18n:govoplan-files.file_connection_saved.68c16ee9" : "i18n:govoplan-files.file_connection_created.bdf6e1f6"); setDraft(null); setSavedDraftKey(""); setEditingProfileId(null); await loadProfiles(); return true; } catch (err) { setError(errorMessage(err)); return false; } finally { setSaving(false); } } async function saveCredentialDraft(): Promise { if (!credentialDraft || !canWrite || !scopeReady) return false; if (!editingCredentialId && !credentialAttachProfileId) { setError("Create credentials from a connection row so the credential is attached to a file connection."); return false; } const label = credentialDraft.label.trim(); const id = credentialDraft.id.trim(); if (!label || !editingCredentialId && !id) { setError("i18n:govoplan-files.credential_id_and_label_are_required.4cfd1ffd"); return false; } let metadata: Record; try { metadata = metadataFromJson(credentialDraft.metadataJson); } catch (err) { setError(errorMessage(err)); return false; } const credentials = { 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 = { label, provider: credentialDraft.provider || null, enabled: credentialDraft.enabled, credential_mode: credentialDraft.credentialMode, credentials, policy: credentialPolicyFromDraft(credentialDraft), metadata }; setSaving(true); setError(""); try { if (editingCredentialId) { const payload: FileConnectorCredentialUpdatePayload = { ...sharedPayload, clear_password: credentialDraft.clearPassword, clear_token: credentialDraft.clearToken }; await updateFileConnectorCredential(settings, editingCredentialId, payload); } else { const payload: FileConnectorCredentialPayload = { id, scope_type: scopeType, scope_id: activeScopeId, ...sharedPayload }; await createFileConnectorCredential(settings, payload); if (credentialAttachProfileId) { await updateFileConnectorProfile(settings, credentialAttachProfileId, { credential_profile_id: id, credential_mode: credentialDraft.credentialMode }); } } setMessage(editingCredentialId ? "i18n:govoplan-files.file_credential_saved.d6a1eef8" : "i18n:govoplan-files.file_credential_created.87c31882"); setCredentialDraft(null); setSavedCredentialDraftKey(""); setEditingCredentialId(null); setCredentialAttachProfileId(null); await loadProfiles(); return true; } catch (err) { setError(errorMessage(err)); return false; } finally { setSaving(false); } } async function saveDirtyDrafts(): Promise { let ok = true; if (policyDirty) ok = await savePolicyDraft() && ok; if (credentialDirty) ok = await saveCredentialDraft() && ok; if (profileDirty) ok = await saveDraft() && ok; return ok; } function discardDirtyDrafts() { setDraft(null); setSavedDraftKey(""); setEditingProfileId(null); setCredentialDraft(null); setSavedCredentialDraftKey(""); setEditingCredentialId(null); setCredentialAttachProfileId(null); setPolicyDraft(JSON.parse(savedPolicyDraftKey) as CentralConnectorPolicyDraft); } async function confirmDeactivate() { if (!pendingDeactivate) return; const profile = pendingDeactivate; setSaving(true); setError(""); try { await deactivateFileConnectorProfile(settings, profile.id); setMessage(i18nMessage("i18n:govoplan-files.file_connection_disabled_value.059a7de9", { value0: profile.label })); setPendingDeactivate(null); await loadProfiles(); } catch (err) { setError(errorMessage(err)); } finally { setSaving(false); } } async function confirmCredentialDeactivate() { if (!pendingCredentialDeactivate) return; const credential = pendingCredentialDeactivate; setSaving(true); setError(""); try { await deactivateFileConnectorCredential(settings, credential.id); setMessage(i18nMessage("i18n:govoplan-files.file_credential_disabled_value.947538fd", { value0: credential.label })); setPendingCredentialDeactivate(null); await loadProfiles(); } catch (err) { setError(errorMessage(err)); } finally { setSaving(false); } } async function testBrowse(profile: FileConnectorProfile) { setTestingProfileId(profile.id); setTestResults((current) => ({ ...current, [profile.id]: { ok: true, message: "i18n:govoplan-files.testing.15ccc832" } })); try { const response = await browseFileConnectorProfile(settings, profile.id, { path: "" }); setTestResults((current) => ({ ...current, [profile.id]: { ok: true, message: i18nMessage("i18n:govoplan-files.browse_ok_value_item_value.921906fd", { value0: response.items.length, value1: response.items.length === 1 ? "" : "s" }) } })); } catch (err) { setTestResults((current) => ({ ...current, [profile.id]: { ok: false, message: errorMessage(err) } })); } finally { setTestingProfileId(null); } } async function discoverProfileEndpoint() { if (!draft) return; const endpointUrl = draft.endpointUrl.trim(); if (!endpointUrl) { setProfileDiscoveryResult({ ok: false, message: "Enter a server URL before running discovery." }); return; } setDiscoveringProfile(true); setProfileDiscoveryResult(null); try { const response = await discoverFileConnectorEndpoint(settings, { provider: draft.provider, endpoint_url: endpointUrl, base_path: cleanOrNull(draft.basePath), credential_mode: draft.credentialMode, credentials: { 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) }); if (response.endpoint_url) { patchDraft({ endpointUrl: response.endpoint_url, basePath: response.base_path ?? draft.basePath, metadataJson: JSON.stringify(response.metadata ?? metadataFromDraft(draft), null, 2) }); } setProfileDiscoveryResult({ ok: response.status === "usable" || response.status === "found", message: response.message }); } catch (err) { setProfileDiscoveryResult({ ok: false, message: errorMessage(err) }); } finally { setDiscoveringProfile(false); } } async function testCredentialLogin() { if (!credentialDraft) return; const profile = credentialTestProfile; if (!profile) { setCredentialLoginResult({ ok: false, message: "Create or select a file connection before testing this credential." }); return; } if (profile.provider !== "webdav" && profile.provider !== "nextcloud") { setCredentialLoginResult({ ok: false, message: "Credential login testing is currently available for WebDAV and Nextcloud connections." }); return; } setTestingCredentialLogin(true); setCredentialLoginResult(null); try { const response = await discoverFileConnectorEndpoint(settings, { provider: knownProvider(profile.provider), endpoint_url: profile.endpoint_url || "", base_path: profile.base_path || "", credential_mode: credentialDraft.credentialMode, credentials: { 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 ?? {}, require_valid_credentials: true }); const ok = response.status === "usable" || response.status === "found"; setCredentialLoginResult({ ok, message: ok ? "Login successful. The credentials can browse the selected connection." : response.message }); } catch (err) { setCredentialLoginResult({ ok: false, message: errorMessage(err) }); } finally { setTestingCredentialLogin(false); } } const panelTitle = title ?? i18nMessage("i18n:govoplan-files.value_file_connections", { value0: scopeLabel(scopeType) }); const selectedCredentialIds = new Set(scopedProfiles.map((profile) => profile.credential_profile_id).filter((value): value is string => Boolean(value))); const firstCompatibleProfileByCredential = new Map(); for (const credential of scopedCredentials) { const selectedProfile = scopedProfiles.find((profile) => profile.credential_profile_id === credential.id); const fallbackProfile = selectedProfile ?? scopedProfiles.find((profile) => !selectedCredentialIds.has(credential.id) && credentialMatchesProfile(credential, profile)); if (fallbackProfile) firstCompatibleProfileByCredential.set(credential.id, fallbackProfile.id); } const connectorRows: ConnectorTreeRow[] = scopedProfiles.map((profile) => ({ kind: "profile", id: `profile:${profile.id}`, profile })); const credentialAttachedProfile = credentialAttachProfileId ? scopedProfiles.find((profile) => profile.id === credentialAttachProfileId) ?? null : null; const credentialTestProfile = credentialDraft ? credentialAttachedProfile ?? (editingCredentialId ? scopedProfiles.find((profile) => profile.credential_profile_id === editingCredentialId) ?? scopedProfiles.find((profile) => credentialDraft.provider ? profile.provider === credentialDraft.provider : true) ?? null : null) : null; const profileMissingFields = profileDraftMissingFields(draft, editingProfileId); const credentialMissingFields = credentialDraftMissingFields(credentialDraft, editingCredentialId, credentialAttachProfileId); const profileMissingRequired = profileMissingFields.length > 0; const credentialMissingRequired = credentialMissingFields.length > 0; const profileSaveDisabled = !draft || saving || profileMissingRequired; const credentialSaveDisabled = !credentialDraft || saving || credentialMissingRequired; const profileSaveTooltip = requiredFieldsTooltip(profileMissingFields); const credentialSaveTooltip = requiredFieldsTooltip(credentialMissingFields); const canDiscoverProfileEndpoint = Boolean(draft && (draft.provider === "webdav" || draft.provider === "nextcloud")); const credentialTestUnsupported = Boolean(credentialTestProfile && credentialTestProfile.provider !== "webdav" && credentialTestProfile.provider !== "nextcloud"); const credentialTestDisabled = !credentialDraft || saving || testingCredentialLogin || !credentialTestProfile || credentialTestUnsupported; const credentialTestTooltip = credentialTestHelpText(credentialTestProfile, credentialTestUnsupported); const profileCredentialOptions = draft ? credentialOptionsForProfileDraft(scopedCredentials, draft) : []; const profileCredentialSelectValue = draft ? credentialProfileIdForDraft(draft, scopedCredentials) : ""; const connectorColumns: ConnectionTreeColumn[] = [ { id: "connection", header: "i18n:govoplan-files.connection_credential.178babe0", width: "minmax(260px, 1.5fr)", render: (row) => row.kind === "profile" ? : }, { id: "endpoint", header: "i18n:govoplan-files.endpoint.92ec6350", width: "minmax(190px, 1fr)", render: (row) => row.kind === "profile" ? {row.profile.endpoint_url || "i18n:govoplan-files.no_endpoint.d0ea68c4"} : {row.credential.provider ? providerLabel(row.credential.provider) : "i18n:govoplan-files.any_provider.b3fc542f"} }, { id: "policy", header: "i18n:govoplan-files.policy.bb9cf141", width: "minmax(160px, 0.8fr)", render: (row) => {connectorPolicySummary(row)} }, { id: "status", header: "i18n:govoplan-files.status.bae7d5be", width: "120px", render: (row) => { const enabled = row.kind === "profile" ? row.profile.enabled : row.credential.enabled; return {enabled ? "i18n:govoplan-files.enabled.df174a3f" : "i18n:govoplan-files.disabled.f4f4473d"}; } }]; function connectorChildren(row: ConnectorTreeRow): ConnectorTreeRow[] { if (row.kind !== "profile") return []; return scopedCredentials. filter((credential) => firstCompatibleProfileByCredential.get(credential.id) === row.profile.id). map((credential) => ({ kind: "credential" as const, id: `credential:${row.profile.id}:${credential.id}`, credential, profile: row.profile })); } function renderConnectorActions(row: ConnectorTreeRow) { if (row.kind === "credential") { const readOnly = row.credential.source_kind !== "database"; return (
); } const readOnly = row.profile.source_kind !== "database"; return (
); } return (
{error && {error}} {message && !error && {message}} {!canWrite && GLOBAL or TENANT > File Connectors" }} /> } {targetSelectionRequired && !hasSelectableTarget && } {targetSelectionRequired &&
} {scopeReady && <>
: null }> row.id} getChildren={connectorChildren} renderActions={renderConnectorActions} emptyText="i18n:govoplan-files.no_file_connections_configured.3ef02049" /> void savePolicyDraft()} disabled={saving || loading}>