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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,7 @@ nul
*.wasm

# debug artefacts
*.sln
*.sln


packages/server-admin-ui-react19/*
2 changes: 1 addition & 1 deletion packages/server-admin-ui/src/store/slices/dataSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const createDataSlice: StateCreator<DataSlice, [], [], DataSlice> = (

const newContextMeta = {
...contextMeta,
[path]: metaData as MetaData
[path]: { ...contextMeta[path], ...metaData } as MetaData
}

return {
Expand Down
16 changes: 15 additions & 1 deletion packages/server-admin-ui/src/utils/unitConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export function convertValue(
siUnit: string,
category: string,
presetDetails: PresetDetails | null,
unitDefinitions: UnitDefinitions | null
unitDefinitions: UnitDefinitions | null,
displayUnits?: { targetUnit?: string; formula?: string; symbol?: string }
): ConvertedValue | null {
if (typeof value !== 'number' || !category) {
return null
Expand All @@ -55,6 +56,19 @@ export function convertValue(
if (category === 'base' && siUnit) {
return { value, unit: siUnit }
}
// "custom" category uses explicitly stored formula/targetUnit
if (category === 'custom' && displayUnits?.formula) {
try {
const compiled = getCompiledFormula(displayUnits.formula)
const converted = compiled.evaluate({ value })
return {
value: converted,
unit: displayUnits.symbol || displayUnits.targetUnit || ''
}
} catch {
return null
}
}
if (!presetDetails || !unitDefinitions) {
return null
}
Expand Down
13 changes: 12 additions & 1 deletion packages/server-admin-ui/src/views/DataBrowser/DataRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ function DataRow({
category = findCategoryForPath(data.path, defaultCategories) ?? undefined
}

const displayUnits =
(meta as Record<string, unknown> | null)?.displayUnits &&
typeof (meta as Record<string, unknown>).displayUnits === 'object'
? (meta as Record<string, unknown>).displayUnits as {
targetUnit?: string
formula?: string
symbol?: string
}
: undefined

let convertedValue: number | null = null
let convertedUnit: string | null = null
if (category && typeof data.value === 'number') {
Expand All @@ -138,7 +148,8 @@ function DataRow({
units,
category,
presetDetails,
unitDefinitions
unitDefinitions,
displayUnits
)
if (converted && converted.unit !== units) {
convertedValue = converted.value
Expand Down
87 changes: 68 additions & 19 deletions packages/server-admin-ui/src/views/DataBrowser/Meta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ interface MetaFormRowProps {
idPrefix: string
categories?: string[]
siUnit?: string
unitDefinitions?: UnitDefinitions | null
description?: string
}

Expand Down Expand Up @@ -203,6 +204,7 @@ const CATEGORY_BADGE_COLORS: Record<string, string> = {
interface CategorySelectProps extends ValueRenderProps {
categories?: string[]
siUnit?: string
unitDefinitions?: UnitDefinitions | null
}

const CategorySelect: React.FC<CategorySelectProps> = ({
Expand All @@ -211,28 +213,65 @@ const CategorySelect: React.FC<CategorySelectProps> = ({
setValue,
inputId,
categories,
siUnit
siUnit,
unitDefinitions
}) => {
const displayUnits = value as DisplayUnits | undefined
const category = displayUnits?.category || ''
const categoryList =
categories !== undefined ? categories : DEFAULT_CATEGORIES
const conversions =
siUnit && unitDefinitions ? unitDefinitions[siUnit]?.conversions : undefined
return (
<Form.Select
id={inputId}
disabled={disabled}
value={category}
size="sm"
onChange={(e) => setValue({ category: e.target.value })}
>
<option value="">-- No category --</option>
{siUnit && <option value="base">base ({siUnit})</option>}
{categoryList.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</Form.Select>
<>
<Form.Select
id={inputId}
disabled={disabled}
value={category}
size="sm"
onChange={(e) => setValue({ category: e.target.value })}
>
<option value="">-- No category --</option>
{siUnit && <option value="base">base ({siUnit})</option>}
{conversions && <option value="custom">custom unit</option>}
{categoryList.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</Form.Select>
{category === 'custom' && conversions && (
<Form.Select
disabled={disabled}
value={displayUnits?.targetUnit || ''}
size="sm"
style={{ marginTop: '4px' }}
onChange={(e) => {
const targetUnit = e.target.value
if (!targetUnit) {
setValue({ category: 'custom' })
return
}
const conv = conversions[targetUnit]
setValue({
category: 'custom',
targetUnit,
formula: conv?.formula,
inverseFormula: conv?.inverseFormula,
symbol: conv?.symbol || targetUnit
})
}}
>
<option value="">-- Select unit --</option>
{Object.entries(conversions).map(([unit, conv]) => (
<option key={unit} value={unit}>
{conv.symbol || unit}
{conv.longName ? ` - ${conv.longName}` : ''}
</option>
))}
</Form.Select>
)}
</>
)
}

Expand Down Expand Up @@ -453,7 +492,7 @@ const DisplaySelect: React.FC<ValueRenderProps> = ({
}

const DisplayUnitsView: React.FC<CategorySelectProps> = (props) => {
const { disabled, categories, siUnit } = props
const { disabled, categories, siUnit, unitDefinitions } = props
if (disabled) {
const displayUnits = props.value as DisplayUnits | undefined
if (!displayUnits || !displayUnits.category) {
Expand All @@ -480,7 +519,14 @@ const DisplayUnitsView: React.FC<CategorySelectProps> = (props) => {
</div>
)
}
return <CategorySelect {...props} categories={categories} siUnit={siUnit} />
return (
<CategorySelect
{...props}
categories={categories}
siUnit={siUnit}
unitDefinitions={unitDefinitions}
/>
)
}

const METAFIELDRENDERERS: Record<
Expand Down Expand Up @@ -510,6 +556,7 @@ const METAFIELDRENDERERS: Record<
{...p}
categories={props.categories}
siUnit={props.siUnit}
unitDefinitions={props.unitDefinitions}
/>
)}
description="Category for unit conversion"
Expand Down Expand Up @@ -599,7 +646,8 @@ const Meta: React.FC<MetaProps> = ({ meta, path, context }) => {
siUnit,
category,
presetDetails,
unitDefinitions
unitDefinitions,
localMeta.displayUnits
)

const handleEdit = () => {
Expand Down Expand Up @@ -771,6 +819,7 @@ const Meta: React.FC<MetaProps> = ({ meta, path, context }) => {
disabled: !isEditing,
categories: filteredCategories,
siUnit,
unitDefinitions,
setValue: (metaFieldValue) =>
setLocalMeta({ ...localMeta, [key]: metaFieldValue }),
setKey: (metaFieldKey) => {
Expand Down
10 changes: 5 additions & 5 deletions src/interfaces/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ function handleValuesMeta(
// Clone and enhance metadata with displayUnits formulas
meta = JSON.parse(JSON.stringify(meta))
let storedDisplayUnits = (meta as Record<string, unknown>)
.displayUnits as { category?: string } | undefined
.displayUnits as Record<string, unknown> | undefined
if (!storedDisplayUnits?.category && path) {
const defaultCategory = getDefaultCategory(path)
if (defaultCategory) {
Expand All @@ -918,7 +918,7 @@ function handleValuesMeta(
if (storedDisplayUnits?.category) {
const username = this.spark.request.skPrincipal?.identifier
const enhanced = resolveDisplayUnits(
{ category: storedDisplayUnits.category },
storedDisplayUnits as { category: string; targetUnit?: string; formula?: string; inverseFormula?: string; symbol?: string; displayFormat?: string },
(meta as Record<string, unknown>).units as string | undefined,
username
)
Expand Down Expand Up @@ -1157,12 +1157,12 @@ function handleRealtimeConnection(
const fullPath = 'vessels.self.' + path
const pathMeta =
(getMetadata(fullPath) as Record<string, unknown>) || {}
const storedDU = pathMeta.displayUnits as Record<string, unknown> | undefined
const category =
(pathMeta.displayUnits as { category?: string } | undefined)
?.category || getDefaultCategory(path)
(storedDU?.category as string | undefined) || getDefaultCategory(path)
if (category) {
const displayUnits = resolveDisplayUnits(
{ category },
{ ...storedDU, category } as { category: string; targetUnit?: string; formula?: string; inverseFormula?: string; symbol?: string; displayFormat?: string },
pathMeta.units as string | undefined,
username
)
Expand Down
39 changes: 39 additions & 0 deletions src/unitpreferences/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,40 @@ export function resolveDisplayUnits(
}
}

// "custom" category stores explicit conversion info
if (category === 'custom') {
if (!storedDisplayUnits.targetUnit) {
return null
}
// If formula is stored, use it directly
if (storedDisplayUnits.formula) {
return {
category: 'custom',
targetUnit: storedDisplayUnits.targetUnit,
formula: storedDisplayUnits.formula,
inverseFormula: storedDisplayUnits.inverseFormula || '',
symbol: storedDisplayUnits.symbol || storedDisplayUnits.targetUnit,
displayFormat: storedDisplayUnits.displayFormat
}
}
// Otherwise look up from definitions using pathSiUnit
if (pathSiUnit) {
const definitions = getMergedDefinitions()
const conversion = definitions[pathSiUnit]?.conversions?.[storedDisplayUnits.targetUnit]
if (conversion) {
return {
category: 'custom',
targetUnit: storedDisplayUnits.targetUnit,
formula: conversion.formula,
inverseFormula: conversion.inverseFormula,
symbol: conversion.symbol || storedDisplayUnits.targetUnit,
displayFormat: storedDisplayUnits.displayFormat
}
}
}
return null
}

const categoriesData = getCategories()
const definitions = getMergedDefinitions()
const preset = username ? getActivePresetForUser(username) : getActivePreset()
Expand Down Expand Up @@ -98,6 +132,11 @@ export function validateCategoryAssignment(
return null
}

// "custom" category is always valid - user picks an explicit target unit
if (category === 'custom') {
return null
}

const categoriesData = getCategories()
const preset = getActivePreset()

Expand Down
3 changes: 3 additions & 0 deletions src/unitpreferences/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export interface UnitPreferencesConfig {
export interface DisplayUnitsMetadata {
category: string
targetUnit?: string // Only if path override
formula?: string // Only if custom category
inverseFormula?: string // Only if custom category
symbol?: string // Only if custom category
displayFormat?: string // Only if path override
}

Expand Down
Loading