From 84051c6cb0da06550ec25ecd0d61035e82c434c0 Mon Sep 17 00:00:00 2001 From: Yann Amsellem Date: Tue, 7 Jan 2025 19:38:29 +0100 Subject: [PATCH 1/5] feat: create repository for query+ migration and state --- src/lib/components/SideBar.svelte | 4 +- src/lib/database/index.ts | 6 ++ .../migrations/002_create_queries_table.sql | 16 +++++ src/lib/migrations/index.ts | 4 +- src/lib/repositories/history.ts | 4 +- src/lib/repositories/queries.ts | 71 +++++++++++++++++++ src/routes/+page.svelte | 14 ++-- 7 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 src/lib/migrations/002_create_queries_table.sql create mode 100644 src/lib/repositories/queries.ts diff --git a/src/lib/components/SideBar.svelte b/src/lib/components/SideBar.svelte index 2f095dd..16a593e 100644 --- a/src/lib/components/SideBar.svelte +++ b/src/lib/components/SideBar.svelte @@ -1,6 +1,7 @@
diff --git a/src/lib/database/index.ts b/src/lib/database/index.ts index 543c01a..14c04d0 100644 --- a/src/lib/database/index.ts +++ b/src/lib/database/index.ts @@ -1,3 +1,4 @@ +import { dev } from '$app/environment'; import { MIGRATIONS } from '$lib/migrations'; import { IndexedDBCache } from '@agnosticeng/cache'; import { MigrationManager } from '@agnosticeng/migrate'; @@ -41,3 +42,8 @@ class Database { export type { Database }; export const db = new Database(); + +if (dev) { + // @ts-ignore + window.db = db; +} diff --git a/src/lib/migrations/002_create_queries_table.sql b/src/lib/migrations/002_create_queries_table.sql new file mode 100644 index 0000000..47b0147 --- /dev/null +++ b/src/lib/migrations/002_create_queries_table.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS queries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + sql TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TRIGGER IF NOT EXISTS update_queries_updated_at +AFTER UPDATE ON queries +FOR EACH ROW +BEGIN + UPDATE queries + SET updated_at = datetime('now') + WHERE id = OLD.id; +END; diff --git a/src/lib/migrations/index.ts b/src/lib/migrations/index.ts index 555450d..cdbdb19 100644 --- a/src/lib/migrations/index.ts +++ b/src/lib/migrations/index.ts @@ -1,7 +1,9 @@ import type { Migration } from '@agnosticeng/migrate'; import CREATE_HISTORY_TABLE_SCRIPT from './001_create_history_table.sql?raw'; +import CREATE_QUERIES_TABLE_SCRIPT from './002_create_queries_table.sql?raw'; export const MIGRATIONS: Migration[] = [ - { name: 'create_history_table', script: CREATE_HISTORY_TABLE_SCRIPT } + { name: 'create_history_table', script: CREATE_HISTORY_TABLE_SCRIPT }, + { name: 'create_queries_table', script: CREATE_QUERIES_TABLE_SCRIPT } ]; diff --git a/src/lib/repositories/history.ts b/src/lib/repositories/history.ts index 6f88cca..6a217d0 100644 --- a/src/lib/repositories/history.ts +++ b/src/lib/repositories/history.ts @@ -7,14 +7,14 @@ export interface HistoryEntry { } export interface HistoryRepository { - get_all(): Promise; + getAll(): Promise; add(content: string): Promise; } class SQLiteHistoryRepository implements HistoryRepository { constructor(private db: Database) {} - async get_all(): Promise { + async getAll(): Promise { const rows = await this.db.exec('SELECT * FROM history ORDER BY timestamp DESC'); return rows.map((row) => ({ id: row.id as number, diff --git a/src/lib/repositories/queries.ts b/src/lib/repositories/queries.ts new file mode 100644 index 0000000..c91a274 --- /dev/null +++ b/src/lib/repositories/queries.ts @@ -0,0 +1,71 @@ +import { db, type Database } from '$lib/database'; + +export interface Query { + id: number; + name: string; + sql: string; + + createdAt: Date; + updatedAt: Date; +} + +export interface QueryRepository { + getAll(): Promise; + getById(id: number): Promise; + create(name: string, sql: string): Promise; + update(query: Query): Promise; + delete(id: number): Promise; +} + +class SQLiteQueryRepository implements QueryRepository { + constructor(private db: Database) {} + + async getAll(): Promise { + const rows = await this.db.exec('SELECT * FROM queries'); + return rows.map((row) => row_to_query(row)); + } + + async getById(id: number): Promise { + const [row] = await this.db.exec('SELECT * FROM queries WHERE id = ?', [id]); + if (!row) throw Error('Query not found'); + return row_to_query(row); + } + + async create(name: string, sql: string): Promise { + const [row] = await this.db.exec('INSERT INTO queries (name, sql) VALUES (?, ?) RETURNING *', [ + name, + sql + ]); + + if (!row) throw Error('Failed to insert query'); + + return row_to_query(row); + } + + async update(query: Query): Promise { + const [row] = await this.db.exec( + 'UPDATE queries SET name = ?, sql = ? WHERE id = ? RETURNING *', + [query.name, query.sql, query.id] + ); + + if (!row) throw Error('Failed to update query'); + + return row_to_query(row); + } + + async delete(id: number): Promise { + await this.db.exec('DELETE FROM queries WHERE id = ?', [id]); + } +} + +function row_to_query(row: Awaited>[number]): Query { + return { + id: row.id as number, + name: row.name as string, + sql: row.sql as string, + createdAt: new Date(row.created_at as string), + updatedAt: new Date(row.updated_at as string) + }; +} + +export const query_repository: QueryRepository = new SQLiteQueryRepository(db); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ffa3006..4016874 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,6 +7,7 @@ import type { Table } from '$lib/olap-engine'; import { engine, type OLAPResponse } from '$lib/olap-engine'; import { history_repository, type HistoryEntry } from '$lib/repositories/history'; + import { query_repository, type Query } from '$lib/repositories/queries'; import type { PageData } from './$types'; let response = $state.raw(); @@ -23,8 +24,9 @@ if (response) await addHistoryEntry(); } - let tables = $state([]); - let history = $state([]); + let tables = $state.raw([]); + let history = $state.raw([]); + let queries = $state.raw([]); $effect(() => { engine.getSchema().then((t) => { @@ -33,7 +35,7 @@ }); $effect(() => { - history_repository.get_all().then((entries) => { + history_repository.getAll().then((entries) => { history = entries; }); }); @@ -50,6 +52,10 @@ function handleHistoryClick(entry: HistoryEntry) { query = entry.content; } + + $effect(() => { + query_repository.getAll().then((q) => (queries = q)); + }); @@ -61,7 +67,7 @@
{#snippet a()} - + {/snippet} {#snippet b()} From d39cfe03e02e8a343d22e8c8dac64d0cb24fdc06 Mon Sep 17 00:00:00 2001 From: Yann Amsellem Date: Tue, 7 Jan 2025 21:54:04 +0100 Subject: [PATCH 2/5] feat: list saved queries --- src/lib/components/Queries/Queries.svelte | 95 +++++++++++++++++++++++ src/lib/components/Queries/index.ts | 1 + src/lib/components/SideBar.svelte | 4 + src/lib/icons/SQLFile.svelte | 17 ++++ 4 files changed, 117 insertions(+) create mode 100644 src/lib/components/Queries/Queries.svelte create mode 100644 src/lib/components/Queries/index.ts create mode 100644 src/lib/icons/SQLFile.svelte diff --git a/src/lib/components/Queries/Queries.svelte b/src/lib/components/Queries/Queries.svelte new file mode 100644 index 0000000..69cb746 --- /dev/null +++ b/src/lib/components/Queries/Queries.svelte @@ -0,0 +1,95 @@ + + +
    + {#each queries as query (query.id)} +
  1. { + e.preventDefault(); + }} + role="menuitem" + onkeydown={(e) => { + if (e.key === 'Enter') { + e.currentTarget.blur(); + } + }} + onclick={(e) => { + if (e.detail >= 2) { + e.currentTarget.blur(); + } + }} + > + +
    + {query.name} + {dayjs(query.createdAt).fromNow()} +
    +
  2. + {/each} +
+ + diff --git a/src/lib/components/Queries/index.ts b/src/lib/components/Queries/index.ts new file mode 100644 index 0000000..04ab481 --- /dev/null +++ b/src/lib/components/Queries/index.ts @@ -0,0 +1 @@ +export { default as Queries } from './Queries.svelte'; diff --git a/src/lib/components/SideBar.svelte b/src/lib/components/SideBar.svelte index 16a593e..5e88c6b 100644 --- a/src/lib/components/SideBar.svelte +++ b/src/lib/components/SideBar.svelte @@ -4,6 +4,7 @@ import type { Query } from '$lib/repositories/queries'; import Datasets from './Datasets/Datasets.svelte'; import History from './History.svelte'; + import Queries from './Queries/Queries.svelte'; type Tab = 'sources' | 'queries' | 'history'; @@ -31,6 +32,9 @@ {#if tab === 'sources'} {/if} + {#if tab === 'queries'} + + {/if} {#if tab === 'history'} {/if} diff --git a/src/lib/icons/SQLFile.svelte b/src/lib/icons/SQLFile.svelte new file mode 100644 index 0000000..b21e220 --- /dev/null +++ b/src/lib/icons/SQLFile.svelte @@ -0,0 +1,17 @@ + + + + + From af1f20d6986191e9bc0b9bc73537b9a65dc00335 Mon Sep 17 00:00:00 2001 From: Yann Amsellem Date: Tue, 7 Jan 2025 22:59:43 +0100 Subject: [PATCH 3/5] feat: save query --- src/lib/components/Modal.svelte | 6 +- src/lib/components/Queries/Queries.svelte | 6 +- .../components/Queries/SaveQueryModal.svelte | 121 ++++++++++++++++++ src/lib/components/Queries/index.ts | 1 + src/routes/+page.svelte | 28 +++- src/vite-env.d.ts | 2 + 6 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 src/lib/components/Queries/SaveQueryModal.svelte diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 8985065..89b0cad 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -57,8 +57,8 @@ max-block-size: min(80vh, 100%); margin-top: 0; overflow: hidden; - transition: opacity 0.5s; - animation: slide-out-up 0.5s cubic-bezier(0.25, 0, 0.3, 1) forwards; + transition: opacity 0.3s; + animation: slide-out-up 0.3s cubic-bezier(0.25, 0, 0.3, 1) forwards; filter: drop-shadow(3px 5px 10px hsla(0deg 0% 0% / 10%)); z-index: 2147483647; padding: 0; @@ -74,7 +74,7 @@ } dialog[open] { - animation: slide-in-down 0.5s cubic-bezier(0.25, 0, 0.3, 1) forwards; + animation: slide-in-down 0.3s cubic-bezier(0.25, 0, 0.3, 1) forwards; } dialog:not([open]) { diff --git a/src/lib/components/Queries/Queries.svelte b/src/lib/components/Queries/Queries.svelte index 69cb746..c377ac1 100644 --- a/src/lib/components/Queries/Queries.svelte +++ b/src/lib/components/Queries/Queries.svelte @@ -21,11 +21,7 @@ e.preventDefault(); }} role="menuitem" - onkeydown={(e) => { - if (e.key === 'Enter') { - e.currentTarget.blur(); - } - }} + onkeydown={(e) => {}} onclick={(e) => { if (e.detail >= 2) { e.currentTarget.blur(); diff --git a/src/lib/components/Queries/SaveQueryModal.svelte b/src/lib/components/Queries/SaveQueryModal.svelte new file mode 100644 index 0000000..e0ee006 --- /dev/null +++ b/src/lib/components/Queries/SaveQueryModal.svelte @@ -0,0 +1,121 @@ + + +{#if open} + (open = false)} bind:this={modal}> +
+

Save Query

+ + +
+ + +
+
+
+{/if} + + diff --git a/src/lib/components/Queries/index.ts b/src/lib/components/Queries/index.ts index 04ab481..b48fa77 100644 --- a/src/lib/components/Queries/index.ts +++ b/src/lib/components/Queries/index.ts @@ -1 +1,2 @@ export { default as Queries } from './Queries.svelte'; +export { default as SaveQueryModal } from './SaveQueryModal.svelte'; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4016874..fb49dce 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,5 +1,6 @@ + + {#snippet actions()} + {/snippet} @@ -82,6 +97,14 @@
+ { + const q = await query_repository.create(name, query); + queries = queries.concat(q); + }} +/> + diff --git a/src/lib/components/ContextMenu/ContextMenuState.svelte.ts b/src/lib/components/ContextMenu/ContextMenuState.svelte.ts new file mode 100644 index 0000000..785bce3 --- /dev/null +++ b/src/lib/components/ContextMenu/ContextMenuState.svelte.ts @@ -0,0 +1,32 @@ +import type { ContextMenuItem, ContextMenuOptions } from './types'; + +export class ContextMenuState { + #items = $state.raw([]); + #is_open = $derived(!!this.#items.length); + #position = $state.raw<{ x: number; y: number }>({ x: 0, y: 0 }); + + get items() { + return this.#items; + } + + get open() { + return this.#is_open; + } + + get position() { + return this.#position; + } + + constructor() { + this.close = this.close.bind(this); + } + + show(options: ContextMenuOptions) { + this.#items = options.items; + this.#position = options.position; + } + + close() { + this.#items = []; + } +} diff --git a/src/lib/components/ContextMenu/index.ts b/src/lib/components/ContextMenu/index.ts new file mode 100644 index 0000000..85681be --- /dev/null +++ b/src/lib/components/ContextMenu/index.ts @@ -0,0 +1,3 @@ +export { default as ContextMenu } from './ContextMenu.svelte'; +export { ContextMenuState } from './ContextMenuState.svelte'; +export type { ContextMenuOptions } from './types'; diff --git a/src/lib/components/ContextMenu/types.d.ts b/src/lib/components/ContextMenu/types.d.ts new file mode 100644 index 0000000..8fb243a --- /dev/null +++ b/src/lib/components/ContextMenu/types.d.ts @@ -0,0 +1,16 @@ +export type ContextMenuItem = ContextMenuAction | ContextMenuSeparator; + +export interface ContextMenuAction { + label: string; + disabled?: boolean; + onClick?: (e: MouseEvent) => MaybePromise; +} + +export interface ContextMenuSeparator { + is_separator: true; +} + +export interface ContextMenuOptions { + position: { x: number; y: number }; + items: ContextMenuItem[]; +} diff --git a/src/lib/components/History.svelte b/src/lib/components/History.svelte index 6017ce6..b2740c7 100644 --- a/src/lib/components/History.svelte +++ b/src/lib/components/History.svelte @@ -34,8 +34,8 @@ } }} > - {dayjs(entry.timestamp).fromNow()}
{entry.content}
+ {dayjs(entry.timestamp).fromNow()} {/each} @@ -63,13 +63,17 @@ user-select: none; -webkit-user-select: none; - &:focus { + &:is(:focus-within) { outline: none; - background-color: hsl(210deg 100% 52%); + background-color: hsl(0deg 0% 19% / 100%); } } .content { + height: 18px; + padding: 3px 0; + line-height: 1.15; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 89b0cad..256229c 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -70,7 +70,7 @@ border-top-right-radius: 0; width: 100%; - max-width: 580px; + max-width: 420px; } dialog[open] { diff --git a/src/lib/components/Queries/Queries.svelte b/src/lib/components/Queries/Queries.svelte index c377ac1..f13c8eb 100644 --- a/src/lib/components/Queries/Queries.svelte +++ b/src/lib/components/Queries/Queries.svelte @@ -1,5 +1,5 @@
    @@ -19,19 +37,66 @@ tabindex="-1" oncontextmenu={(e) => { e.preventDefault(); + context_menu.show({ + items: [ + { + label: 'Open', + onClick: () => onopen?.(query) + }, + { + label: 'Rename', + onClick: () => (editing_id = query.id) + }, + { is_separator: true }, + { + label: 'Delete', + onClick: () => ondelete?.(query) + } + ], + position: { x: e.clientX, y: e.clientY } + }); }} role="menuitem" - onkeydown={(e) => {}} - onclick={(e) => { + onkeydown={(e) => { + if (e.key === 'Enter' && !editing_id) { + e.preventDefault(); + editing_id = query.id; + } + }} + onclick={async (e) => { if (e.detail >= 2) { + await onopen?.(query); e.currentTarget.blur(); } }} > -
    - {query.name} - {dayjs(query.createdAt).fromNow()} + {#if editing_id === query.id} +
    { + e.preventDefault(); + const form_data = new FormData(e.currentTarget); + const name = form_data.get('name') as string; + if (name && name !== query.name) { + await onrename?.({ ...query, name }); + } + editing_id = undefined; + }} + > + (editing_id = undefined)} + autocomplete="off" + /> +
    + {:else} + {query.name} + {/if} + {dayjs(query.createdAt).fromNow()}
    {/each} @@ -59,11 +124,6 @@ align-items: center; gap: 5px; - & > :global(svg) { - fill: hsl(0deg 0% 96%); - flex-shrink: 0; - } - & > div { flex: 1; display: flex; @@ -71,21 +131,40 @@ gap: 2px; overflow: hidden; - & > span:first-of-type { + & > form { + width: 100%; + + & > input { + appearance: none; + -webkit-appearance: none; + width: 100%; + height: 18px; + outline: none; + margin: 0; + padding: 0; + border: none; + } + } + + & > span.name { + display: block; + height: 18px; + padding: 3px 0; + line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - & > span:last-of-type { + & > span.time { font-size: 10px; color: hsl(0deg 0% 96%); } } - &:focus { + &:is(:focus-within) { outline: none; - background-color: hsl(210deg 100% 52%); + background-color: hsl(0deg 0% 19% / 100%); } } diff --git a/src/lib/components/Queries/SaveQueryModal.svelte b/src/lib/components/Queries/SaveQueryModal.svelte index e0ee006..d880f70 100644 --- a/src/lib/components/Queries/SaveQueryModal.svelte +++ b/src/lib/components/Queries/SaveQueryModal.svelte @@ -40,12 +40,11 @@ {#if open} (open = false)} bind:this={modal}>
    -

    Save Query