Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Component, createSignal, onMount, Show, For } from "solid-js";
import { EditorView, basicSetup } from "codemirror";
import { EditorState } from "@codemirror/state";
import { Component, Show, For, createSignal } from "solid-js";
import { sensibleqlExecute } from "../../lib/api";
import { activeDb } from "../../stores/app";
import { useCodeMirror } from "../../lib/useCodeMirror";
import type { SensibleqlResult } from "../../types";
import "./SensibleQLEditor.css";

Expand All @@ -27,40 +26,21 @@ const sampleQueries: SampleQuery[] = [
];

const SensibleQLEditor: Component = () => {
let editorRef: HTMLDivElement | undefined;
const [query, setQuery] = createSignal("");
const [result, setResult] = createSignal<SensibleqlResult | null>(null);
const [isRunning, setIsRunning] = createSignal(false);
let editor: EditorView | undefined;

onMount(() => {
if (!editorRef) return;
editor = new EditorView({
state: EditorState.create({
doc: "// Select a sample query below or write your own\n// Supported: MATCH, GET, FIND, COUNT\n",
extensions: [
basicSetup,
EditorView.theme({
"&": { fontSize: "14px" },
".cm-editor": { background: "#f8fafc" },
".cm-gutters": { background: "#f1f5f9", border: "none" },
}),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
setQuery(update.state.doc.toString());
}
}),
],
}),
parent: editorRef,
});
const { ref: editorRef, setDoc: setEditorDoc, getDoc } = useCodeMirror({
initialDoc: "// Select a sample query below or write your own\n// Supported: MATCH, GET, FIND, COUNT\n",
onChange: (doc) => setQuery(doc),
});

const handleRun = async () => {
if (!activeDb() || !query()) return;
const currentQuery = getDoc();
if (!activeDb() || !currentQuery) return;
setIsRunning(true);
try {
const res = await sensibleqlExecute(activeDb()!, query());
const res = await sensibleqlExecute(activeDb()!, currentQuery);
setResult(res);
} catch (e: any) {
setResult({ success: false, message: String(e), data: null });
Expand All @@ -70,11 +50,7 @@ const SensibleQLEditor: Component = () => {
};

const loadSample = (sample: SampleQuery) => {
if (editor) {
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: sample.query },
});
}
setEditorDoc(sample.query);
setQuery(sample.query);
setResult(null);
};
Expand Down Expand Up @@ -105,7 +81,7 @@ const SensibleQLEditor: Component = () => {
</div>
</div>

<div ref={editorRef!} class="editor-container" />
<div ref={editorRef} class="editor-container" />
<Show when={result()}>
{(r) => (
<div classList={{ "result-panel": true, error: !r().success }}>
Expand All @@ -122,4 +98,4 @@ const SensibleQLEditor: Component = () => {
);
};

export default SensibleQLEditor;
export default SensibleQLEditor;
100 changes: 100 additions & 0 deletions sensibledb-explorer/src/frontend/src/components/home/HomeView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen } from "@solidjs/testing-library";
import HomeView from "../home/HomeView";
import { setActiveView, setActiveDb, databases, setDatabases, setNodes, setEdges, setSchema } from "../../stores/app";

vi.mock("../../lib/api", () => ({
nodeList: vi.fn().mockResolvedValue([]),
edgeList: vi.fn().mockResolvedValue([]),
schemaGet: vi.fn().mockResolvedValue({ node_labels: [], edge_types: [], indexes: [], vector_indexes: [] }),
}));

vi.mock("../onboarding/GuidedTour", () => ({
showTour: vi.fn(),
}));

vi.mock("../onboarding/ConnectionWizard", () => ({
sourceOptions: [
{ value: "demo", label: "Load Demo Data", icon: "🧪" },
],
default: () => <div>ConnectionWizard</div>,
}));

describe("HomeView", () => {
beforeEach(() => {
vi.clearAllMocks();
setActiveDb(null);
setDatabases([]);
setNodes([]);
setEdges([]);
setSchema(null);
});

it("renders welcome section", () => {
const { container } = render(() => <HomeView />);
expect(container.querySelector(".welcome-section")).toBeInTheDocument();
});

it("displays welcome title", () => {
render(() => <HomeView />);
expect(screen.getByText(/welcome to sensibledb/i)).toBeInTheDocument();
});

it("shows connect your data section", () => {
render(() => <HomeView />);
expect(screen.getAllByText(/connect your data/i).length).toBeGreaterThan(0);
});

it("shows import your data card", () => {
render(() => <HomeView />);
expect(screen.getAllByText(/import your data/i).length).toBeGreaterThan(0);
});

it("has tour button", () => {
render(() => <HomeView />);
expect(screen.getByRole("button", { name: /take a tour/i })).toBeInTheDocument();
});

it("has connect data button", () => {
render(() => <HomeView />);
expect(screen.getByRole("button", { name: /connect your data/i })).toBeInTheDocument();
});

it("shows demo section", () => {
render(() => <HomeView />);
expect(screen.getByText(/try a demo database/i)).toBeInTheDocument();
});

it("displays demo card titles", () => {
render(() => <HomeView />);
expect(screen.getByText(/health patterns/i)).toBeInTheDocument();
expect(screen.getByText(/project management/i)).toBeInTheDocument();
});

it("displays demo card descriptions", () => {
render(() => <HomeView />);
expect(screen.getByText(/track symptoms, triggers/i)).toBeInTheDocument();
expect(screen.getByText(/see how team members/i)).toBeInTheDocument();
});

it("shows demo item counts", () => {
render(() => <HomeView />);
expect(screen.getAllByText(/items/i).length).toBeGreaterThan(0);
});

it("shows demo connection counts", () => {
render(() => <HomeView />);
expect(screen.getAllByText(/connections/i).length).toBeGreaterThan(0);
});

it("has explore button on demo cards", () => {
render(() => <HomeView />);
expect(screen.getAllByRole("button", { name: /explore/i }).length).toBeGreaterThan(0);
});

it("shows question suggestions on demo cards", () => {
render(() => <HomeView />);
expect(screen.getByText(/what triggers fatigue?/i)).toBeInTheDocument();
expect(screen.getByText(/show me all symptoms/i)).toBeInTheDocument();
});
});
38 changes: 38 additions & 0 deletions sensibledb-explorer/src/frontend/src/lib/useAppInit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect, vi } from "vitest";
import { useAppInitialization, useKeyboardShortcuts } from "./useAppInit";

