Files
windmill/frontend/src/lib/components/dbTableFormat.ts
T
Diego ImbertandClaude Opus 5.5 4fd7d62bd0 feat: rework the db manager: native grid, tabs, sql editor, joined columns (#11340)
* feat: replace ag-grid in the db manager table viewer with a native grid

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add user-managed data, diagram and sql editor tabs to the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add schema autocomplete to the db manager sql editor and polish its layout

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add joined foreign key columns and draggable tabs to the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: move db manager tabs on drop instead of mid-drag

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: open followed foreign keys in a new db manager tab

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: scroll large tables, drop stale joins and return to the last tab on close

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: tolerate the db manager tabs going away while switching data table

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep saved joined columns while metadata loads, test joins in every dialect

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: put the db manager grid on the input surface in dark mode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: address db manager review nits on joins, boolean keys and sql quoting

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep the dark border color on the left pinned column edge

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: tone down the db manager tree menu icons

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: write json and jsonb values from the db manager through a text cast

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: dim unrelated diagram tables less on hover

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: match json columns as text when deleting a db manager row

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: compare postgres json columns as text in exact db manager filters

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: pick the columns the db manager grid shows

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: toggle every db manager column from one checkbox

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: word joined columns as a view in the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: keep data table roles and access visible when unavailable, with the reason

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: keep data table and instance roles visible in settings when unavailable

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: hide a db manager column from its header menu

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: format db manager columns with a unit, significant digits and color rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: drop the before/after hints from the db manager unit picker

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: write the euro after the amount and keep units off non-numeric values

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: color rule presets, bold and italic, layered and reorderable rules in the db manager

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: decimals, thousands separator, compact notation and alignment in db manager column formats

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: compact db manager format controls, a notation toggle group and a reset button

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: db manager format and filter nits

Keep the value's scale when decimals are auto, read boolean color-rule conditions as
booleans, let a rule's text color reach foreign-key links, close the formatter when the
columns picker opens, and filter BigQuery complex columns through TO_JSON_STRING.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 15:58:25 +02:00

144 lines
5.8 KiB
TypeScript

import { dbColumnKind, matchesColumnFilter } from './dbTableFilters'
/** How a column's values are displayed. Display only: filters, sorting, edits and copies keep
* the raw value. */
export type ColumnFormat = {
unit?: { symbol: string; position: 'before' | 'after' }
/** Digits after the decimal point; undefined keeps those of the value. */
decimals?: number
/** Grouped by thousands (1,234,567) or shortened (1.2M); undefined writes 1234567. */
notation?: 'thousands' | 'compact'
/** Undefined aligns numbers right and anything else left. */
align?: 'left' | 'right'
/** Every rule whose condition matches styles the cell, a later one overriding an earlier one. */
rules?: ColorRule[]
}
export type ColorRule = {
/** In the search bar's filter syntax: `=paid`, `>= 4`, `pend`. Empty matches every cell. */
condition: string
bg?: string
text?: string
bold?: boolean
italic?: boolean
}
/** Picked when adding a rule, so that one click sets a background and a text color that read
* well together. */
export const RULE_PRESETS: { bg?: string; text: string }[] = [
{ bg: '#fee2e2', text: '#991b1b' },
{ bg: '#ffedd5', text: '#9a3412' },
{ bg: '#fef3c7', text: '#92400e' },
{ bg: '#dcfce7', text: '#166534' },
{ bg: '#ccfbf1', text: '#115e59' },
{ bg: '#dbeafe', text: '#1e40af' },
{ bg: '#ede9fe', text: '#5b21b6' },
{ bg: '#fce7f3', text: '#9d174d' },
{ bg: '#f3f4f6', text: '#374151' },
{ text: '#dc2626' },
{ text: '#ea580c' },
{ text: '#d97706' },
{ text: '#16a34a' },
{ text: '#0d9488' },
{ text: '#2563eb' },
{ text: '#7c3aed' },
{ text: '#db2777' },
{ text: '#9ca3af' }
]
// Each where it is usually written: 12.5%, $5, 5 €, £5, ¥5.
export const UNIT_PRESETS: { symbol: string; position: 'before' | 'after' }[] = [
{ symbol: '%', position: 'after' },
{ symbol: '$', position: 'before' },
{ symbol: '\u00A0€', position: 'after' },
{ symbol: '£', position: 'before' },
{ symbol: '¥', position: 'before' }
]
const NUMERIC = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i
/** Previews read most values as text, so a number may arrive as a string. */
export function asNumber(value: unknown): number | undefined {
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined
if (typeof value === 'string' && NUMERIC.test(value.trim())) return Number(value.trim())
return undefined
}
/** Whether the numbers of the column are formatted at all. */
export function formatsNumbers(format: ColumnFormat | undefined): boolean {
return !!format && (format.decimals !== undefined || !!format.notation || !!format.unit?.symbol)
}
/** The text a cell shows for `value`, or undefined when the format leaves it as is. */
export function formatValue(value: unknown, format: ColumnFormat | undefined): string | undefined {
if (!format || value === null || value === undefined) return undefined
const n = asNumber(value)
// A unit on text would read as a quantity: `N/A` must not become `$N/A`.
if (n === undefined) return undefined
if (!formatsNumbers(format)) return undefined
// A string keeps the digits a float would lose: numeric columns are read as text.
const source = typeof value === 'string' ? value.trim() : String(n)
const compact = format.notation === 'compact'
// Auto keeps the value's own scale, trailing zeros included: numeric(10,2) shows 1234.50.
const scale = /e/i.test(source) ? 0 : Math.min(source.split('.')[1]?.length ?? 0, 100)
const text = new Intl.NumberFormat('en-US', {
useGrouping: format.notation === 'thousands',
notation: compact ? 'compact' : 'standard',
minimumFractionDigits: format.decimals ?? (compact ? undefined : scale),
// Left to the value, or to the compact notation's own rounding.
maximumFractionDigits: format.decimals ?? (compact ? undefined : 100)
}).format(typeof value === 'string' ? (source as Intl.StringNumericLiteral) : n)
if (!format.unit?.symbol) return text
const { symbol, position } = format.unit
if (position === 'after') return `${text}${symbol}`
// A sign reads before a leading unit: -$5, not $-5.
return text.startsWith('-') ? `-${symbol}${text.slice(1)}` : `${symbol}${text}`
}
const TRUE_TEXT = ['true', 't', '1', 'yes', 'y']
const FALSE_TEXT = ['false', 'f', '0', 'no', 'n']
function asBoolean(value: unknown): boolean | undefined {
if (typeof value === 'boolean') return value
const text = String(value).trim().toLowerCase()
return TRUE_TEXT.includes(text) ? true : FALSE_TEXT.includes(text) ? false : undefined
}
/** The search bar filters a boolean column with a toggle, but a rule's condition is typed:
* `true`, `=false`, `t`… are read as the boolean they name. */
function matchesCondition(value: unknown, datatype: string | undefined, condition: string) {
if (dbColumnKind(datatype) !== 'boolean') return matchesColumnFilter(value, datatype, condition)
const expected = asBoolean(condition.replace(/^=/, ''))
return expected !== undefined && value !== null && asBoolean(value) === expected
}
export type CellStyle = Pick<ColorRule, 'bg' | 'text' | 'bold' | 'italic'>
/** Every rule `value` matches, layered in order: a later rule overrides only what it sets. */
export function cellStyle(
value: unknown,
datatype: string | undefined,
format: ColumnFormat | undefined
): CellStyle | undefined {
let style: CellStyle | undefined
for (const rule of format?.rules ?? []) {
const condition = rule.condition.trim()
// No condition styles every cell.
if (condition && !matchesCondition(value, datatype, condition)) continue
style = { ...style }
if (rule.bg) style.bg = rule.bg
if (rule.text) style.text = rule.text
if (rule.bold) style.bold = true
if (rule.italic) style.italic = true
}
return style
}
export function isEmptyFormat(format: ColumnFormat): boolean {
return (
!format.unit?.symbol &&
format.decimals === undefined &&
!format.notation &&
!format.align &&
!format.rules?.length
)
}