Skip to content
Open
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.
- 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
import { StatusPageV3, StatusPageV3AutomationRule, StatusPageV3Component } from 'checkly/constructs'
Expand All @@ -37,6 +38,7 @@ const webApp = new StatusPageV3Component('example-web-app-group', {
type: 'GROUP',
name: 'Web application',
displayOrder: 1,
expandedByDefault: true,
})

const signUp = new StatusPageV3Component('example-sign-up-service', {
Expand All @@ -46,6 +48,7 @@ const signUp = new StatusPageV3Component('example-sign-up-service', {
name: 'Sign up',
description: 'The sign up flow',
displayOrder: 1,
showHistoricalData: false,
})

new StatusPageV3AutomationRule('example-api-down-rule', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const page: StatusPageV3Resource = {
description: 'All systems',
defaultTheme: 'DARK',
allowIndexing: false,
supportLink: 'https://acme.example/support',
}

const group: StatusPageV3ComponentResource = {
Expand All @@ -35,6 +36,7 @@ const group: StatusPageV3ComponentResource = {
type: 'GROUP',
name: 'Platform',
displayOrder: 0,
configuration: { expandedByDefault: true, showHistoricalData: true },
}

const service: StatusPageV3ComponentResource = {
Expand All @@ -46,6 +48,7 @@ const service: StatusPageV3ComponentResource = {
description: 'REST API',
hidden: true,
displayOrder: 1,
configuration: { showHistoricalData: false },
}

const rule: StatusPageV3AutomationRuleResource = {
Expand Down Expand Up @@ -114,6 +117,7 @@ describe('StatusPageV3 codegen', () => {
expect(pageSource).toContain('description: \'All systems\'')
expect(pageSource).toContain('defaultTheme: \'DARK\'')
expect(pageSource).toContain('allowIndexing: false')
expect(pageSource).toContain('supportLink: \'https://acme.example/support\'')
expect(pageSource).not.toContain('cards')

const groupSource = sources['resources/status-pages/components/platform.check.ts']
Expand All @@ -122,13 +126,18 @@ describe('StatusPageV3 codegen', () => {
expect(groupSource).toContain('statusPage: acmeStatusPage')
expect(groupSource).toContain('type: \'GROUP\'')
expect(groupSource).toContain('displayOrder: 0')
expect(groupSource).toContain('expandedByDefault: true')
expect(groupSource).not.toContain('configuration')
// Only the value that differs from the backend default is generated.
expect(groupSource).not.toContain('showHistoricalData')
expect(groupSource).not.toContain('parent:')

const serviceSource = sources['resources/status-pages/components/public-api.check.ts']
expect(serviceSource).toContain('import { platformComponent } from \'./platform.check\'')
expect(serviceSource).toContain('parent: platformComponent')
expect(serviceSource).toContain('hidden: true')
expect(serviceSource).toContain('description: \'REST API\'')
expect(serviceSource).toContain('showHistoricalData: false')
// SERVICE is the default and is left implicit.
expect(serviceSource).not.toContain('type: \'SERVICE\'')

Expand All @@ -143,6 +152,28 @@ describe('StatusPageV3 codegen', () => {
expect(ruleSource).toContain('targetImpact: \'MAJOR_OUTAGE\'')
})

it('leaves settings that only restate the backend defaults implicit', async () => {
const sources = await generate(rootDirectory, [
{
type: 'status-page-component',
logicalId: 'platform',
payload: { ...group, configuration: { expandedByDefault: false, showHistoricalData: true } },
},
{
type: 'status-page-component',
logicalId: 'public-api',
payload: { ...service, configuration: { showHistoricalData: true } },
},
{ type: 'status-page', logicalId: 'acme', payload: page },
])

for (const file of ['platform', 'public-api']) {
const source = sources[`resources/status-pages/components/${file}.check.ts`]
expect(source).not.toContain('expandedByDefault')
expect(source).not.toContain('showHistoricalData')
}
})

it('falls back to fromId() references for resources outside the plan', async () => {
const sources = await generate(rootDirectory, [
{
Expand Down
58 changes: 58 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 @@ -24,6 +24,7 @@ describe('StatusPageV3', () => {
url: 'acme-status',
defaultTheme: 'DARK',
termsOfServiceLink: 'https://acme.example/terms',
supportLink: 'https://acme.example/support',
allowIndexing: false,
})

Expand All @@ -33,6 +34,7 @@ describe('StatusPageV3', () => {
url: 'acme-status',
defaultTheme: 'DARK',
termsOfServiceLink: 'https://acme.example/terms',
supportLink: 'https://acme.example/support',
allowIndexing: false,
version: 3,
}))
Expand Down Expand Up @@ -91,6 +93,7 @@ describe('StatusPageV3Component', () => {
description: undefined,
hidden: undefined,
displayOrder: 1,
configuration: undefined,
})
expect(service.synthesize()).toEqual(expect.objectContaining({
statusPageId: { ref: 'acme' },
Expand All @@ -106,6 +109,61 @@ describe('StatusPageV3Component', () => {
expect(component.synthesize().statusPageId).toEqual({ ref: page.logicalId })
})

it('synthesizes the type-specific settings as the backend configuration', async () => {
const page = new StatusPageV3('acme', { name: 'ACME', url: 'acme-status' })
const group = new StatusPageV3Component('web-app', {
statusPage: page,
type: 'GROUP',
name: 'Web',
displayOrder: 1,
expandedByDefault: true,
})
const service = new StatusPageV3Component('login', {
statusPage: page,
name: 'Login',
displayOrder: 2,
showHistoricalData: false,
})

// Only what is set: the backend fills the defaults for the rest.
expect(group.synthesize().configuration).toEqual({ expandedByDefault: true })
expect(service.synthesize().configuration).toEqual({ showHistoricalData: false })

for (const component of [group, service]) {
const diagnostics = new Diagnostics()
await component.validate(diagnostics)
expect(diagnostics.isFatal()).toBe(false)
}
})

it('rejects a setting that belongs to the other component type', async () => {
const page = new StatusPageV3('acme', { name: 'ACME', url: 'acme-status' })
// Loosely typed on purpose: the props union already stops this in TypeScript.
const props: any = { statusPage: page, name: 'Login', displayOrder: 1, expandedByDefault: true }
const service = new StatusPageV3Component('login', props)

const diagnostics = new Diagnostics()
await service.validate(diagnostics)
expect(diagnostics.isFatal()).toBe(true)
expect(diagnostics.observations).toEqual(expect.arrayContaining([
expect.objectContaining({ message: expect.stringContaining('SERVICE') }),
]))
expect(service.synthesize().configuration).toBeUndefined()
})

it('rejects non-boolean settings', async () => {
const page = new StatusPageV3('acme', { name: 'ACME', url: 'acme-status' })
const props: any = { statusPage: page, name: 'Login', displayOrder: 1, showHistoricalData: 'yes' }
const service = new StatusPageV3Component('login', props)

const diagnostics = new Diagnostics()
await service.validate(diagnostics)
expect(diagnostics.isFatal()).toBe(true)
expect(diagnostics.observations).toEqual(expect.arrayContaining([
expect.objectContaining({ message: expect.stringContaining('boolean') }),
]))
})

it('rejects a parent that is not a GROUP', async () => {
const page = new StatusPageV3('acme', { name: 'ACME', url: 'acme-status' })
const sibling = new StatusPageV3Component('sibling', { statusPage: page, name: 'Sibling', displayOrder: 1 })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { StatusPageV3ComponentType } from '../status-page-v3-component.js'

/**
* The backend's per-type `configuration` properties and their defaults
* (`@checkly/shapes/status-pages-component` in the monorepo). The construct
* surfaces them as top-level props, derives which ones each type accepts
* from this table, and the import codegen elides values that only restate a
* default, so a new property is one entry here plus the prop on the
* construct.
*/
export const defaultConfigurationByType: Record<StatusPageV3ComponentType, Record<string, boolean>> = {
SERVICE: { showHistoricalData: true },
GROUP: { expandedByDefault: false, showHistoricalData: true },
}
5 changes: 5 additions & 0 deletions packages/cli/src/constructs/status-page-v3-codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface StatusPageV3Resource {
defaultTheme?: StatusPageTheme | null
privacyPolicyLink?: string | null
termsOfServiceLink?: string | null
supportLink?: string | null
footerText?: string | null
googleAnalyticsTag?: string | null
allowIndexing?: boolean | null
Expand Down Expand Up @@ -126,6 +127,10 @@ export class StatusPageV3Codegen extends Codegen<StatusPageV3Resource> {
builder.string('termsOfServiceLink', resource.termsOfServiceLink)
}

if (resource.supportLink) {
builder.string('supportLink', resource.supportLink)
}

if (resource.footerText) {
builder.string('footerText', resource.footerText)
}
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/constructs/status-page-v3-component-codegen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Codegen, Context } from './internal/codegen/index.js'
import { decl, expr, GeneratedFile, ident, Value } from '../sourcegen/index.js'
import { StatusPageV3ComponentType } from './status-page-v3-component.js'
import { defaultConfigurationByType } from './internal/status-page-v3-component-configuration.js'
import { valueForStatusPageV3Ref } from './status-page-v3-codegen.js'

export interface StatusPageV3ComponentResource {
Expand All @@ -12,6 +13,16 @@ export interface StatusPageV3ComponentResource {
description?: string | null
hidden?: boolean | null
displayOrder: number
configuration?: Record<string, boolean> | null
}

// The configuration's properties are top-level props on the construct. The
// backend fills the defaults when one is omitted, so only values that differ
// are worth generating.
function nonDefaultConfiguration (resource: StatusPageV3ComponentResource): Array<[string, boolean]> {
const defaults = defaultConfigurationByType[resource.type] ?? {}
return Object.entries(resource.configuration ?? {})
.filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean' && defaults[entry[0]] !== entry[1])
}

const construct = 'StatusPageV3Component'
Expand Down Expand Up @@ -95,6 +106,10 @@ export class StatusPageV3ComponentCodegen extends Codegen<StatusPageV3ComponentR

builder.number('displayOrder', resource.displayOrder)

for (const [key, value] of nonDefaultConfiguration(resource)) {
builder.boolean(key, value)
}

if (resource.parentId) {
builder.value('parent', valueForStatusPageV3ComponentRef(file, resource.parentId, context))
}
Expand Down
Loading