vi.mock("../lib/api", () => ({
dbList: vi.fn().mockResolvedValue([]),
nodeList: vi.fn().mockResolvedValue([]),
edgeList: vi.fn().mockResolvedValue([]),
schemaGet: vi.fn().mockResolvedValue({ node_labels: [], edge_types: [], indexes: [], vector_indexes: [] }),
logError: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../components/onboarding/GuidedTour", () => ({
isTourCompleted: vi.fn().mockReturnValue(true),
}));

describe("useAppInitialization", () => {
it("exports useAppInitialization function", () => {
expect(typeof useAppInitialization).toBe("function");
});

it("returns loadDbData function", () => {
const setDatabases = vi.fn();
const setActiveDb = vi.fn();
const setNodes = vi.fn();
const setEdges = vi.fn();
const setSchema = vi.fn();

const result = useAppInitialization(setDatabases, setActiveDb, setNodes, setEdges, setSchema);
expect(result).toHaveProperty("loadDbData");
expect(typeof result.loadDbData).toBe("function");
});
});

describe("useKeyboardShortcuts", () => {
it("exports useKeyboardShortcuts function", () => {
expect(typeof useKeyboardShortcuts).toBe("function");
});
});
88 changes: 88 additions & 0 deletions sensibledb-explorer/src/frontend/src/lib/useAppInit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { onMount, onCleanup } from "solid-js";
import { setActiveView, setSelectedNode, activeDb } from "../stores/app";
import { logError, dbList as apiDbList, nodeList, edgeList, schemaGet } from "./api";
import { isTourCompleted } from "../components/onboarding/GuidedTour";

export function useAppInitialization(
setDatabases: (dbs: string[]) => void,
setActiveDb: (db: string | null) => void,
setNodes: (nodes: any[]) => void,
setEdges: (edges: any[]) => void,
setSchema: (schema: any) => void
) {
const loadDbData = async (dbName: string) => {
try {
const n = await nodeList(dbName);
setNodes(n);
const e = await edgeList(dbName);
setEdges(e);
const s = await schemaGet(dbName);
setSchema(s);
} catch (err) {
const errMsg = "[loadDbData] ERROR: " + String(err);
logError(errMsg).catch(() => {});
}
};

onMount(async () => {
try {
const dbs = await apiDbList();
setDatabases(dbs);
if (dbs.length > 0) {
const firstDb = dbs[0];
setActiveDb(firstDb);
await loadDbData(firstDb);
}
} catch (e) {
}

if (!isTourCompleted()) {
setTimeout(() => {
const tourEvent = new CustomEvent("show-tour");
window.dispatchEvent(tourEvent);
}, 1500);
}
});

return { loadDbData };
}

export function useKeyboardShortcuts(
activeDb: () => string | null,
loadDbData: (dbName: string) => Promise<void>
) {
onMount(() => {
const handler = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;

if (e.key === "1") setActiveView("home");
else if (e.key === "2") setActiveView("graph");
else if (e.key === "3") setActiveView("chat");
else if (e.key === "4") setActiveView("report");
else if (e.key === "5") setActiveView("nodes");
else if (e.key === "6") setActiveView("edges");
else if (e.key === "7") setActiveView("schema");
else if (e.key === "8") setActiveView("sensibleql");
else if (e.key === "9") setActiveView("models");
else if (e.key === "Escape") {
setSelectedNode(null);
setActiveView("home");
}
else if (e.key === "/" || (e.ctrlKey && e.key === "k")) {
e.preventDefault();
setActiveView("chat");
}
else if (e.ctrlKey && e.key === "g") {
e.preventDefault();
setActiveView("graph");
}
else if (e.ctrlKey && e.key === "r") {
e.preventDefault();
if (activeDb()) loadDbData(activeDb()!);
}
};

window.addEventListener("keydown", handler);
onCleanup(() => window.removeEventListener("keydown", handler));
});
}
31 changes: 31 additions & 0 deletions sensibledb-explorer/src/frontend/src/lib/useCodeMirror.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect, vi } from "vitest";
import { useCodeMirror } from "./useCodeMirror";

describe("useCodeMirror", () => {
it("creates ref function", () => {
const { ref } = useCodeMirror();
expect(typeof ref).toBe("function");
});

it("creates getDoc function", () => {
const { getDoc } = useCodeMirror();
expect(typeof getDoc).toBe("function");
});

it("creates setDoc function", () => {
const { setDoc } = useCodeMirror();
expect(typeof setDoc).toBe("function");
});

it("accepts initial doc option", () => {
const hook = useCodeMirror({ initialDoc: "initial text" });
expect(hook.getDoc).toBeDefined();
expect(hook.setDoc).toBeDefined();
});

it("accepts onChange callback", () => {
const onChange = vi.fn();
const hook = useCodeMirror({ onChange });
expect(typeof hook.ref).toBe("function");
});
});
Loading
Loading