This commit is contained in:
shamoon
2025-10-13 12:58:45 -07:00
parent 8ab3da9aef
commit 08c6593980
4 changed files with 57 additions and 58 deletions

View File

@@ -152,7 +152,7 @@ Widgets can tint their metric blocks automatically based on rules defined alongs
value: "All good"
```
Supported numeric operators for the `when` property are `gt`, `gte`, `lt`, `lte`, `eq`, `ne`, `between`, and `outside`. String rules support `equals`, `includes`, `startsWith`, `endsWith`, and `regex`. Each rule can be inverted with `negate: true`, and string rules may pass `caseSensitive: true` or custom regex `flags`. If you format values before passing them into `<Block>`, also pass the unformatted number or string via the `valueRaw` prop so the highlight engine can evaluate correctly.
Supported numeric operators for the `when` property are `gt`, `gte`, `lt`, `lte`, `eq`, `ne`, `between`, and `outside`. String rules support `equals`, `includes`, `startsWith`, `endsWith`, and `regex`. Each rule can be inverted with `negate: true`, and string rules may pass `caseSensitive: true` or custom regex `flags`. The highlight engine does its best to coerce formatted values, but you will get the most reliable results when you pass plain numbers or strings into `<Block>`.
## Descriptions

View File

@@ -111,14 +111,14 @@ Supported colors are: `slate`, `gray`, `zinc`, `neutral`, `stone`, `amber`, `yel
## Block Highlight Levels
You can override the default Tailwind classes applied when a widget highlight rule resolves to the `good`, `warn`, or `danger` level. Only the `levels` map is read from `settings.yaml`; all highlight rules themselves are defined per widget in `services.yaml`.
You can override the default Tailwind classes applied when a widget highlight rule resolves to the `good`, `warn`, or `danger` level.
```yaml
blockHighlights:
levels:
good: "bg-emerald-400/30 text-emerald-900 dark:bg-emerald-900/30 dark:text-emerald-100"
warn: "bg-amber-300/30 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
danger: "bg-rose-400/30 text-rose-900 dark:bg-rose-900/30 dark:text-rose-100"
good: "bg-emerald-500/40 text-emerald-950 dark:bg-emerald-900/60 dark:text-emerald-400",
warn: "bg-amber-300/30 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200",
danger: "bg-rose-700/45 text-rose-200 dark:bg-rose-950/70 dark:text-rose-400",
```
Any unspecified level falls back to the built-in defaults.

View File

