Release / publish (, ubuntu-22.04) (push) Failing after 14m42s
Release / publish (, windows-latest) (push) Canceled after 0s
Release / publish (--target aarch64-apple-darwin, macos-latest) (push) Canceled after 0s
Release / publish (--target x86_64-apple-darwin, macos-15-intel) (push) Canceled after 0s
* [P1-T1] Change::Ddl Rust variant + execute_change arm * [P1-T2] Frontend ddl change type + objectCrud capability * [P1-T3] use_keychain data model + migration v8 * [P2-T1] object_crud skeleton + validators + build_ddl dispatch * [P2-T2] Sequence builders * [P2-T3] Enum builders (no value removal) * [P2-T4] View / matview / extension builders * [P2-T5] Index + constraint builders * [P2-T6] Function / procedure + trigger builders * [P2-T7] build_object_ddl + get_available_extensions commands + wrappers * [P3-T1] conditional keychain + session passwords * [P3-T2] keychain-off password prompt on connect * [P3-T3] default-ON keychain opt-out + tooltip + modal conditional * [P3-T4] ddl queue card + after-commit refetch * [P4-T1] ObjectCrudDialog shell * [P4-T2] SequenceForm * [P4-T3] EnumForm with no-removal note * [P4-T4] ExtensionForm with available-extensions picker * [P4-T5] ViewForm (view + materialized view) * [P5-T1] IndexForm with column picker * [P5-T2] ConstraintForm (check/unique/pk/fk) + ColumnPicker * [P5-T3] FunctionForm (function + procedure) * [P5-T4] TriggerForm with trigger-function picker * [P5-T5] ObjectContextMenu + Explorer/TableOverflowMenu CRUD wiring * [P6-T1] object tab type + openObjectTab dedup * [P6-T2] extract ObjectDetail for object tabs * [P6-T3] Objects view two-pane sidebar + workspace * [P6-T4] object-tab content + per-type tab icons * [P7-T1] Version bump 0.7.5 -> 0.7.6 * [P7-T2] docs sync README/ROADMAP/AGENTS for v0.7.6 * [P7-T3] chore: Cargo.lock version sync 0.7.5 -> 0.7.6 * fix(ui): object tab icon stacks above name (preflight svg block) * fix(ui): optically center object tab icon with name * fix(ui): object tab icon matches query/table icon handling * [UI-POLISH-1] objectForm tab type + openFormTab store action * [UI-POLISH-2] ObjectFormTab + KindForm with Visual/SQL toggle * [UI-POLISH-3] route create/edit through form tabs; remove modal * docs: create/edit now open as form tabs (AGENTS sync) * [FB-1] follow app styling patterns + schema dropdown in forms * [FB-2] Monaco editor for function body + view definition * [FB-3] form tabs styled like viewers + in-cell editing * [FB-5] focus outline scoped to input area (label excluded) * [FB-6] no amber focus outline on Monaco body/definition rows * [FB-7] header dedupe + schema default + full edit prefill * [FB-8] SQL view in read-only Monaco editor * docs: roadmap — table create/edit + relationships (next) * docs: roadmap — Admin follow-up is 0.7.7 (next after 0.7.6)
149 lines
5.5 KiB
TypeScript
149 lines
5.5 KiB
TypeScript
import { useState, useCallback } from "react";
|
|
import { AnimatedModal } from "../ui/AnimatedModal";
|
|
import { Button } from "../ui/Button";
|
|
import { DetailedConnectionForm } from "../connections/DetailedConnectionForm";
|
|
import { useConnectionStore } from "../../stores/connectionStore";
|
|
import { useNotificationStore } from "../../stores/notificationStore";
|
|
import { updateConnection, testConnection, saveConnectionSshPassword, saveConnectionSshPassphrase } from "../../lib/commands";
|
|
import { persistDbPassword } from "../../lib/keychain";
|
|
import { detectProviderFromHost } from "../../lib/connectionString";
|
|
import type { Connection, ConnectionInput } from "../../lib/types";
|
|
import type { ConnectionFormData } from "../connections/connectionFormData";
|
|
|
|
interface EditConnectionModalProps {
|
|
connection: Connection;
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onSaved: (updated: Connection) => void;
|
|
}
|
|
|
|
export function EditConnectionModal({
|
|
connection,
|
|
open,
|
|
onClose,
|
|
onSaved,
|
|
}: EditConnectionModalProps) {
|
|
const managedPreset = detectProviderFromHost(connection.host);
|
|
|
|
const [form, setForm] = useState<ConnectionFormData>(() => ({
|
|
name: connection.name,
|
|
environment: (connection.environment as ConnectionFormData["environment"]) ?? null,
|
|
folder_id: connection.folder_id,
|
|
tag_ids: [...connection.tag_ids],
|
|
connection_string: "",
|
|
db_type: connection.db_type,
|
|
host: connection.host,
|
|
port: connection.port,
|
|
username: connection.username,
|
|
password: null,
|
|
database: connection.database ?? null,
|
|
use_keychain: connection.use_keychain ?? true,
|
|
ssh_host: connection.ssh_host ?? null,
|
|
ssh_port: connection.ssh_port ?? null,
|
|
ssh_user: connection.ssh_user ?? null,
|
|
ssh_auth_method:
|
|
(connection.ssh_auth_method as "password" | "key" | null | undefined) ??
|
|
null,
|
|
ssh_private_key: connection.ssh_private_key_path ?? null,
|
|
ssh_password: null,
|
|
ssh_passphrase: null,
|
|
}));
|
|
const [saving, setSaving] = useState(false);
|
|
const [testing, setTesting] = useState(false);
|
|
const loadAll = useConnectionStore((s) => s.loadAll);
|
|
const notify = useNotificationStore((s) => s.notify);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
if (!form.name.trim()) return;
|
|
setSaving(true);
|
|
try {
|
|
const input: ConnectionInput = {
|
|
name: form.name,
|
|
db_type: form.db_type,
|
|
host: form.host,
|
|
port: form.port,
|
|
username: form.username,
|
|
password: form.password,
|
|
database: form.database,
|
|
folder_id: form.folder_id,
|
|
environment: form.environment,
|
|
tag_ids: form.tag_ids,
|
|
ssh_host: form.ssh_host ?? null,
|
|
ssh_port: form.ssh_port ?? null,
|
|
ssh_user: form.ssh_user ?? null,
|
|
ssh_auth_method: form.ssh_auth_method ?? null,
|
|
ssh_private_key_path: form.ssh_private_key ?? null,
|
|
ssh_password: form.ssh_password ?? null,
|
|
ssh_passphrase: form.ssh_passphrase ?? null,
|
|
};
|
|
const updated = await updateConnection(connection.id, input);
|
|
await persistDbPassword(connection.id, form.use_keychain, form.password).catch(() => {});
|
|
// Persist SSH secrets to the OS keychain (not SQLite)
|
|
if (form.ssh_host && (form.ssh_auth_method ?? "password") === "password" && form.ssh_password) {
|
|
await saveConnectionSshPassword(connection.id, form.ssh_password).catch(() => {});
|
|
}
|
|
if (form.ssh_host && form.ssh_passphrase) {
|
|
await saveConnectionSshPassphrase(connection.id, form.ssh_passphrase).catch(() => {});
|
|
}
|
|
notify("Connection updated", "success");
|
|
onSaved(updated);
|
|
onClose();
|
|
loadAll();
|
|
} catch (e) {
|
|
notify(`Failed to update: ${e instanceof Error ? e.message : e}`, "error");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [form, connection.id, notify, onSaved, onClose, loadAll]);
|
|
|
|
const handleTest = useCallback(async () => {
|
|
setTesting(true);
|
|
try {
|
|
// Fetch password from keychain if not provided in form
|
|
let password = form.password;
|
|
if (!password) {
|
|
password = await useConnectionStore.getState().getConnectionPassword(connection.id).catch(() => null);
|
|
}
|
|
|
|
const result = await testConnection({
|
|
name: form.name,
|
|
db_type: form.db_type,
|
|
host: form.host,
|
|
port: form.port,
|
|
username: form.username,
|
|
password,
|
|
database: form.database,
|
|
folder_id: form.folder_id,
|
|
environment: form.environment,
|
|
tag_ids: form.tag_ids,
|
|
ssh_password: form.ssh_password ?? null,
|
|
});
|
|
if (result.ok) {
|
|
notify("Connection successful", "success");
|
|
} else {
|
|
notify(result.error ?? "Connection failed", "error");
|
|
}
|
|
} catch (e) {
|
|
notify(`Test failed: ${e instanceof Error ? e.message : e}`, "error");
|
|
} finally {
|
|
setTesting(false);
|
|
}
|
|
}, [form, notify, connection.id]);
|
|
|
|
return (
|
|
<AnimatedModal open={open} onClose={onClose}>
|
|
<div className="w-full min-w-md max-w-lg max-h-[80vh] overflow-y-auto">
|
|
<h3 className="font-heading text-text text-lg mb-4">Edit Connection</h3>
|
|
<DetailedConnectionForm form={form} onChange={(updates) => setForm((prev) => ({ ...prev, ...updates }))} managedPreset={managedPreset} />
|
|
<div className="flex justify-end gap-2 mt-4">
|
|
<Button variant="ghost" onClick={handleTest} disabled={testing}>
|
|
{testing ? "Testing..." : "Test"}
|
|
</Button>
|
|
<Button onClick={handleSave} disabled={saving}>
|
|
{saving ? "Saving..." : "Save"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</AnimatedModal>
|
|
);
|
|
} |