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
16 changes: 11 additions & 5 deletions apps/web/src/lab/alerts-lab/update.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { chartEnvHandlers } from "@maple/views/charts/chart-env"
import * as Policy from "@maple/domain/policy"
import { OrgId } from "@maple/domain"
import { Clock, Effect, Schema, Array as Arr } from "effect"
import { Command } from "foldkit"
Expand All @@ -10,20 +9,27 @@ import * as AlertsHub from "../../page/alerts/hub/update"
import type { PageContext } from "../../page/contract"
import type { Resources } from "../../platform/resources"
import { foldLabShell, initLabShell, type LabShellFlags } from "../app-shell"
import { labGrants, LabRole, readLabRole } from "../viewer-role"
import { Message, type Model } from "./model"

export type Return = Update.Return<Model, Message, Resources>

export const AlertsLabFlags = Schema.Struct({ nowMs: Schema.Number, timeZone: Schema.String })
export const AlertsLabFlags = Schema.Struct({
nowMs: Schema.Number,
timeZone: Schema.String,
/** `?role=member|observer`: an observer gets no acknowledge, rule or routing control. */
role: LabRole,
})
export type AlertsLabFlags = typeof AlertsLabFlags.Type

/** The boot clock and the browser's zone (`Intl.DateTimeFormat().resolvedOptions()`). */
/** The boot clock, the browser's zone (`Intl.DateTimeFormat().resolvedOptions()`) and `?role=`. */
export const alertsLabFlags: Effect.Effect<AlertsLabFlags> = Effect.all({
nowMs: Clock.currentTimeMillis,
timeZone: Effect.sync(() => Intl.DateTimeFormat().resolvedOptions().timeZone),
role: readLabRole,
})

