Skip to content
Merged
4 changes: 3 additions & 1 deletion apps/web/src/page/alerts/hub/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const overviewOf = (model: Model): Option.Option<AlertsOverviewReady> =>
export const destinationsOf = (model: Model): ReadonlyArray<AlertDestinationDocument> =>
(model.destinations ?? []).map(rowToAlertDestinationDocument)

export type AlertsTab = "inbox" | "rules" | "uptime" | "routing"
export type AlertsTab = "inbox" | "rules" | "uptime" | "status-pages" | "routing"

/** Legacy `tab` values map onto the current tabs so old links keep resolving. */
export const resolveTab = (tab: string | undefined): AlertsTab => {
Expand All @@ -43,6 +43,8 @@ export const resolveTab = (tab: string | undefined): AlertsTab => {
return "rules"
case "uptime":
return "uptime"
case "status-pages":
return "status-pages"
case "routing":
case "settings":
return "routing"
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/page/alerts/hub/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LoadFailure } from "../../../data/load-failure"
import * as Dialog from "../../../ui/dialog"
import * as Menu from "../../../ui/menu"
import * as DestinationDialog from "../destination/dialog"
import * as StatusPages from "../status-pages/list"
import { TestOutcome, UptimeKindFilter, UptimeStatusFilter } from "./model"
import { ServiceNamespaces } from "./uptime-groups"

Expand Down Expand Up @@ -68,5 +69,6 @@ export const Message = defineMessageUnion({
ClickedConfirmDeleteMonitor: {},
CompletedDeleteMonitor: { monitorId: UptimeMonitorId, result: outcome(Schema.Void) },
GotDeleteMonitorDialogMessage: { message: Dialog.Message },
GotStatusPagesMessage: { message: StatusPages.Message },
})
export type Message = typeof Message.Type
3 changes: 3 additions & 0 deletions apps/web/src/page/alerts/hub/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AlertsSearch } from "../../../route/search"
import * as Dialog from "../../../ui/dialog"
import * as Menu from "../../../ui/menu"
import * as DestinationDialog from "../destination/dialog"
import * as StatusPages from "../status-pages/list"
import { ServiceNamespaces } from "./uptime-groups"

// The `/alerts` hub.
Expand Down Expand Up @@ -73,5 +74,7 @@ export const Model = Schema.Struct({
deletingMonitorId: Schema.NullOr(UptimeMonitorId),
confirmDeleteMonitorId: Schema.NullOr(UptimeMonitorId),
deleteMonitorDialog: Dialog.Model,
/** Status pages tab: its pages, read only while the tab is open, and the create dialog. */
statusPages: StatusPages.Model,
})
export type Model = typeof Model.Type
29 changes: 27 additions & 2 deletions apps/web/src/page/alerts/hub/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { AppRoute } from "../../../route"
import * as Dialog from "../../../ui/dialog"
import * as Menu from "../../../ui/menu"
import * as DestinationDialog from "../destination/dialog"
import * as StatusPages from "../status-pages/list"
import { type PageContext, PageOutMessage, type PageReturn } from "../../contract"
import { alertsHref, createHref, ruleHref } from "../shared/links"
import { editMonitorHref, newMonitorHref, UPTIME_LIST_LIMIT } from "../shared/uptime-commands"
Expand Down Expand Up @@ -114,6 +115,21 @@ const requestMonitors = (model: Model, context: PageContext, mode: Keyed.ReadMod
})
}

/** The Status pages tab reads its pages only while it is open. */
const requestStatusPages = (model: Model, context: PageContext, mode: Keyed.ReadMode): Requested => {
if (resolveTab(model.search.tab) !== "status-pages") return { model, commands: [] }
return liftStatusPages(model, StatusPages.request(model.statusPages, context, mode))
}

/** The tab's step as the hub's: its Commands wrapped, its navigation and toasts passed up. */
const liftStatusPages = (model: Model, step: StatusPages.Return) => ({
model: { ...model, statusPages: step.model },
commands: FoldkitCommand.mapMessages(step.commands ?? [], (message) =>
Message.GotStatusPagesMessage({ message }),
),
outMessage: step.outMessage,
})

/** `reactivityKeys: ["alertDeliveryEvents"]`: both reads revalidate. */
const refreshDeliveries = (model: Model, context: PageContext) => {
const overview = requestDeliveries(model, context, "Refresh")
Expand Down Expand Up @@ -154,26 +170,33 @@ export const init = (route: HubRoute, context: PageContext): Return => {
deletingMonitorId: null,
confirmDeleteMonitorId: null,
deleteMonitorDialog: Dialog.init(DELETE_MONITOR_DIALOG_ID, "alert-dialog"),
statusPages: StatusPages.init(),
}
const deliveries = requestDeliveries(model, context, "Load")
const routing = requestRoutingDeliveries(deliveries.model, context, "Load")
const monitors = requestMonitors(routing.model, context, "Load")
const statusPages = requestStatusPages(monitors.model, context, "Load")
return {
model: monitors.model,
model: statusPages.model,
commands: [
ReadHubClock(),
FetchHubSession({ orgId: context.orgId }),
...deliveries.commands,
...routing.commands,
...monitors.commands,
...statusPages.commands,
],
}
}

