Skip to content
Draft
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
9 changes: 7 additions & 2 deletions web/components/QueryHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChevronRight, Clock, Play, Search, Trash2 } from 'lucide-preact'
import { A, navigate, url } from '@01edu/signal-router'
import { queriesHistory, runQuery } from '../lib/shared.tsx'
import { highlightSQL } from '../lib/highlight-sql.ts'

const deleteQuery = (hash: string) => {
const updatedHistory = { ...queriesHistory.value }
Expand Down Expand Up @@ -58,8 +59,12 @@ export const QueryHistory = () => {
<Clock class='w-3 h-3' />
{new Date(item.timestamp).toLocaleString()}
</div>
<p class='font-mono text-sm truncate mt-1' title={item.query}>
{item.query}
<p
class='font-mono text-sm truncate mt-1'
title={item.query}
ref={highlightSQL}
>
item.query
</p>
<div class='text-xs text-base-content/60 mt-1'>
{item.columns} columns, {item.rows} rows
Expand Down
96 changes: 96 additions & 0 deletions web/lib/highlight-sql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const SQLITE_KEYWORDS =
'CURRENT_TIMESTAMP|AUTOINCREMENT|CURRENT_DATE|CURRENT_TIME|MATERIALIZED|TRANSACTION|CONSTRAINT|DEFERRABLE|REFERENCES|EXCLUSIVE|FOLLOWING|GENERATED|IMMEDIATE|INITIALLY|INTERSECT|PARTITION|PRECEDING|RECURSIVE|RETURNING|SAVEPOINT|TEMPORARY|UNBOUNDED|CONFLICT|DATABASE|DEFERRED|DISTINCT|RESTRICT|ROLLBACK|ANALYZE|BETWEEN|CASCADE|COLLATE|CURRENT|DEFAULT|EXCLUDE|EXPLAIN|FOREIGN|INDEXED|INSTEAD|NATURAL|NOTHING|NOTNULL|PRIMARY|REINDEX|RELEASE|REPLACE|TRIGGER|VIRTUAL|WITHOUT|ACTION|ALWAYS|ATTACH|BEFORE|COLUMN|COMMIT|CREATE|DELETE|DETACH|ESCAPE|EXCEPT|EXISTS|FILTER|GROUPS|HAVING|IGNORE|INSERT|ISNULL|OFFSET|OTHERS|PRAGMA|REGEXP|RENAME|SELECT|UNIQUE|UPDATE|VACUUM|VALUES|WINDOW|ABORT|AFTER|ALTER|BEGIN|CHECK|CROSS|FIRST|GROUP|INDEX|INNER|LIMIT|MATCH|NULLS|ORDER|OUTER|QUERY|RAISE|RANGE|RIGHT|TABLE|UNION|USING|WHERE|CASE|CAST|DESC|DROP|EACH|ELSE|FAIL|FROM|FULL|GLOB|INTO|JOIN|LAST|LEFT|LIKE|NULL|OVER|PLAN|ROWS|TEMP|THEN|TIES|VIEW|WHEN|WITH|ADD|ALL|AND|ASC|END|FOR|KEY|NOT|ROW|SET|AS|BY|DO|IF|IN|IS|NO|OF|ON|OR|TO'

const TOKENS = {
comment: /--[^\n\r]*|\/\*[\s\S]*?\*\//y,
string: /'(?:''|[^'])*'/y,
'id-quoted': /"(?:""|[^"])*"|\[(?:[^\]])*\]|`(?:``|[^`]*)`/y,
'param-named': /[:@$][A-Za-z_][A-Za-z0-9_]*/y,
'param-qmark': /\?(?:\d+)?/y,
operator: /\|\||<<|>>|<=|>=|==|!=|<>|[-+*/%&|<>=~]/y,
punct: /[(),.;]/y,
keyword: new RegExp(String.raw`\b(?:${SQLITE_KEYWORDS})\b`, 'iy'),
id: /[A-Za-z_][A-Za-z0-9_$]*/y,
number: /\b(?:0x[0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/y,
} as const

type TokenType = keyof typeof TOKENS

const defs =
(Object.entries(TOKENS) as [TokenType, (typeof TOKENS)[TokenType]][])
.map(([type, re]) => ({ type, re, hl: new Highlight() }))

export function highlightSQL(elem: HTMLElement | null): (() => void) | void {
const firstChild = elem?.firstChild
if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE) return
const sql = (firstChild as Text).data
let i = -1
const cleanups = new Set<{ hl: Highlight; range: Range }>()
main: while (++i < sql.length) {
if (sql[i] === ' ' || sql[i] === '\t' || sql[i] === '\n') continue
for (const { hl, re } of defs) {
re.lastIndex = i
const m = re.exec(sql)
if (!m) continue
const value = m[0]
const end = i + value.length
const range = new Range()
range.setStart(firstChild, i)
range.setEnd(firstChild, end)
hl.add(range)
cleanups.add({ hl, range })
i = end - 1
continue main
}
}
return () => {
for (const { hl, range } of cleanups) hl.delete(range)
}
}

