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
8 changes: 6 additions & 2 deletions apps/web/src/lab/bench/chart-bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { ChartRuntime, isChartRuntimeReady, LoadChartRuntime } from "@maple/view
import type { Resources } from "../../platform/resources"

// The chart benches' shared state: a canvas chart per id,
// the chart runtime, the resolved theme, and one linked cursor across them.
// the chart runtime, the resolved theme, one linked cursor across them, and the span a drag zoomed to.

export const Model = Schema.Struct({
nowMs: Schema.Number,
timeZone: Schema.String,
charts: Schema.Record(Schema.String, Chart.Model),
/** The span the last drag selected; a page writes it to its URL, a bench narrows its rows. */
zoom: Schema.NullOr(Schema.Struct({ startMs: Schema.Number, endMs: Schema.Number })),
...ChartEnvFields,
})
export type Model = typeof Model.Type
Expand All @@ -23,6 +25,7 @@ export const Message = defineMessageUnion({
GotChartMessage: { chartId: Schema.String, message: Chart.Message },
...ChartEnvMessages,
CompletedMountLinkedCursor: {},
SelectedChartRange: { startMs: Schema.Number, endMs: Schema.Number },
})
export type Message = typeof Message.Type

Expand All @@ -37,7 +40,7 @@ export const benchFlags: Effect.Effect<BenchFlags> = Effect.all({
type Return = Update.Return<Model, Message, Resources>

export const init = (flags: BenchFlags): Return => ({
model: { ...flags, charts: {}, chartRuntime: ChartRuntime.Loading(), themeColors: null },
model: { ...flags, charts: {}, zoom: null, chartRuntime: ChartRuntime.Loading(), themeColors: null },
commands: [LoadChartRuntime()],
})

Expand All @@ -58,6 +61,7 @@ export const update = (model: Model, message: Message): Return =>
model: { ...model, chartRuntime: ChartRuntime.Failed({ reason }) },
}),
CompletedMountLinkedCursor: () => ({ model }),
SelectedChartRange: (zoom) => ({ model: { ...model, zoom } }),
})

export const isReady = (model: Model): boolean =>
Expand Down
17 changes: 13 additions & 4 deletions apps/web/src/lab/bench/service-detail-bench.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { parseBucketMs } from "@maple/ui/lib/format"
import { Array as Arr } from "effect"
import type { Html, HtmlBuilder } from "foldkit/html"
import type { FixedMetricRow } from "@maple/views/charts/chart-spec"
import { MountLinkedCursor } from "@maple/views/charts/linked-cursor"
import { MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import { ResolveThemeColors } from "@maple/views/charts/theme-colors"
import { isReady, Message, type Model } from "./chart-bench"
import { CHART_TOKENS, metricsGridOf } from "../../page/kit/service-metrics/grid"

// `/lab/bench/service-detail` and
// `/lab/bench/overview`): the service-detail chart grid over synthetic rows.
// `/lab/bench/overview`): the service-detail chart grid over synthetic rows. A drag zooms the rows.

const BUCKET_COUNT = 145
const BUCKET_MS = 5 * 60 * 1000
Expand All @@ -31,6 +32,14 @@ export const SERVICE_DETAIL_POINTS: ReadonlyArray<FixedMetricRow> = Arr.makeBy(B
}
})

const rowsIn = (zoom: Model["zoom"]): ReadonlyArray<FixedMetricRow> =>
zoom === null
? SERVICE_DETAIL_POINTS
: Arr.filter(SERVICE_DETAIL_POINTS, (row) => {
const at = parseBucketMs(row.bucket)
return at !== null && at >= zoom.startMs && at <= zoom.endMs
})

