Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- A v3 status page has no cards or services. Its structure is declared with `StatusPageV3Component` constructs that point at the page via `statusPage`; nest a `SERVICE` under a `GROUP` via `parent`.
- `StatusPageV3AutomationRule` opens one incident impacting the listed components when a check whose tags overlap with the rule's `tags` fails, and resolves it on recovery. Requires the automated incident management add-on.
- A logical id deployed as a `StatusPage` cannot be redeployed as a `StatusPageV3` (or vice versa); use a new logical id.
- `themeColors` overrides the page's colors per theme (`light` and `dark`) with hex strings; only the colors you set change, the rest keep the defaults. Requires custom theme colors on the plan.
- Type-specific settings are top-level props checked against `type`: both types take `showHistoricalData` (default `true`); only a `GROUP` takes `expandedByDefault` (default `false`). Omit them to keep the defaults.

```ts
Expand All @@ -31,6 +32,10 @@ const statusPage = new StatusPageV3('example-status-page-v3', {
url: 'example-status-page-v3',
customDomain: 'status.example.com',
defaultTheme: 'AUTO',
themeColors: {
light: { linkFontColor: '#005AC2', primaryButtonBackgroundColor: '#151A1E' },
dark: { linkFontColor: '#248AFF' },
},
})

const webApp = new StatusPageV3Component('example-web-app-group', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,37 @@ describe('StatusPageV3 codegen', () => {
expect(ruleSource).toContain('targetImpact: \'MAJOR_OUTAGE\'')
})

it('generates the stored theme colors per theme', async () => {
const sources = await generate(rootDirectory, [
{
type: 'status-page',
logicalId: 'acme',
payload: {
...page,
themeColors: {
light: { linkFontColor: '#005AC2', primaryButtonBackgroundColor: '#151A1E' },
dark: { bodyBackgroundColor: '#14171C' },
},
},
},
])

const source = sources['resources/status-pages/acme-status.check.ts']
expect(source).toMatch(/themeColors: \{\s*light: \{\s*linkFontColor: '#005AC2',/)
expect(source).toMatch(/primaryButtonBackgroundColor: '#151A1E'/)
expect(source).toMatch(/dark: \{\s*bodyBackgroundColor: '#14171C'/)
})

it('leaves theme colors out when the page has none', async () => {
const sources = await generate(rootDirectory, [
{ type: 'status-page', logicalId: 'acme', payload: { ...page, themeColors: null } },
{ type: 'status-page', logicalId: 'acme-empty', payload: { ...page, name: 'Empty', url: 'empty', themeColors: { light: {} } } },
])

expect(sources['resources/status-pages/acme-status.check.ts']).not.toContain('themeColors')
expect(sources['resources/status-pages/empty.check.ts']).not.toContain('themeColors')
})

it('leaves settings that only restate the backend defaults implicit', async () => {
const sources = await generate(rootDirectory, [
{
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/src/constructs/__tests__/status-page-v3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,72 @@ describe('StatusPageV3', () => {
expect(page.synthesize()).not.toHaveProperty('cards')
})

it('synthesizes partial theme colors as given and accepts them', async () => {
const page = new StatusPageV3('acme', {
name: 'ACME',
url: 'acme-status',
themeColors: {
light: { linkFontColor: '#005AC2', primaryButtonBackgroundColor: '#f00' },
dark: { bodyBackgroundColor: '#000' },
},
})

// Only what is set: the backend fills the defaults for the rest.
expect(page.synthesize().themeColors).toEqual({
light: { linkFontColor: '#005AC2', primaryButtonBackgroundColor: '#f00' },
dark: { bodyBackgroundColor: '#000' },
})

const diagnostics = new Diagnostics()
await page.validate(diagnostics)
expect(diagnostics.isFatal()).toBe(false)
})

it('leaves theme colors out when not set', async () => {
const page = new StatusPageV3('acme', { name: 'ACME', url: 'acme-status' })
expect(page.synthesize().themeColors).toBeUndefined()

const diagnostics = new Diagnostics()
await page.validate(diagnostics)
expect(diagnostics.isFatal()).toBe(false)
})

it('rejects theme colors that are not hex colors', async () => {
const page = new StatusPageV3('acme', {
name: 'ACME',
url: 'acme-status',
themeColors: { light: { linkFontColor: 'red' }, dark: { borderColor: '#12345' } },
})

const diagnostics = new Diagnostics()
await page.validate(diagnostics)
expect(diagnostics.isFatal()).toBe(true)
const messages = diagnostics.observations.map(o => o.message)
expect(messages).toEqual(expect.arrayContaining([
expect.stringContaining('themeColors.light.linkFontColor'),
expect.stringContaining('themeColors.dark.borderColor'),
]))
})

it('rejects unknown color properties and malformed groups', async () => {
// Loosely typed on purpose: TypeScript already stops this.
const props: any = {
name: 'ACME',
url: 'acme-status',
themeColors: { light: { linkColor: '#000' }, dark: 'black' },
}
const page = new StatusPageV3('acme', props)

const diagnostics = new Diagnostics()
await page.validate(diagnostics)
expect(diagnostics.isFatal()).toBe(true)
const messages = diagnostics.observations.map(o => o.message)
expect(messages).toEqual(expect.arrayContaining([
expect.stringContaining('themeColors.light.linkColor'),
expect.stringContaining('themeColors.dark'),
]))
})

it('should produce a diagnostic if the same logicalId is used twice', async () => {
const project = newProject()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { StatusPageV3ThemeColorGroup } from '../status-page-v3.js'

// Typed against the public group so a colour added to one cannot be missed
// in the other; the backend palette has the same twelve entries.
const properties: Record<keyof StatusPageV3ThemeColorGroup, true> = {
bodyBackgroundColor: true,
headerBackgroundColor: true,
headerFontColor: true,
titleFontColor: true,
bodyFontColor: true,
bodyFontColorMuted: true,
navigationFontColor: true,
linkFontColor: true,
cardBackgroundColor: true,
borderColor: true,
primaryButtonBackgroundColor: true,
primaryButtonFontColor: true,
}

export const statusPageV3ThemeColorProperties = Object.keys(properties) as (keyof StatusPageV3ThemeColorGroup)[]

export const statusPageV3Themes = ['light', 'dark'] as const

// Same format the backend accepts: #RGB or #RRGGBB, case-insensitive.
export const hexColorPattern = /^#([0-9A-F]{3}|[0-9A-F]{6})$/i
39 changes: 39 additions & 0 deletions packages/cli/src/constructs/status-page-v3-codegen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Codegen, Context } from './internal/codegen/index.js'
import { statusPageV3ThemeColorProperties, statusPageV3Themes } from './internal/status-page-v3-theme-colors.js'
import { decl, expr, GeneratedFile, ident, Value } from '../sourcegen/index.js'
import { StatusPageTheme } from './status-page.js'
import type { StatusPageV3ThemeColors } from './status-page-v3.js'

export interface StatusPageV3Resource {
id: string
Expand All @@ -20,10 +22,34 @@ export interface StatusPageV3Resource {
footerText?: string | null
googleAnalyticsTag?: string | null
allowIndexing?: boolean | null
themeColors?: StatusPageV3ThemeColors | null
}

const construct = 'StatusPageV3'

// The stored palette is complete, so every color is generated as-is: the CLI
// does not know the backend defaults and must not guess which ones to omit.
function themeColorEntries (themeColors: StatusPageV3ThemeColors): Array<[string, Array<[string, string]>]> {
const themes: Array<[string, Array<[string, string]>]> = []
for (const theme of statusPageV3Themes) {
const colors = themeColors[theme]
if (!colors) {
continue
}
const entries: Array<[string, string]> = []
for (const property of statusPageV3ThemeColorProperties) {
const value = colors[property]
if (value) {
entries.push([property, value])
}
}
if (entries.length > 0) {
themes.push([theme, entries])
}
}
return themes
}

export function valueForStatusPageV3FromId (genfile: GeneratedFile, physicalId: string): Value {
genfile.namedImport(construct, 'checkly/constructs')

Expand Down Expand Up @@ -143,6 +169,19 @@ export class StatusPageV3Codegen extends Codegen<StatusPageV3Resource> {
if (resource.allowIndexing === false) {
builder.boolean('allowIndexing', false)
}

const themes = resource.themeColors ? themeColorEntries(resource.themeColors) : []
if (themes.length > 0) {
builder.object('themeColors', builder => {
for (const [theme, colors] of themes) {
builder.object(theme, builder => {
for (const [property, value] of colors) {
builder.string(property, value)
}
})
}
})
}
})
})
}))
Expand Down
Loading