/** The hub views run against a hub Model; its org is the fixture's, its admin rights on. */
/** The hub views run against a hub Model; its org is the fixture's, its admin rights on by default. */
const LAB_CONTEXT: PageContext = {
orgId: Schema.decodeSync(OrgId)("org_lab_alerts"),
pinnedNamespace: null,
Expand All @@ -37,7 +43,7 @@ export const init = (flags: AlertsLabFlags & LabShellFlags): Return => {
model: {
nowMs: flags.nowMs,
timeZone: flags.timeZone,
hub: { ...hub, grants: Policy.allPermissions, nowMs: flags.nowMs },
hub: { ...hub, grants: labGrants(flags.role), nowMs: flags.nowMs },
status: null,
chart: Chart.init(),
chartRuntime: { _tag: "Loading" },
Expand Down
25 changes: 14 additions & 11 deletions apps/web/src/lab/alerts-lab/view-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Message, type Model } from "./model"
import { Message as HubMessage } from "../../page/alerts/hub/message"
import { FRAME_CLASS, hubSections, section } from "./view"
import { toEpochMs } from "@maple/ui/lib/time-format"
import { canRespondToIncidents } from "../../page/kit/session-grants"

type H = HtmlBuilder<Message>

Expand Down Expand Up @@ -79,17 +80,19 @@ const incidentFrame = (h: H, model: Model, fixture: AlertsLabFixture, isAcknowle
headerTitle(h, incident, rule, model.nowMs, model.timeZone),
hstack(h, { gap: "2", wrap: true }, [
// The lab hub drops this message, as it does the inbox buttons': the lab has no API.
...acknowledgeAction(h, {
incident,
isLoading: false,
onAcknowledge: (acknowledged) =>
Message.GotHubMessage({
message: HubMessage.ClickedAcknowledgeIncident({
incidentId: incident.id,
acknowledged,
}),
}),
}),
...(canRespondToIncidents(model.hub)
? acknowledgeAction(h, {
incident,
isLoading: false,
onAcknowledge: (acknowledged) =>
Message.GotHubMessage({
message: HubMessage.ClickedAcknowledgeIncident({
incidentId: incident.id,
acknowledged,
}),
}),
})
: []),
linkButton(
h,
{
Expand Down
17 changes: 11 additions & 6 deletions apps/web/src/lab/alerts-lab/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { button } from "@maple/views/ui/button"
import { icon } from "@maple/views/ui/icon"
import type { Model } from "./model"
import { Array as Arr } from "effect"
import { canManageAlerts, canRespondToIncidents } from "../../page/kit/session-grants"

type AlertsTab = "inbox" | "rules" | "routing"

Expand Down Expand Up @@ -110,11 +111,15 @@ type HubInputs = Readonly<{
nowMs: number
}>

const inboxAcknowledge = (hub: HubModel) => ({
onAcknowledge: (incidentId: AlertIncidentId, acknowledged: boolean) =>
HubMessageUnion.ClickedAcknowledgeIncident({ incidentId, acknowledged }),
pendingIds: hub.acknowledgingIncidentIds,
})
/** As the hub passes it: `null` for a viewer who may not respond, whose rows carry no button. */
const inboxAcknowledge = (hub: HubModel) =>
canRespondToIncidents(hub)
? {
onAcknowledge: (incidentId: AlertIncidentId, acknowledged: boolean) =>
HubMessageUnion.ClickedAcknowledgeIncident({ incidentId, acknowledged }),
pendingIds: hub.acknowledgingIncidentIds,
}
: null

/** The sections the hub's views draw, run against the lab's hub Model. */
export const hubSections = Submodel.defineView<HubModel, HubMessage, HubInputs>((hub, inputs, h) => {
Expand Down Expand Up @@ -144,7 +149,7 @@ export const hubSections = Submodel.defineView<HubModel, HubMessage, HubInputs>(
destinations: fixture.destinations,
status: inputs.status ?? undefined,
emptyState: alertsEmptyState(h, {
canEdit: true,
canEdit: canManageAlerts(hub),
hasDestinations: true,
serviceName: undefined,
onAddDestination: undefined,
Expand Down
19 changes: 11 additions & 8 deletions apps/web/src/lab/anomaly-alerts-lab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ import {
} from "../page/kit/alert-test-rows"
import type { PageContext } from "../page/contract"
import type { Resources } from "../platform/resources"
import { labGrants, LabRole, readLabRole } from "./viewer-role"

// `/lab/anomaly-alerts`: anomalies paging through alert rules, over fixtures. `?state=` picks one:
// editor-new, editor-blank, editor-scoped, editor-default, editor-critical-destination, incident-warning, incident-critical, incident-spike,
// hub-anomalies, hub-anomalies-resolved, hub-anomalies-empty, hub-rules, rule-anomaly, overview-banner.
// `?role=member|observer` reads the hub and the incident as that viewer: an observer gets no acknowledge.

export const LabState = Schema.Literals([
"editor-new",
Expand All @@ -74,14 +76,15 @@ export const LabState = Schema.Literals([
])
export type LabState = typeof LabState.Type

export const Flags = Schema.Struct({ state: LabState })
export const Flags = Schema.Struct({ state: LabState, role: LabRole })
export type Flags = typeof Flags.Type

export const flags: Effect.Effect<Flags> = Effect.sync(() => ({
export const flags: Effect.Effect<Flags> = Effect.map(readLabRole, (role) => ({
state: Option.getOrElse(
Schema.decodeUnknownOption(LabState)(new URLSearchParams(window.location.search).get("state")),
(): LabState => "hub-anomalies",
),
role,
}))

export const Model = Schema.Struct({
Expand Down Expand Up @@ -157,7 +160,7 @@ const RESOLVED = [
}),
]

const hubFor = (state: LabState) => {
const hubFor = (state: LabState, role: LabRole) => {
const search: HubAt =
state === "hub-rules"
? { tab: "rules" }
Expand All @@ -183,7 +186,7 @@ const hubFor = (state: LabState) => {
}),
HubMessage.Message.UpdatedIncidents({ orgId: ORG, rows: incidents }),
HubMessage.Message.UpdatedDestinations({ orgId: ORG, rows: [destinationRow()] }),
HubMessage.Message.CompletedFetchSession({ orgId: ORG, grants: Policy.allPermissions }),
HubMessage.Message.CompletedFetchSession({ orgId: ORG, grants: labGrants(role) }),
],
(model, message) => HubUpdate.update(model, message, CONTEXT),
)
Expand Down Expand Up @@ -256,7 +259,7 @@ const INCIDENTS = {
"incident-spike": { n: 3, signal: "error_spike", severity: "critical", service: "payments-worker" },
} as const

const incidentFor = (state: LabState) => {
const incidentFor = (state: LabState, role: LabRole) => {
const scenario =
state === "incident-warning" || state === "incident-critical" || state === "incident-spike"
? INCIDENTS[state]
Expand Down Expand Up @@ -302,7 +305,7 @@ const incidentFor = (state: LabState) => {
key: synced.activity.key ?? "",
result: Result.succeed([]),
}),
IncidentMessage.Message.CompletedFetchSession({ grants: Policy.allPermissions }),
IncidentMessage.Message.CompletedFetchSession({ grants: labGrants(role) }),
IncidentMessage.Message.CompletedFetchSilences({
key: synced.silences.key ?? "",
result: Result.succeed([]),
Expand Down Expand Up @@ -355,9 +358,9 @@ const isIncident = Str.startsWith("incident")
export const init = (flags: Flags): Return => ({
model: {
state: flags.state,
hub: hubFor(flags.state),
hub: hubFor(flags.state, flags.role),
editor: editorFor(flags.state),
incident: incidentFor(flags.state),
incident: incidentFor(flags.state, flags.role),
rule: ruleFor(),
},
// The incident page's chart runtime still loads, so its anomaly chart draws.
Expand Down
Loading
Loading