/** The bench page: `MetricsGrid` with `yAxisWidth={72}` under a padded full-height frame. */
export const view = (model: Model, h: HtmlBuilder<Message>): Html =>
h.div(
Expand All @@ -44,10 +53,10 @@ export const view = (model: Model, h: HtmlBuilder<Message>): Html =>
h,
{
charts: model.charts,
rows: SERVICE_DETAIL_POINTS,
rows: rowsIn(model.zoom),
markers: [],
toChartMessage: (chartId, message) => Message.GotChartMessage({ chartId, message }),
linkedCursorMount: MountLinkedCursor(),
linkedCursorMount: MountLinkedZoom({ timeZone: model.timeZone }),
},
{
isLoading: !isReady(model),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { describe, expect, test } from "vitest"
import { PageOutMessage, type PageContext } from "../../../contract"
import { ResolveThemeColors, ThemeMessage } from "@maple/views/charts/theme-colors"
import * as Frame from "../../../kit/time-frame"
import { LinkedCursorMessage, MountLinkedCursor } from "@maple/views/charts/linked-cursor"
import { LinkedCursorMessage, MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import { Message } from "./message"
import type { HostSummary, Model } from "./model"
import { init, update } from "./update"
Expand Down Expand Up @@ -38,7 +38,7 @@ const summary: NonNullable<HostSummary> = {
/** The page root reads theme colours and the metrics panel wires the linked cursor. */
const mounted = () => [
Mount.resolve(ResolveThemeColors, ThemeMessage.ResolvedThemeColors({ colors: {} })),
Mount.resolve(MountLinkedCursor, LinkedCursorMessage.CompletedMountLinkedCursor()),
Mount.resolve(MountLinkedZoom, LinkedCursorMessage.CompletedMountLinkedCursor()),
]

const resolved = (model: Model, at = window) =>
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/page/infra/hosts/detail/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const Message = defineMessageUnion({
ClickedWidenRange: {},
...ChartEnvMessages,
CompletedMountLinkedCursor: {},
SelectedChartRange: { startMs: Schema.Number, endMs: Schema.Number },
GotChartMessage: { metric: HostMetric, message: Chart.Message },
GotCopyMessage: { label: Schema.String, message: CopyButton.Message },
GotFrameMessage: { message: Frame.Message },
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/page/infra/hosts/detail/update.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { zoomSearchPatch } from "../../../kit/chart-zoom"
import { chartEnvHandlers } from "@maple/views/charts/chart-env"
import { bucketSecondsForRange } from "../../kit/constants"
import * as Chart from "@maple/views/charts/chart"
Expand Down Expand Up @@ -136,6 +137,7 @@ export const update = (model: Model, message: Message, context: PageContext): Re
}),
...chartEnvHandlers(model),
CompletedMountLinkedCursor: () => ({ model }),
SelectedChartRange: (range) => navigate(model, { ...model.search, ...zoomSearchPatch(range) }),
GotChartMessage: ({ metric, message: inner }) => {
const chart = model.charts[metric] ?? Chart.init()
return {
Expand Down
9 changes: 7 additions & 2 deletions apps/web/src/page/infra/hosts/detail/view-panels.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cn } from "@maple/ui/lib/utils"
import { countLabel } from "@maple/ui/lib/format"
import { relativeTime } from "../../../kit/relative-time"
import { AsyncData } from "foldkit"
Expand All @@ -6,7 +7,7 @@ import * as Chart from "@maple/views/charts/chart"
import { panel, panelHeader } from "@maple/views/ui/panel"
import { middleTruncate } from "@maple/views/ui/middle-truncate"
import { formatValueWithUnit } from "@maple/views/lib/infra-chart-units"
import { LINKED_CURSOR_STYLE, MountLinkedCursor } from "@maple/views/charts/linked-cursor"
import { LINKED_CURSOR_STYLE, LINKED_ZOOM_CLASS, MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import {
INFRA_METRIC_CHART_HEIGHT,
type InfraSeriesInfo,
Expand Down Expand Up @@ -153,7 +154,11 @@ export const metricsPanel = (h: H, model: Model, timeZone: string): Html =>
]),
}),
h.div(
[h.Class("px-4"), h.Style(LINKED_CURSOR_STYLE), h.OnMount(MountLinkedCursor())],
[
h.Class(cn("px-4", LINKED_ZOOM_CLASS)),
h.Style(LINKED_CURSOR_STYLE),
h.OnMount(MountLinkedZoom({ timeZone })),
],
Arr.map(METRIC_STRIPS, (strip) => metricStrip(h, model, strip, timeZone)),
),
])
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/page/kit/chart-zoom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, test } from "vitest"
import { zoomSearchPatch } from "./chart-zoom"

