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
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|analyst`: an analyst 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|analyst` reads the hub and the incident as that viewer: an analyst 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
39 changes: 32 additions & 7 deletions apps/web/src/lab/entries/error-issue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OrgId } from "@maple/domain"
import * as Policy from "@maple/domain/policy"
import {
ErrorIssueEventsResponse,
ErrorIssueId,
Expand All @@ -7,7 +8,7 @@ import {
IssueEscalationAttemptsResponse,
} from "@maple/domain/http"
import { encodePublicId, V2ErrorIssueDetail } from "@maple/domain/http/v2"
import { Effect, Result, Schema, Array as Arr, String as Str } from "effect"
import { Effect, Option, Result, Schema, Array as Arr, String as Str } from "effect"
import { Command } from "foldkit"
import type { Html, HtmlBuilder } from "foldkit/html"
import { toLoadFailure } from "@maple/views/data/load-failure"
Expand All @@ -20,7 +21,9 @@ import { view as pageView } from "../../page/errors/issue/view"
import { defineShellLab } from "../shell-lab"

// `/lab/error-issue`: the error issue page over one fixture issue, a surging high-severity error
// with a regression. API Commands are dropped; the page is sent the answers its reads would get.
// with a regression, as a member or an analyst, who reads it and gets no triage control
// (`?role=member|analyst&tab=overview|occurrences|activity`). API Commands are dropped; the page
// is sent the answers its reads would get.

const context: PageContext = {
orgId: Schema.decodeSync(OrgId)("org_lab"),
Expand Down Expand Up @@ -118,12 +121,28 @@ const send = (step: Step, message: Message): Step => {
}
}

const Flags = Schema.Struct({})
const ROLES = ["member", "analyst"] as const
const TABS = ["overview", "occurrences", "activity"] as const
const Flags = Schema.Struct({ role: Schema.Literals(ROLES), tab: Schema.Literals(TABS) })
type Flags = typeof Flags.Type
type LabRole = Flags["role"]
type LabTab = Flags["tab"]

const init = () => {
const entered = pageInit({ issueId, search: {} }, context)
const decodeRole = Schema.decodeUnknownOption(Schema.Literals(ROLES))
const decodeTab = Schema.decodeUnknownOption(Schema.Literals(TABS))

const init = (flags: Flags) => {
const entered = pageInit({ issueId, search: flags.tab === "overview" ? {} : { tab: flags.tab } }, context)
const start: Step = { model: entered.model, commands: Arr.filter(entered.commands ?? [], isLocalCommand) }
const windowed = send(start, Message.CompletedResolveWindow({ window, nowMs: NOW, mode: "Load" }))
// The session's grants for the role, as the page's own read would answer.
const granted = send(
start,
Message.CompletedFetchSession({
orgId: context.orgId,
grants: Policy.grantsOf([`org:${flags.role}`]),
}),
)
const windowed = send(granted, Message.CompletedResolveWindow({ window, nowMs: NOW, mode: "Load" }))
const keyOf = (field: "detail" | "events" | "pullRequests" | "verifications" | "escalations") =>
windowed.model[field].key ?? ""
return Arr.reduce(
Expand Down Expand Up @@ -183,7 +202,13 @@ export const lab = defineShellLab("Error issue", {
Model,
Message,
Flags,
flags: Effect.succeed({}),
flags: Effect.sync((): Flags => {
const params = new URLSearchParams(globalThis.location.search)
return {
role: Option.getOrElse(decodeRole(params.get("role")), (): LabRole => "member"),
tab: Option.getOrElse(decodeTab(params.get("tab")), (): LabTab => "overview"),
}
}),
init,
update,
view,
Expand Down
Loading
Loading