export const routeChanged = (model: Model, route: HubRoute, context: PageContext): Return => {
const routing = requestRoutingDeliveries({ ...model, search: route.search }, context, "Load")
const monitors = requestMonitors(routing.model, context, "Load")
return { model: monitors.model, commands: [...routing.commands, ...monitors.commands] }
const statusPages = requestStatusPages(monitors.model, context, "Load")
return {
model: statusPages.model,
commands: [...routing.commands, ...monitors.commands, ...statusPages.commands],
}
}

const navigate = (search: Model["search"]): PageOutMessage =>
Expand Down Expand Up @@ -537,6 +560,8 @@ export const update = (model: Model, message: Message, context: PageContext): Re
})
},
ClickedConfirmDeleteMonitor: () => confirmDeleteMonitor(model),
GotStatusPagesMessage: ({ message: child }) =>
liftStatusPages(model, StatusPages.update(model.statusPages, child, context)),
GotDeleteMonitorDialogMessage: ({ message: child }) =>
model.deletingMonitorId !== null
? { model }
Expand Down
51 changes: 40 additions & 11 deletions apps/web/src/page/alerts/hub/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { alertsEmptyState } from "./view-empty"
import { inboxTab } from "./view-inbox"
import { routingTab } from "./view-routing"
import * as DestinationDialogView from "../destination/view"
import * as StatusPages from "../status-pages/list"
import { rulesTab } from "./view-rules"
import { deleteMonitorDialog, uptimeTab } from "./view-uptime"

Expand All @@ -32,6 +33,7 @@ const ALERTS_TABS: ReadonlyArray<{ value: AlertsTab; label: string }> = [
{ value: "inbox", label: "Inbox" },
{ value: "rules", label: "Rules" },
{ value: "uptime", label: "Uptime" },
{ value: "status-pages", label: "Status pages" },
{ value: "routing", label: "Routing" },
]

Expand All @@ -51,15 +53,17 @@ const tabBar = (
const count =
tab.value === "uptime"
? Option.getOrUndefined(monitorCount)
: Option.match(overview, {
onNone: () => undefined,
onSome: (data) =>
tab.value === "inbox"
? data.openIncidents.length
: tab.value === "rules"
? data.rules.length
: undefined,
})
: tab.value === "status-pages"
? StatusPages.pageCount(model.statusPages)
: Option.match(overview, {
onNone: () => undefined,
onSome: (data) =>
tab.value === "inbox"
? data.openIncidents.length
: tab.value === "rules"
? data.rules.length
: undefined,
})
return {
key: tab.value,
href: alertsHref({ ...model.search, tab: tab.value }),
Expand Down Expand Up @@ -160,11 +164,23 @@ const newRuleButton = (h: H, model: Model): Html =>
],
)

// Destinations are a Routing concern, so "New destination" only shows on that tab.
/** Opens the Status pages tab's create dialog. */
const newStatusPage = Message.GotStatusPagesMessage({ message: StatusPages.Message.ClickedNew() })

// Destinations are a Routing concern, so "New destination" only shows on that tab; pages likewise.
const headerActions = (h: H, model: Model, tab: AlertsTab): Html =>
h.div(
[h.Class("flex items-center gap-2")],
[
...(model.isAdmin && tab === "status-pages"
? [
button(
h,
{ size: "sm", variant: "outline", attributes: [h.OnClick(newStatusPage)] },
[icon(h, "PlusIcon"), "New status page"],
),
]
: []),
...(model.isAdmin && tab === "routing"
? [
button(
Expand Down Expand Up @@ -273,7 +289,20 @@ export const view = Submodel.defineView<Model, Message, PageViewInputs>((model,
})
: tab === "uptime"
? uptimeTab(h, model, model.nowMs ?? 0, inputs.context.pinnedNamespace)
: hubBody(h, model, overview, tab),
: tab === "status-pages"
? h.submodel({
slotId: "alerts-status-pages",
model: model.statusPages,
view: StatusPages.view,
viewInputs: {
isAdmin: model.isAdmin,
nowMs: model.nowMs ?? 0,
timeZone: inputs.context.timeZone,
},
toParentMessage: (message) =>
Message.GotStatusPagesMessage({ message }),
})
: hubBody(h, model, overview, tab),
],
),
deleteDialog(h, model),
Expand Down
Loading
Loading