@@ -6,7 +6,7 @@ import { evaluateHighlight, getHighlightClass } from "utils/highlights";
import { BlockHighlightContext } from "./highlight-context";
export default function Block({ value, valueRaw, label, field }) {
export default function Block({ value, label, field }) {
const { t } = useTranslation();
const highlightConfig = useContext(BlockHighlightContext);
@@ -14,9 +14,8 @@ export default function Block({ value, valueRaw, label, field }) {
if (!highlightConfig) return null;
const fieldKey = field || label;
if (!fieldKey) return null;
const candidateValue = valueRaw ?? value;
return evaluateHighlight(fieldKey, candidateValue, highlightConfig);
}, [field, label, value, valueRaw, highlightConfig]);
return evaluateHighlight(fieldKey, value, highlightConfig);
}, [field, label, value, highlightConfig]);
const highlightClass = useMemo(() => {
if (!highlight?.level) return undefined;

View File

@@ -13,8 +13,14 @@ const normalizeFieldKeys = (fields, widgetType) => {
const trimmedKey = key.trim();
if (trimmedKey === "") return acc;
const targetKey = widgetType ? `${widgetType}.${trimmedKey}` : trimmedKey;
acc[targetKey] = value;
acc[trimmedKey] = value;
if (widgetType && !trimmedKey.includes(".")) {
const namespacedKey = `${widgetType}.${trimmedKey}`;
if (!(namespacedKey in acc)) {
acc[namespacedKey] = value;
}
}
return acc;
}, {});
@@ -76,65 +82,59 @@ const parseNumericValue = (value) => {
const trimmed = value.trim();
if (!trimmed) return undefined;
const numericMatch = trimmed.match(/[-+]?\d+(?:[\d.,]*\d)?/);
if (!numericMatch) return undefined;
const direct = Number(trimmed);
if (!Number.isNaN(direct)) return direct;
const numeric = numericMatch[0];
const separators = numeric.match(/[.,]/g) ?? [];
const uniqueSeparators = [...new Set(separators)];
const compact = trimmed.replace(/\s+/g, "");
if (!compact || !/^[-+]?[0-9.,]+$/.test(compact)) return undefined;
if (uniqueSeparators.length === 0) {
const parsed = Number(numeric);
return Number.isNaN(parsed) ? undefined : parsed;
}
const commaCount = (compact.match(/,/g) || []).length;
const dotCount = (compact.match(/\./g) || []).length;
if (uniqueSeparators.length >= 2) {
const lastComma = numeric.lastIndexOf(",");
const lastDot = numeric.lastIndexOf(".");
const decimalSeparator = lastComma > lastDot ? "," : ".";
const thousandsSeparator = decimalSeparator === "." ? "," : ".";
let canonical = numeric;
const thousandsPattern = thousandsSeparator === "." ? /\./g : /,/g;
canonical = canonical.replace(thousandsPattern, "");
if (decimalSeparator === ",") {
canonical = canonical.replace(/,/g, ".");
}
const parsed = Number(canonical);
return Number.isNaN(parsed) ? undefined : parsed;
}
const separator = uniqueSeparators[0];
const occurrences = separators.length;
if (separator === ".") {
if (occurrences === 1) {
const parsed = Number(numeric);
if (commaCount && dotCount) {
const lastComma = compact.lastIndexOf(",");
const lastDot = compact.lastIndexOf(".");
if (lastComma > lastDot) {
const asDecimal = compact.replace(/\./g, "").replace(/,/g, ".");
const parsed = Number(asDecimal);
return Number.isNaN(parsed) ? undefined : parsed;
}
const canonical = numeric.replace(/\./g, "");
const parsed = Number(canonical);
const asThousands = compact.replace(/,/g, "");
const parsed = Number(asThousands);
return Number.isNaN(parsed) ? undefined : parsed;
}
if (separator === ",") {
if (occurrences === 1) {
const decimalCandidate = Number(numeric.replace(/,/g, "."));
const thousandsCandidate = Number(numeric.replace(/,/g, ""));
const candidates = [thousandsCandidate, decimalCandidate].filter((candidate) => !Number.isNaN(candidate));
if (candidates.length === 0) return undefined;
if (candidates.length === 1) return candidates[0];
const uniqueCandidates = [...new Set(candidates)];
return uniqueCandidates.length === 1 ? uniqueCandidates[0] : uniqueCandidates;
if (commaCount) {
const parts = compact.split(",");
if (commaCount === 1 && parts[1]?.length <= 2) {
const parsed = Number(compact.replace(",", "."));
if (!Number.isNaN(parsed)) return parsed;
}
const isGrouped = parts.length > 1 && parts.slice(1).every((part) => part.length === 3);
if (isGrouped) {
const parsed = Number(compact.replace(/,/g, ""));
if (!Number.isNaN(parsed)) return parsed;
}
return undefined;
}
const canonical = numeric.replace(/,/g, "");
const parsed = Number(canonical);
if (dotCount) {
const parts = compact.split(".");
if (dotCount === 1 && parts[1]?.length <= 2) {
const parsed = Number(compact);
if (!Number.isNaN(parsed)) return parsed;
}
const isGrouped = parts.length > 1 && parts.slice(1).every((part) => part.length === 3);
if (isGrouped) {
const parsed = Number(compact.replace(/\./g, ""));
if (!Number.isNaN(parsed)) return parsed;
}
const parsed = Number(compact);
return Number.isNaN(parsed) ? undefined : parsed;
}
const parsed = Number(compact);
return Number.isNaN(parsed) ? undefined : parsed;
}
if (typeof value === "object" && value !== null && "props" in value) {