describe("zoomSearchPatch", () => {
test("a dragged span becomes whole-second bounds that contain it, and drops the preset", () => {
expect(
zoomSearchPatch({
startMs: Date.parse("2026-10-01T11:00:00.400Z"),
endMs: Date.parse("2026-10-01T11:30:00.200Z"),
}),
).toEqual({
startTime: "2026-10-01 11:00:00",
endTime: "2026-10-01 11:30:01",
timePreset: undefined,
})
})

test("a sliver of a drag still opens a minute", () => {
const at = Date.parse("2026-10-01T11:00:00Z")
expect(zoomSearchPatch({ startMs: at, endMs: at + 4_000 })).toEqual({
startTime: "2026-10-01 11:00:00",
endTime: "2026-10-01 11:01:00",
timePreset: undefined,
})
})
})
16 changes: 16 additions & 0 deletions apps/web/src/page/kit/chart-zoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { formatWarehouseDateTime } from "@maple/query-engine"
import type { ChartRange } from "@maple/views/charts/linked-cursor"
import type { TimeSearchPatch } from "./time-range-picker"

const MIN_ZOOM_MS = 60_000

/** A span dragged across a chart as the URL's absolute window: whole seconds, at least a minute, no preset. */
export const zoomSearchPatch = (range: ChartRange): TimeSearchPatch => {
const startMs = Math.floor(range.startMs / 1000) * 1000
const endMs = Math.max(Math.ceil(range.endMs / 1000) * 1000, startMs + MIN_ZOOM_MS)
return {
startTime: formatWarehouseDateTime(startMs),
endTime: formatWarehouseDateTime(endMs),
timePreset: undefined,
}
}
16 changes: 13 additions & 3 deletions apps/web/src/page/kit/service-metrics/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
import { resolveColor, type ThemeColors } from "@maple/views/charts/theme-colors"
import { widgetShell } from "../widget-shell"
import { type commitMarkers, markerLayout } from "./markers"
import { LINKED_CURSOR_STYLE, linkedCursorOverlay } from "@maple/views/charts/linked-cursor"
import {
LINKED_CURSOR_STYLE,
LINKED_ZOOM_CLASS,
linkedCursorOverlay,
} from "@maple/views/charts/linked-cursor"
import { markersOverlay } from "./markers-view"
import { chartFrameClass } from "@maple/views/ui/chart-frame"

Expand Down Expand Up @@ -155,7 +159,7 @@ export type GridInput<M> = Readonly<{
rows: ReadonlyArray<FixedMetricRow>
markers: ReturnType<typeof commitMarkers>
toChartMessage: (chartId: string, message: Chart.Message) => M
/** `h.OnMount(MountLinkedCursor())`, lifted to the page's Message. */
/** `h.OnMount(MountLinkedZoom(...))`, lifted to the page's Message. */
linkedCursorMount: Parameters<HtmlBuilder<M>["OnMount"]>[0]
}>

