setConfirmDelete(null)}
diff --git a/frontend/src/components/DatasourceListItem.tsx b/frontend/src/components/DatasourceListItem.tsx
new file mode 100644
index 0000000..3d58abe
--- /dev/null
+++ b/frontend/src/components/DatasourceListItem.tsx
@@ -0,0 +1,67 @@
+import { ENV_COLORS, T } from '../lib/tokens';
+import type { Datasource } from '../types';
+import { ElephantIcon } from './ConnectionFormFields';
+
+interface DatasourceListItemProps {
+ ds: Datasource;
+ selected: boolean;
+ isActive: boolean;
+ onSelect: () => void;
+ onDoubleClick: () => void;
+}
+
+export function DatasourceListItem({
+ ds,
+ selected,
+ isActive,
+ onSelect,
+ onDoubleClick,
+}: DatasourceListItemProps) {
+ const envColor = ENV_COLORS[ds.env] ?? T.textSec;
+ return (
+
+
+
+
+ {ds.name}
+
+
{ds.host}
+
+
+
+ );
+}
diff --git a/frontend/src/lib/connectionForm.test.ts b/frontend/src/lib/connectionForm.test.ts
new file mode 100644
index 0000000..fad1056
--- /dev/null
+++ b/frontend/src/lib/connectionForm.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from 'vitest';
+import { getConnectionFormValidity, makeEmptyForm } from './connectionForm';
+
+describe('makeEmptyForm', () => {
+ it('returns default values', () => {
+ const f = makeEmptyForm();
+ expect(f.host).toBe('localhost');
+ expect(f.port).toBe(5432);
+ expect(f.env).toBe('local');
+ expect(f.sslMode).toBe('require');
+ expect(f.name).toBe('');
+ });
+});
+
+describe('getConnectionFormValidity', () => {
+ it('canSave is true only when name, host, and database are all present', () => {
+ expect(getConnectionFormValidity({ name: 'c', host: 'h', database: 'db' }).canSave).toBe(true);
+ });
+
+ it('canSave is false when name is empty', () => {
+ expect(getConnectionFormValidity({ name: '', host: 'h', database: 'db' }).canSave).toBe(false);
+ });
+
+ it('canSave is false when host is empty', () => {
+ expect(getConnectionFormValidity({ name: 'c', host: '', database: 'db' }).canSave).toBe(false);
+ });
+
+ it('canSave is false when database is empty', () => {
+ expect(getConnectionFormValidity({ name: 'c', host: 'h', database: '' }).canSave).toBe(false);
+ });
+
+ it('canTest is true when host and database are present, even without a name', () => {
+ expect(getConnectionFormValidity({ name: '', host: 'h', database: 'db' }).canTest).toBe(true);
+ });
+
+ it('canTest is false when host is empty', () => {
+ expect(getConnectionFormValidity({ name: 'c', host: '', database: 'db' }).canTest).toBe(false);
+ });
+
+ it('canTest is false when database is empty', () => {
+ expect(getConnectionFormValidity({ name: 'c', host: 'h', database: '' }).canTest).toBe(false);
+ });
+});
diff --git a/frontend/src/lib/connectionForm.ts b/frontend/src/lib/connectionForm.ts
new file mode 100644
index 0000000..a7c2d99
--- /dev/null
+++ b/frontend/src/lib/connectionForm.ts
@@ -0,0 +1,37 @@
+import type { Datasource } from '../types';
+
+// The editable subset of a Datasource held in the connection form's local state.
+export type ConnectionFormState = Omit
;
+
+export function makeEmptyForm(): ConnectionFormState {
+ return {
+ name: '',
+ host: 'localhost',
+ port: 5432,
+ database: '',
+ username: '',
+ password: '',
+ env: 'local',
+ sslMode: 'require',
+ };
+}
+
+export interface ConnectionFormValidity {
+ /** Required fields for saving are present (name, host, database). */
+ canSave: boolean;
+ /** Required fields for a test connection are present (host, database). */
+ canTest: boolean;
+}
+
+// Pure field-presence validation for the connection form. UI state such as
+// in-flight saving/testing is combined by the caller, keeping this testable.
+export function getConnectionFormValidity(
+ form: Pick
+): ConnectionFormValidity {
+ const hasHost = !!form.host;
+ const hasDatabase = !!form.database;
+ return {
+ canSave: !!form.name && hasHost && hasDatabase,
+ canTest: hasHost && hasDatabase,
+ };
+}