// Setup the style and register the highligths
const css = String.raw
const style = document.createElement('style')
style.innerHTML = css`
::highlight(comment) {
color: #a6acb9;
}
::highlight(string) {
color: #5c99d6;
}
::highlight(number) {
color: #c594c5;
}
::highlight(id-quoted) {
color: #99c794;
}
::highlight(id) {
color: #5fb4b4;
}
::highlight(param-named) {
color: #f7f7f7;
}
::highlight(param-qmark) {
color: #f9ae58;
}
::highlight(operator) {
color: #f97b58;
}
::highlight(punct) {
color: #596878;
}
::highlight(keyword) {
color: #c695c6;
}
`
// Unused theme colors:
// normal #D8DEE9
// end #F9AE58
// error #EC5F66
// selection --background=#3E4347
// search_match --background=#596673
// option #5FB4B4
// host_remote #A3CE9E
document.head.append(style)

for (const { hl, type } of defs) CSS.highlights.set(type, hl)
24 changes: 14 additions & 10 deletions web/pages/DeploymentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from '../components/Filtre.tsx'
import { computed, effect, Signal, untracked } from '@preact/signals'
import { api, type ApiOutput } from '../lib/api.ts'
import { highlightSQL } from '../lib/highlight-sql.ts'
import { QueryHistory } from '../components/QueryHistory.tsx'

import type { ComponentChildren } from 'preact'
Expand All @@ -59,9 +60,7 @@ export const metricsData = api['GET/api/deployment/metrics-sql'].signal()

const toastSignal = new Signal<
{ message: string; type: 'info' | 'error' } | null
>(
null,
)
>(null)

function toast(message: string, type: 'info' | 'error' = 'info') {
toastSignal.value = { message, type }
Expand Down Expand Up @@ -184,17 +183,16 @@ const SQLEditor = () => (
<div class='resize-y min-h-[120px] max-h-[80vh] overflow-hidden border border-base-300 rounded-lg bg-base-100'>
<div class='relative h-full bg-base-100 rounded-lg overflow-hidden focus-within:border-primary/50 transition-colors'>
<LineNumbers />
<textarea
value={url.params.q || ''}
<pre
ref={highlightSQL}
onInput={handleInput}
onKeyDown={handleKeyDown}
class='w-full h-full font-mono text-sm leading-6 pl-12 pr-4 py-3 bg-transparent border-0 focus:outline-none focus:ring-0 text-base-content caret-primary resize-none tracking-wide placeholder:text-base-content/40'
placeholder='SELECT * FROM users WHERE active = true;'
aria-label='SQL editor'
spellcheck={false}
autocapitalize='off'
autocomplete='off'
/>
contenteditable='plaintext-only'
>{url.params.q || 'SELECT * FROM users WHERE active = true;'}</pre>
</div>
</div>
)
Expand Down Expand Up @@ -1607,7 +1605,10 @@ function MetricDetail() {
<div class='text-[10px] font-bold uppercase tracking-widest text-base-content/40 mb-2 flex items-center gap-1.5'>
<Database class='w-3.5 h-3.5' /> Query
</div>
<pre class='font-mono text-[12px] text-base-content/80 bg-base-100 rounded-lg border border-base-200 p-3 overflow-x-auto whitespace-pre-wrap break-all leading-relaxed'>{metric.query}</pre>
<pre
ref={highlightSQL}
class='font-mono text-[12px] text-base-content/80 bg-base-100 rounded-lg border border-base-200 p-3 overflow-x-auto whitespace-pre-wrap break-all leading-relaxed'
>{metric.query}</pre>
</div>
<div class='grid grid-cols-1 lg:grid-cols-2 gap-5'>
{metric.status && <StatusCounters status={metric.status} />}
Expand All @@ -1633,7 +1634,10 @@ function MetricRow({ metric }: MetricRowProps) {
params={{ expanded: isExpanded ? null : metric.id }}
>
<div class='flex-1 min-w-0'>
<div class='font-mono text-[13px] text-base-content/85 truncate'>
<div
ref={highlightSQL}
class='font-mono text-[13px] text-base-content/85 truncate'
>
{metric.query}
</div>
<div class='mt-1.5 h-1 bg-base-200 rounded-full overflow-hidden max-w-[200px]'>
Expand Down
Loading