Expand Down Expand Up @@ -205,7 +209,13 @@ export const metricsGridOf = <M>(h: HtmlBuilder<M>, input: GridInput<M>, state:
h.DataAttribute("metrics-grid", ""),
h.OnMount(input.linkedCursorMount),
...(state.isWaiting ? [h.AriaBusy(true)] : []),
h.Class(cn("grid grid-cols-1 md:grid-cols-2 gap-3", refreshingClass(state.isWaiting))),
h.Class(
cn(
"grid grid-cols-1 md:grid-cols-2 gap-3",
refreshingClass(state.isWaiting),
LINKED_ZOOM_CLASS,
),
),
h.Style(LINKED_CURSOR_STYLE),
],
Arr.map(CHARTS, (chart) => {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/page/overview/analyst/analyst.scene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
withViewInputs,
} from "foldkit/scene"
import { describe, expect, test } from "vitest"
import { LinkedCursorMessage, MountLinkedCursor } from "@maple/views/charts/linked-cursor"
import { LinkedCursorMessage, MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import { ResolveThemeColors, ThemeMessage } from "@maple/views/charts/theme-colors"
import { buildErrorsLabFixture } from "../../../lab/errors-fixture"
import {
Expand Down Expand Up @@ -332,7 +332,7 @@ describe("overview, analyst", () => {
},
given(entered.model),
Mount.resolve(ResolveThemeColors, ThemeMessage.ResolvedThemeColors({ colors: {} })),
Mount.resolve(MountLinkedCursor, LinkedCursorMessage.CompletedMountLinkedCursor()),
Mount.resolve(MountLinkedZoom, LinkedCursorMessage.CompletedMountLinkedCursor()),
expectView(role("heading", { level: 1, name: "Overview" })).toExist(),
expectView(text("Open incidents")).toBeAbsent(),
expectView(text("Top errors")).toBeAbsent(),
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/page/overview/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const result = <A extends Schema.Top>(schema: A) => ({

export const Message = defineMessageUnion({
CompletedMountLinkedCursor: {},
SelectedChartRange: { startMs: Schema.Number, endMs: Schema.Number },
CompletedReadEntry: { nowMs: Schema.Number, hint: FacetsHint },
CompletedResolveClock: { nowMs: Schema.Number },
CompletedFetchFacets: result(OverviewFacets),
Expand Down
26 changes: 22 additions & 4 deletions apps/web/src/page/overview/overview.scene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
withViewInputs,
} from "foldkit/scene"
import { describe, expect, test } from "vitest"
import { LinkedCursorMessage, MountLinkedCursor } from "@maple/views/charts/linked-cursor"
import { LinkedCursorMessage, MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import { ResolveThemeColors, ThemeMessage } from "@maple/views/charts/theme-colors"
import { toLoadFailure } from "@maple/views/data/load-failure"
import { PageOutMessage, type PageContext } from "../contract"
Expand Down Expand Up @@ -90,7 +90,7 @@ const withPresence = (model: Model, traces: Presence, logs: Presence = traces):

const themeResolved = () =>
Mount.resolve(ResolveThemeColors, ThemeMessage.ResolvedThemeColors({ colors: {} }))
const cursorMounted = () => Mount.resolve(MountLinkedCursor, LinkedCursorMessage.CompletedMountLinkedCursor())
const cursorMounted = () => Mount.resolve(MountLinkedZoom, LinkedCursorMessage.CompletedMountLinkedCursor())

describe("overview", () => {
test("entry reads the clock and hint; downstream queries wait for facets", () => {
Expand Down Expand Up @@ -253,7 +253,7 @@ describe("overview", () => {
given(failed),
Mount.resolveAll(
[ResolveThemeColors, ThemeMessage.ResolvedThemeColors({ colors: {} })],
[MountLinkedCursor, LinkedCursorMessage.CompletedMountLinkedCursor()],
[MountLinkedZoom, LinkedCursorMessage.CompletedMountLinkedCursor()],
),
expectView(text("Firing now:")).toBeAbsent(),
)
Expand All @@ -271,7 +271,7 @@ describe("overview", () => {
]
const mounts = Mount.resolveAll(
[ResolveThemeColors, ThemeMessage.ResolvedThemeColors({ colors: {} })],
[MountLinkedCursor, LinkedCursorMessage.CompletedMountLinkedCursor()],
[MountLinkedZoom, LinkedCursorMessage.CompletedMountLinkedCursor()],
)
scene(
{ update: pageUpdate, view },
Expand Down Expand Up @@ -504,3 +504,21 @@ describe("overview", () => {
expect(settled.model.isReloading).toBe(false)
})
})

describe("chart zoom", () => {
test("a drag across an Overview chart opens that span as the window", () => {
const zoomed = pageUpdate(
loaded([snapshotRow("checkout")]),
Message.SelectedChartRange({
startMs: Date.parse("2026-10-01T09:00:00Z"),
endMs: Date.parse("2026-10-01T09:45:00Z"),
}),
)
expect(zoomed.outMessage).toEqual(
PageOutMessage.RequestedNavigation({
url: "/?startTime=2026-10-01+09%3A00%3A00&endTime=2026-10-01+09%3A45%3A00",
isReplace: false,
}),
)
})
})
4 changes: 2 additions & 2 deletions apps/web/src/page/overview/setup-checklist.scene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { OrgId } from "@maple/domain"
import { Result, Schema, Array as Arr } from "effect"
import { expect as expectView, given, Mount, role, scene, text, withViewInputs } from "foldkit/scene"
import { describe, expect, test } from "vitest"
import { LinkedCursorMessage, MountLinkedCursor } from "@maple/views/charts/linked-cursor"
import { LinkedCursorMessage, MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import { ResolveThemeColors, ThemeMessage } from "@maple/views/charts/theme-colors"
import { toLoadFailure } from "@maple/views/data/load-failure"
import { failureSentence } from "@maple/views/ui/error-state"
Expand Down Expand Up @@ -77,7 +77,7 @@ const realServices = (model: Model) => (model.setup === null ? [] : realServices
const RECT = { left: 0, right: 225, top: 4, bottom: 5, width: 112, height: 32 }
const pageMounts = [
Mount.resolve(ResolveThemeColors, ThemeMessage.ResolvedThemeColors({ colors: {} })),
Mount.resolve(MountLinkedCursor, LinkedCursorMessage.CompletedMountLinkedCursor()),
Mount.resolve(MountLinkedZoom, LinkedCursorMessage.CompletedMountLinkedCursor()),
] as const
/** The guided setup's two underline tab lists (snippets and package managers). */
const tabMounts = [
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/page/overview/update.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { zoomSearchPatch } from "../kit/chart-zoom"
import { pickerStep } from "../kit/page-time"
import { keyed as keyedStep, runSteps, type Step as KitStep } from "../kit/keyed-step"
import { chartEnvHandlers } from "@maple/views/charts/chart-env"
Expand Down Expand Up @@ -371,9 +372,7 @@ export const init = initWith(isClerkAuthEnabled)
/** Back after leaving: the last data stays on screen while entry re-reads the clock (snapped, so cached reads serve). */
export const revive = (model: Model, route: { readonly search: Search }, context: PageContext): Return => {
if (context.audience === "analyst")
return withAnalyst({ ...model, search: route.search }, (analyst) =>
Analyst.revive(analyst, context),
)
return withAnalyst({ ...model, search: route.search }, (analyst) => Analyst.revive(analyst, context))
// The open-anomalies key never changes, so entry alone would keep the parked counts.
const anomalies = Keyed.request(
Keyed.forgetFailure(model.anomalies),
Expand Down Expand Up @@ -513,6 +512,7 @@ const withChart = (model: Model, chartId: string, message: Chart.Message): Retur
export const update = (model: Model, message: Message, context: PageContext): Return =>
Message.match<Return>(message, {
CompletedMountLinkedCursor: () => ({ model }),
SelectedChartRange: (range) => navigate(model, { ...model.search, ...zoomSearchPatch(range) }),
CompletedReadEntry: ({ nowMs, hint }) =>
resolveSetupWindow(
sync({ ...model, nowMs, hint, facetsWindow: facetsWindowAt(nowMs) }, context, "Load"),
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/page/overview/view-metrics.scene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { describe, expect, test } from "vitest"
import * as Chart from "@maple/views/charts/chart"
import { MountChart, PortalTooltipBody } from "@maple/views/charts/chart-mounts"
import { CHART_SPEC_ATTRIBUTE, decodeChartSpec } from "@maple/views/charts/chart-spec"
import { LinkedCursorMessage, MountLinkedCursor } from "@maple/views/charts/linked-cursor"
import { LinkedCursorMessage, MountLinkedZoom } from "@maple/views/charts/linked-cursor"
import type { PageContext } from "../contract"
import * as TimeRangePicker from "../kit/time-range-picker"
import { Message } from "./message"
Expand Down Expand Up @@ -74,7 +74,7 @@ const view = (model: Model, h: HtmlBuilder<Message>): Html =>
const pageUpdate = (model: Model, message: Message) => ({ model: update(model, message, context).model })
const mounted = () =>
Mount.resolveAll(
[MountLinkedCursor, LinkedCursorMessage.CompletedMountLinkedCursor()],
[MountLinkedZoom, LinkedCursorMessage.CompletedMountLinkedCursor()],
[MountChart, Chart.Message.CompletedMountChart()],
[MountChart, Chart.Message.CompletedMountChart()],
[MountChart, Chart.Message.CompletedMountChart()],
Expand Down
Loading
Loading