diff --git a/web/components/Filtre.tsx b/web/components/Filtre.tsx index 85aeea8..a711593 100644 --- a/web/components/Filtre.tsx +++ b/web/components/Filtre.tsx @@ -3,7 +3,16 @@ import { navigate, url } from '../lib/router.tsx' type FilterRow = { idx: number; key: string; op: string; value: string } -const filterOperators = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'like'] as const +const filterOperators = [ + 'eq', + 'neq', + 'gt', + 'gte', + 'lt', + 'lte', + 'like', + 'ilike', +] as const export function parseFilters(prefix: string): FilterRow[] { const data = url.getAll(`f${prefix}`) @@ -28,7 +37,10 @@ function setFilters(prefix: string, rows: FilterRow[]) { const v = rows[i] next.push([v.key, v.op, v.value].join(',')) } - navigate({ params: { [`f${prefix}`]: next }, replace: true }) + navigate({ + params: { [`f${prefix}`]: next, [`${prefix}page`]: null }, + replace: true, + }) } function addFilter(prefix: string) { @@ -115,12 +127,17 @@ export const FilterMenu = ( const rows = parseFilters(prefix) return ( - + ) } @@ -196,12 +213,17 @@ export const SortMenu = ({ tag, sortKeyOptions }: { const prefix = tag === 'tables' ? 't' : 'l' const rows = parseSort(prefix) return ( - + ) } diff --git a/web/components/QueryHistory.tsx b/web/components/QueryHistory.tsx new file mode 100644 index 0000000..04e4f67 --- /dev/null +++ b/web/components/QueryHistory.tsx @@ -0,0 +1,112 @@ +import { ChevronRight, Clock, Play, Search, Trash2 } from 'lucide-preact' +import { A, navigate, url } from '../lib/router.tsx' +import { onRun, queriesHistory } from '../pages/DeploymentPage.tsx' + +export type QueryHistoryItem = { + query: string + timestamp: string + columns?: number + rows?: number +} + +const deleteQuery = (hash: string) => { + const updatedHistory = { ...queriesHistory.value } + delete updatedHistory[hash] + queriesHistory.value = updatedHistory +} + +export const QueryHistory = () => { + const filteredHistory = Object.entries(queriesHistory.value).sort((a, b) => + new Date(b[1].timestamp).getTime() - new Date(a[1].timestamp).getTime() + ).filter(([_, item]) => { + const qh = url.params.qh?.toLowerCase() || '' + return item.query.toLowerCase().includes(qh) + }) + + return ( +
+
+

Query History

+
+ + +
+
+
+ {filteredHistory.map(([hash, item]) => ( +
+
+
+
+ + {new Date(item.timestamp).toLocaleString()} +
+

+ {item.query} +

+
+ {item.columns} columns, {item.rows} rows +
+
+
+ onRun(item.query)} + > + + + + + + +
+
+
+ ))} + {filteredHistory.length === 0 && ( +
+ No queries found. +
+ )} +
+
+ ) +} diff --git a/web/components/SideBar.tsx b/web/components/SideBar.tsx index ed81c23..854949b 100644 --- a/web/components/SideBar.tsx +++ b/web/components/SideBar.tsx @@ -21,18 +21,17 @@ export function Sidebar( title?: string }, ) { - const { sb } = url.params + const sb = url.params.sb return (
-
- {!sb && ( + {sb && ( {title || 'Project Name'} @@ -42,8 +41,8 @@ export function Sidebar( class='p-2 hover:bg-base-200 rounded' > {sb - ? - : } + ? + : }
@@ -54,12 +53,12 @@ export function Sidebar(
  • - {!sb && item.label} + {sb && item.label}
  • ))} @@ -77,7 +76,7 @@ export function Sidebar( }`} > - {!sb && 'Settings'} + {sb && 'Settings'}
    diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx index f681bf9..0286921 100644 --- a/web/pages/DeploymentPage.tsx +++ b/web/pages/DeploymentPage.tsx @@ -29,6 +29,8 @@ import { } from '../components/Filtre.tsx' import { effect, Signal } from '@preact/signals' import { api } from '../lib/api.ts' +import { QueryHistory, QueryHistoryItem } from '../components/QueryHistory.tsx' +import { JSX } from 'preact' type AnyRecord = Record @@ -48,13 +50,13 @@ const comparators = { const tableData = api['POST/api/deployment/table/data'].signal() const querier = api['GET/api/deployment/query'].signal() -const queriesHistory = new Signal( - JSON.parse(localStorage.getItem('savedQueries') || '{}'), +export const queriesHistory = new Signal>( + JSON.parse(localStorage.getItem('saved_queries') || '{}'), ) type Order = 'ASC' | 'DESC' queriesHistory.subscribe((val) => { - localStorage.setItem('savedQueries', JSON.stringify(val)) + localStorage.setItem('saved_queries', JSON.stringify(val)) }) // Effect to fetch schema when deployment URL changes @@ -85,6 +87,7 @@ effect(() => { key: r.key, order: r.dir === 'asc' ? 'ASC' : 'DESC' as Order, })) + tableData.fetch({ deployment: dep, table: tableName, @@ -110,11 +113,11 @@ effect(() => { } }) -const onRun = () => { +export const onRun = (query: string) => { if (querier.pending) return - const { dep, tab, q } = url.params - if (dep && tab === 'queries' && q) { - querier.fetch({ deployment: dep, sql: q }) + const { dep, tab } = url.params + if (dep && tab === 'queries' && query) { + querier.fetch({ deployment: dep, sql: query }) } } @@ -122,6 +125,7 @@ function sha(message: string) { const data = new TextEncoder().encode(message) return crypto.subtle.digest('SHA-1', data) } + function hashQuery(query: string) { const hash = sha(query).then((hashBuffer) => { const hashArray = Array.from(new Uint8Array(hashBuffer)) @@ -136,10 +140,14 @@ function hashQuery(query: string) { const onSave = () => { const query = (url.params.q || '').trim() if (query) { - console.log('queryHash', queryHash.value) - if (!queryHash.value) return - queriesHistory.value = { ...queriesHistory.value, [queryHash.value]: query } + queriesHistory.value = { + ...queriesHistory.value, + [queryHash.value]: { + query, + timestamp: new Date().toISOString(), + }, + } } } @@ -193,7 +201,10 @@ export function QueryEditor() { onKeyDown={(e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault() - onRun() + const q = url.params.q + if (q) { + onRun(q) + } } }} class='textarea w-full h-full font-mono text-sm leading-6 pl-12 pr-3 py-3 bg-base-100 border-base-300 focus:outline-none focus:ring-0 focus:shadow-none focus:border-primary resize-none' @@ -431,10 +442,13 @@ export function Header() { ) @@ -663,9 +677,11 @@ export function TabNavigation({ )} {activeTab !== 'logs' && ( - + )} {activeTab !== 'queries' && ( <> @@ -746,7 +762,7 @@ export function LogsViewer() {
    - + {logThreads.map((header) => (
    , - queries: , - logs: , -} +const CommingSoon = ({ title }: { title: string }) => ( +
    +

    {title}

    +

    This feature is coming soon!

    +
    +) + +type DrawerTab = 'history' | 'insert' +const drawerViews: Record = { + history: , + insert: , +} as const const Drawer = () => (
    - + { + if (!e.currentTarget.checked) { + navigate({ params: { drawer: null }, replace: true }) + } + }} + />
    - +
    +
    +
    + {drawerViews[url.params.drawer as DrawerTab] || ( +
    +

    No view selected

    +

    Please select a view from the sidebar.

    +
    + )} +
    +
    +
    ) +const TabViews = { + tables: , + queries: , + logs: , +} + export const DeploymentPage = () => { const tab = (url.params.tab as 'tables' | 'queries' | 'logs') || 'tables' const view = TabViews[tab]