diff --git a/web/components/Filtre.tsx b/web/components/Filtre.tsx new file mode 100644 index 0000000..2d5d254 --- /dev/null +++ b/web/components/Filtre.tsx @@ -0,0 +1,258 @@ +import { ArrowUpDown, Filter, Plus } from 'lucide-preact' +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 + +function parseFilters(prefix: string): FilterRow[] { + const data = url.getAll(`f${prefix}`) + const rows: FilterRow[] = [] + for (let i = 0; i < data.length; i++) { + const str = data[i] + const first = str.indexOf(',') + const second = str.indexOf(',', first + 1) + const key = str.slice(0, first) + const op = str.slice(first + 1, second) + const value = str.slice(second + 1) + rows.push({ idx: i, key, op, value }) + } + + if (!rows.length) rows.push({ idx: 0, key: '', op: 'eq', value: '' }) + return rows +} + +function setFilters(prefix: string, rows: FilterRow[]) { + const next: string[] = [] + for (let i = 0; i < rows.length; i++) { + const v = rows[i] + next.push([v.key, v.op, v.value].join(',')) + } + navigate({ params: { [`f${prefix}`]: next }, replace: true }) +} + +function addFilter(prefix: string) { + const rows = parseFilters(prefix) + setFilters(prefix, [...rows, { + idx: rows.length, + key: '', + op: 'eq', + value: '', + }]) +} + +function updateFilter( + prefix: string, + idx: number, + patch: Partial>, +) { + const rows = parseFilters(prefix) + setFilters(prefix, rows.map((r) => (r.idx === idx ? { ...r, ...patch } : r))) +} + +function removeFilter(prefix: string, idx: number) { + const rows = parseFilters(prefix) + setFilters( + prefix, + rows.filter((r) => r.idx !== idx), + ) +} + +// --- Sort (URL) ------------------------------------------------------------- +type SortRow = { idx: number; key: string; dir: 'asc' | 'desc' } + +function parseSort(prefix: string): SortRow[] { + const data = url.getAll(`s${prefix}`) + const rows: SortRow[] = [] + for (let i = 0; i < data.length; i++) { + const str = data[i] + const first = str.indexOf(',') + const key = str.slice(0, first) + const dir = str.slice(first + 1) as 'asc' | 'desc' + rows.push({ idx: i, key, dir }) + } + if (!rows.length) rows.push({ idx: 0, key: '', dir: 'asc' }) + return rows +} + +function setSort(prefix: string, rows: SortRow[]) { + const next: string[] = [] + for (let i = 0; i < rows.length; i++) { + const v = rows[i] + next.push([v.key, v.dir].join(',')) + } + navigate({ params: { [`s${prefix}`]: next }, replace: true }) +} + +function addSort(prefix: string) { + const rows = parseSort(prefix) + setSort(prefix, [...rows, { idx: rows.length, key: '', dir: 'asc' }]) +} + +function updateSort( + prefix: string, + idx: number, + patch: Partial>, +) { + const rows = parseSort(prefix) + setSort(prefix, rows.map((r) => r.idx === idx ? { ...r, ...patch } : r)) +} + +function removeSort(prefix: string, idx: number) { + const rows = parseSort(prefix) + setSort(prefix, rows.filter((r) => r.idx !== idx)) +} + +// --- Components ------------------------------------------------------------- + +export const FilterMenu = ( + { filterKeyOptions, tag }: { + filterKeyOptions: readonly string[] + tag: 'tables' | 'logs' + }, +) => { + const prefix = tag === 'tables' ? 't' : 'l' + const rows = parseFilters(prefix) + + return ( + + ) +} + +export const SortMenu = ({ tag, sortKeyOptions }: { + tag: 'tables' | 'logs' + sortKeyOptions: readonly string[] +}) => { + const prefix = tag === 'tables' ? 't' : 'l' + const rows = parseSort(prefix) + return ( + + ) +} diff --git a/web/components/Layout.tsx b/web/components/Layout.tsx index 42cb47c..4437486 100644 --- a/web/components/Layout.tsx +++ b/web/components/Layout.tsx @@ -1,16 +1,4 @@ import { JSX } from 'preact' -import { SideBar } from './SideBar.tsx' - -export const PageLayoutWithSideBar = ( - { children }: { children: JSX.Element | JSX.Element[] | null }, -) => ( -
- -
- {children} -
-
-) export const PageLayout = ( { children }: { children: JSX.Element | JSX.Element[] }, diff --git a/web/components/SideBar.tsx b/web/components/SideBar.tsx index d389f11..ed81c23 100644 --- a/web/components/SideBar.tsx +++ b/web/components/SideBar.tsx @@ -1,92 +1,85 @@ +import { JSX } from 'preact' import { ChevronsLeft, ChevronsRight, - HardDrive, - ListTodo, LucideIcon, Settings, } from 'lucide-preact' -import { A, LinkProps, url } from '../lib/router.tsx' import { user } from '../lib/session.ts' +import { A, url } from '../lib/router.tsx' -const NavLink = ( - { icon: Icon, children, current, ...props }: LinkProps & { - current: boolean - icon: LucideIcon - }, -) => ( - - - {url.params.sidebar_collapsed !== 'true' && {children}} - -) +export type SidebarItem = { + label: string + icon: LucideIcon + component: () => JSX.Element +} -export const SideBar = () => { - const { nav, sidebar_collapsed } = url.params - const isCollapsed = sidebar_collapsed === 'true' +export function Sidebar( + { sidebarItems, sbi, title }: { + sidebarItems: Record + sbi?: string + title?: string + }, +) { + const { sb } = url.params return ( -
-
-
- {!isCollapsed &&

Project

} +
+ +
+
+
+ {!sb && ( + + {title || 'Project Name'} + + )} + + {sb + ? + : } + +
+
+ +
+ +
+ + - -
- - Settings - -
) diff --git a/web/lib/router.tsx b/web/lib/router.tsx index a6f02c5..fbe9f0a 100644 --- a/web/lib/router.tsx +++ b/web/lib/router.tsx @@ -44,12 +44,13 @@ const navigateUrl = (to: string, replace = false) => { dispatchNavigation() } +type ParamPrimitive = string | number | boolean +type ParamValue = ParamPrimitive | null | undefined | ParamPrimitive[] type GetUrlProps = { href?: string hash?: string - params?: - | URLSearchParams - | Record + // params supports arrays to allow multiple identical keys: { tag: ['a','b'] } -> ?tag=a&tag=b + params?: URLSearchParams | Record } const getUrl = ({ href, hash, params }: GetUrlProps) => { @@ -63,6 +64,19 @@ const getUrl = ({ href, hash, params }: GetUrlProps) => { return url } for (const [key, value] of Object.entries(params)) { + if (Array.isArray(value)) { + // Remove existing then append each to preserve ordering + url.searchParams.delete(key) + for (const v of value) { + if (v === false || v == null) continue // skip deletions inside arrays + if (v === true) { + url.searchParams.append(key, '') + } else { + url.searchParams.append(key, v) + } + } + continue + } if (value === true) { url.searchParams.set(key, '') } else if (value === false || value == null) { @@ -192,5 +206,9 @@ export const url = { return hashSignal.value }, params, + // Retrieve all values (including duplicates) for a given key + getAll: (key: string) => urlSignal.value.searchParams.getAll(key), + // All param entries preserving duplicates & order + paramEntries: () => [...urlSignal.value.searchParams.entries()], equals: (url: URL) => isCurrentURL(url), } diff --git a/web/pages/DeploymentPage.tsx b/web/pages/DeploymentPage.tsx new file mode 100644 index 0000000..651a2a1 --- /dev/null +++ b/web/pages/DeploymentPage.tsx @@ -0,0 +1,673 @@ +import { A, navigate, url } from '../lib/router.tsx' +import { + AlertCircle, + AlertTriangle, + Bug, + Clock, + Download, + Eye, + FileText, + Info, + MoreHorizontal, + Play, + Plus, + RefreshCw, + Save, + Search, + Table, + XCircle, +} from 'lucide-preact' +import { deployments, sidebarItems } from './ProjectPage.tsx' +import { FilterMenu, SortMenu } from '../components/Filtre.tsx' + +type AnyRecord = Record + +const onRun = async () => { + // TODO: call backend here +} + +export function QueryEditor() { + const query = url.params.q || '' + const results: AnyRecord[] = [] + const running = false + + return ( +
+
+
+
+
+ {Array(Math.max(1, (query.match(/\n/g)?.length ?? 0) + 1)) + .keys().map((i) =>
{i + 1}
).toArray()} +
+
+ +