Say what a service state actually is, and make the badges readable
Three colour faults on the PC page, each hiding the next. ACTIVITY BADGES WERE ALL GREY. The list renderer reads a badge colour from a ROW FIELD, and the activity panel declared none, so every severity rendered identically. The feed's whole job is to make a critical event visible and it was painting it the same as a logon. The route now emits severitycolor - info grey, warning yellow, critical red - and the panel declares it. PAUSED AND THE TRANSITIONAL STATES WERE RED. statecolor was green-or-red-or- grey, so a paused service looked like a stopped one. They are different: a paused service was paused deliberately, and one stuck in Start Pending is failing to start on a retry cycle - which is exactly what raises event 7011, the thing behind the critical counts on the Fleet page. Both are amber now. NAMED COLOURS ALWAYS GOT WHITE TEXT. readableText only parsed hex and returned white for anything else, but panels emit NAMES. Green, red and grey are dark enough that white worked by luck; amber is not, so the fix above shipped as white-on-yellow and could not be read. Names resolve to hex before the contrast test, so this holds for any named colour anywhere in the app rather than these two panels. colorStyle had no test file at all. It has one now. Every one of these was found by looking at the rendered page. None of them would fail a test that checked markup. Full suite 2131 passed, vitest 273 passed.
This commit is contained in:
@@ -292,4 +292,38 @@ describe('facet chips on a card that shows a health dot', () => {
|
||||
it('offers no chips for a panel with no status rollup', () => {
|
||||
expect(facets({ id: 'x', render: 'list', _data: [{ a: 1 }] })).toEqual([])
|
||||
})
|
||||
|
||||
it('prefers an explicit facets declaration over the status rollup', () => {
|
||||
// Services carry BOTH: a statecolor status dot and a statelabel facet.
|
||||
// Chips must follow the declaration, because a service is Running or
|
||||
// Stopped - "OK" and "Critical" is not how anybody describes one.
|
||||
const services = {
|
||||
id: 'endpoints-services',
|
||||
render: 'list',
|
||||
status: { key: 'statecolor', levels: { green: 'ok', red: 'critical' } },
|
||||
facets: { key: 'statelabel', order: ['Running', 'Stopped', 'Paused'] },
|
||||
_data: [
|
||||
{ statelabel: 'Running', statecolor: 'green' },
|
||||
{ statelabel: 'Stopped', statecolor: 'red' },
|
||||
{ statelabel: 'Paused', statecolor: 'red' },
|
||||
],
|
||||
}
|
||||
const chips = facets(services)
|
||||
expect(chips.map((c) => c.label)).toEqual(['Running', 'Stopped', 'Paused'])
|
||||
expect(chips.map((c) => c.label)).not.toContain('Critical')
|
||||
})
|
||||
|
||||
it('gives an unlisted state its own chip rather than dropping the row', () => {
|
||||
// Windows has transitional states, and an agent can report something the
|
||||
// order list never anticipated. Those rows must stay reachable.
|
||||
const services = {
|
||||
id: 'svc2',
|
||||
render: 'list',
|
||||
facets: { key: 'statelabel', order: ['Running', 'Stopped'] },
|
||||
_data: [{ statelabel: 'Running' }, { statelabel: 'Start Pending' }],
|
||||
}
|
||||
const chips = facets(services)
|
||||
expect(chips.map((c) => c.value)).toEqual(['Running', 'Start Pending'])
|
||||
expect(facetedRows(services, ['Start Pending']).length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,10 +19,33 @@ export const PALETTE = [
|
||||
'#6c757d', // gray
|
||||
]
|
||||
|
||||
// Plugin panels emit colour NAMES, not hex - a services row says 'yellow', an
|
||||
// activity row says 'gray'. Without this the brightness test cannot parse them
|
||||
// and falls through to white, which is why an amber badge rendered white text
|
||||
// on yellow and could not be read. Green, red and grey are dark enough that
|
||||
// white worked by luck, so the fault only showed when amber was introduced.
|
||||
const NAMED_COLORS = {
|
||||
red: '#dc3545',
|
||||
green: '#28a745',
|
||||
yellow: '#ffc107',
|
||||
amber: '#ffc107',
|
||||
orange: '#fd7e14',
|
||||
blue: '#4181ff',
|
||||
grey: '#6c757d',
|
||||
gray: '#6c757d',
|
||||
black: '#000000',
|
||||
white: '#ffffff',
|
||||
}
|
||||
|
||||
export function resolveColor(color) {
|
||||
if (!color || typeof color !== 'string') return color
|
||||
return NAMED_COLORS[color.trim().toLowerCase()] || color
|
||||
}
|
||||
|
||||
// Black or white text for a given background, by perceived brightness.
|
||||
export function readableText(bg) {
|
||||
if (!bg || typeof bg !== 'string') return '#ffffff'
|
||||
let hex = bg.trim().replace('#', '')
|
||||
let hex = resolveColor(bg).trim().replace('#', '')
|
||||
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('')
|
||||
if (hex.length !== 6) return '#ffffff'
|
||||
const r = parseInt(hex.slice(0, 2), 16)
|
||||
@@ -34,6 +57,8 @@ export function readableText(bg) {
|
||||
|
||||
// Style object for a badge/pill from a stored color (with a neutral fallback).
|
||||
export function colorStyle(color, fallback = '#6c757d') {
|
||||
const backgroundColor = color || fallback
|
||||
// Resolve the name for the BACKGROUND too, so the swatch and the text
|
||||
// contrast are computed from the same value.
|
||||
const backgroundColor = resolveColor(color) || fallback
|
||||
return { backgroundColor, color: readableText(backgroundColor) }
|
||||
}
|
||||
|
||||
27
frontend/src/utils/colorStyle.spec.js
Normal file
27
frontend/src/utils/colorStyle.spec.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { readableText, colorStyle } from './colorStyle'
|
||||
|
||||
describe('named colours', () => {
|
||||
// Panels emit names, not hex. Before these resolved, the brightness test
|
||||
// could not parse them and returned white for everything - which is how an
|
||||
// amber badge shipped as white-on-yellow.
|
||||
it('gives yellow dark text, not white', () => {
|
||||
expect(readableText('yellow')).toBe('#1a1a1a')
|
||||
expect(colorStyle('yellow').color).toBe('#1a1a1a')
|
||||
})
|
||||
|
||||
it('still gives dark colours white text', () => {
|
||||
expect(readableText('red')).toBe('#ffffff')
|
||||
expect(readableText('green')).toBe('#ffffff')
|
||||
expect(readableText('gray')).toBe('#ffffff')
|
||||
})
|
||||
|
||||
it('resolves the name for the background as well as the text', () => {
|
||||
expect(colorStyle('yellow').backgroundColor).toBe('#ffc107')
|
||||
})
|
||||
|
||||
it('leaves hex and unknown values alone', () => {
|
||||
expect(colorStyle('#ffffff').color).toBe('#1a1a1a')
|
||||
expect(colorStyle('rebeccapurple').backgroundColor).toBe('rebeccapurple')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user