Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
051b978
Add backend code
JeremyFunk Jun 12, 2026
99f0abc
Refactor VscRepository
JeremyFunk Jun 13, 2026
6413a12
Lots of refactors and bug fixes
JeremyFunk Jun 13, 2026
ec15b4a
More general cleanup
JeremyFunk Jun 13, 2026
0c4d1ae
Cleanups, bugfixes
JeremyFunk Jun 14, 2026
eb2cfce
hadnle page max buget to avoid hitting wall clock limit
JeremyFunk Jun 14, 2026
68d0d73
Add basic frontend features
JeremyFunk Jun 15, 2026
d588df3
Use internal instead of external repo and installation ids
JeremyFunk Jun 16, 2026
fd45c52
Add polling
JeremyFunk Jun 16, 2026
cc89ad3
Add branch syncing
JeremyFunk Jun 16, 2026
f66354a
rename
JeremyFunk Jun 16, 2026
9791f32
Add to frontend
JeremyFunk Jun 18, 2026
d6d6c9a
Implement cron job
JeremyFunk Jun 18, 2026
3507d57
rethrow cron
JeremyFunk Jun 18, 2026
a54f611
Nits, test improvements
JeremyFunk Jun 18, 2026
4a8a0ac
improve integration sync status
JeremyFunk Jun 18, 2026
72b82fb
Add dcos
JeremyFunk Jun 18, 2026
40c4c13
Better tracing and logs
JeremyFunk Jun 18, 2026
769dd5a
Improve missing commit card
JeremyFunk Jun 18, 2026
5ba96ad
Cleanup migration files
JeremyFunk Jun 18, 2026
8985ba2
Add security guard, linting, nits
JeremyFunk Jun 18, 2026
623c0da
Comments
JeremyFunk Jun 18, 2026
4d33aeb
Cleanup
JeremyFunk Jun 18, 2026
2954184
Shorten/remove comments
JeremyFunk Jun 19, 2026
62a309b
Merge remote-tracking branch 'origin/main' into feat/github-integration
JeremyFunk Jun 19, 2026
3c08fa8
Oxc
JeremyFunk Jun 19, 2026
054cc5b
Knip
JeremyFunk Jun 19, 2026
3d14a51
make ordering contract in list commits cldear
JeremyFunk Jun 19, 2026
aecf68d
remove dlq
JeremyFunk Jun 19, 2026
fbe76f5
sec fix
JeremyFunk Jun 19, 2026
7f34635
remove unused import
JeremyFunk Jun 19, 2026
bb4fa4a
chore: drop effect workerd Clock patch from this branch
JeremyFunk Jun 19, 2026
3abf10d
chore: drop flushTelemetry macrotask fix from this branch
JeremyFunk Jun 19, 2026
1d5bd44
chore: drop local-trace-quickfilter HTTP-semconv fix from this branch
JeremyFunk Jun 19, 2026
24cf4cf
test(vcs): expand and tighten VCS sync test coverage
JeremyFunk Jun 19, 2026
8838873
Merge remote-tracking branch 'origin/main' into feat/github-integration
JeremyFunk Jun 20, 2026
c5fc6db
Add envs to .env.example
JeremyFunk Jun 20, 2026
102c063
style: apply oxfmt + oxlint --fix
JeremyFunk Jun 20, 2026
cd99729
refactor(github): dedupe connection-state type, trace repo resolution…
JeremyFunk Jun 20, 2026
9b41d47
Merge remote-tracking branch 'origin/main' into feat/github-integration
JeremyFunk Jun 20, 2026
8f06903
fix(vcs): align Schema.Defect() with effect beta.85 after merge
JeremyFunk Jun 20, 2026
7be13a8
feat(services): show commit hover card on deploy markers
JeremyFunk Jun 20, 2026
4c77e1b
Restrict file imports
JeremyFunk Jun 20, 2026
5c917b0
feat(demo): seed deploy markers + resolvable commit hover cards
JeremyFunk Jun 20, 2026
96ae4a3
Merge remote-tracking branch 'origin/main' into feat/github-integration
JeremyFunk Jun 20, 2026
8b6702d
docs(demo): refresh release-marker comment after the detection fix me…
JeremyFunk Jun 20, 2026
72b0c56
revert(demo): drop the deploy-marker demo seeding
JeremyFunk Jun 20, 2026
edb7c1c
feat(charts): drag-to-zoom, fullscreen expand, timezone-aware time co…
JeremyFunk Jun 20, 2026
5a744aa
Merge main into feat/chart-time-range-ux
JeremyFunk Jun 22, 2026
379b80e
Merge main into feat/chart-time-range-ux
pullfrog[bot] Jun 23, 2026
537c002
feat(charts): keyboard navigation for time-series charts (#106)
JeremyFunk Jun 24, 2026
b836acf
Merge branch 'main' into feat/chart-time-range-ux
Makisuo Jun 24, 2026
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 @@ -10,8 +10,10 @@ import type {
WidgetMode,
} from "@/components/dashboard-builder/types"
import { useDashboardActions } from "@/components/dashboard-builder/dashboard-actions-context"
import { useDashboardTimeRange } from "@/components/dashboard-builder/dashboard-providers"
import { WidgetActionsProvider } from "@/components/dashboard-builder/widgets/widget-actions-context"
import { useWidgetData } from "@/hooks/use-widget-data"
import { zoomRangeToWarehouse } from "@/lib/time-utils"
import { ChartWidget } from "@/components/dashboard-builder/widgets/chart-widget"
import { StatWidget } from "@/components/dashboard-builder/widgets/stat-widget"
import { GaugeWidget } from "@/components/dashboard-builder/widgets/gauge-widget"
Expand Down Expand Up @@ -39,6 +41,7 @@ const visualizationRegistry: Record<
dataState: WidgetDataState
display: WidgetDisplayConfig
mode: WidgetMode
onZoomSelect?: (range: { startBucket: string; endBucket: string }) => void
}>
> = {
chart: ChartWidget,
Expand Down Expand Up @@ -97,16 +100,42 @@ function useInViewportSticky() {
return { ref, visible }
}

const WidgetRenderer = memo(function WidgetRenderer({ widget }: { widget: DashboardWidget }) {
const WidgetRenderer = memo(function WidgetRenderer({
widget,
readOnly,
}: {
widget: DashboardWidget
readOnly: boolean
}) {
const { mode } = useDashboardActions()
const {
actions: { setTimeRange },
} = useDashboardTimeRange()
const { ref, visible } = useInViewportSticky()
const { dataState } = useWidgetData(widget, visible)
const Visualization = visualizationRegistry[widget.visualization] ?? visualizationRegistry.chart

// Drag-to-zoom: a chart hands back the bucket timestamps at each end of the
// dragged window; narrow the dashboard time range to that absolute window.
// Disabled in read-only mode (e.g. previewing a historical version) so a
// drag can't mutate the live dashboard time range.
const handleZoomSelect = useCallback(
(range: { startBucket: string; endBucket: string }) => {
const resolved = zoomRangeToWarehouse(range)
if (resolved) setTimeRange({ type: "absolute", ...resolved })
},
[setTimeRange],
)

return (
<div ref={ref} className="h-full w-full">
<WidgetActionsProvider widget={widget} dataState={dataState}>
<Visualization dataState={dataState} display={widget.display} mode={mode} />
<Visualization
dataState={dataState}
display={widget.display}
mode={mode}
onZoomSelect={readOnly ? undefined : handleZoomSelect}
/>
</WidgetActionsProvider>
</div>
)
Expand Down Expand Up @@ -185,7 +214,7 @@ export function DashboardCanvas({ widgets, readOnly = false }: DashboardCanvasPr
>
{widgets.map((widget) => (
<div key={widget.id}>
<WidgetRenderer widget={widget} />
<WidgetRenderer widget={widget} readOnly={readOnly} />
</div>
))}
</GridLayout>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ReactNode } from "react"

import { cn } from "@maple/ui/utils"
import { ChartLegendSlotContext } from "@maple/ui/components/ui/chart"
import { Dialog, DialogPopup, DialogTitle } from "@maple/ui/components/ui/dialog"
import { useMemo } from "react"

interface ChartExpandModalProps {
open: boolean
onOpenChange: (open: boolean) => void
title: string
/** The chart, rendered larger to fill the modal body. */
children: ReactNode
}

/**
* A near-fullscreen, centered modal that renders a chart at a larger size.
* Reused by dashboard chart widgets and the service/home metrics grid so the
* expand affordance and dialog wiring live in one place.
*
* The body provides its own `ChartLegendSlotContext` (the in-modal chart shows
* its legend inline rather than hoisting it into a widget header), so the
* expanded chart renders independently of the parent widget's legend slot.
*/
export function ChartExpandModal({ open, onOpenChange, title, children }: ChartExpandModalProps) {
// The expanded chart renders its legend inline (legend is promoted to
// "visible"), so the hoist slot is a no-op sink — it just absorbs any items a
// chart tries to hoist without forcing a wasted re-render of the modal.
const legendSlot = useMemo(() => ({ setItems: () => {} }), [])

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogPopup
className={cn(
"h-[90vh] max-h-[90vh] w-[90vw] max-w-[1600px] origin-center flex-col gap-3 p-6",
)}
>
<DialogTitle className="shrink-0 truncate pe-10 text-base font-semibold">{title}</DialogTitle>
<div className="min-h-0 flex-1">
<ChartLegendSlotContext.Provider value={legendSlot}>
{open ? children : null}
</ChartLegendSlotContext.Provider>
</div>
</DialogPopup>
</Dialog>
)
}
50 changes: 32 additions & 18 deletions apps/web/src/components/dashboard-builder/widgets/chart-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { memo, Suspense } from "react"

import { getChartById } from "@maple/ui/components/charts/registry"
import { ChartSkeleton } from "@maple/ui/components/charts/_shared/chart-skeleton"
import { useTimezonePreference } from "@/hooks/use-timezone-preference"
import { WidgetFrame } from "@/components/dashboard-builder/widgets/widget-shell"
import type { WidgetDataState, WidgetDisplayConfig, WidgetMode } from "@/components/dashboard-builder/types"

Expand All @@ -14,6 +15,8 @@ interface ChartWidgetProps {
onConfigure?: () => void
onCreateAlert?: () => void
onFix?: () => void
/** Drag-to-zoom on time-series charts. See `BaseChartProps.onZoomSelect`. */
onZoomSelect?: (range: { startBucket: string; endBucket: string }) => void
}

export const ChartWidget = memo(function ChartWidget({
Expand All @@ -25,7 +28,9 @@ export const ChartWidget = memo(function ChartWidget({
onConfigure,
onCreateAlert,
onFix,
onZoomSelect,
}: ChartWidgetProps) {
const { effectiveTimezone } = useTimezonePreference()
const chartId = display.chartId ?? "gradient-area"
const entry = getChartById(chartId)
if (!entry) return null
Expand All @@ -37,6 +42,29 @@ export const ChartWidget = memo(function ChartWidget({
const seriesStats = display.chartPresentation?.seriesStats ?? legend !== "hidden"
const tooltip = display.chartPresentation?.tooltip

const renderChart = (expanded: boolean) => (
<ChartComponent
data={chartData}
className="h-full w-full aspect-auto"
// In the expanded modal the legend renders inline (there is no widget
// header to hoist it into), so promote a hidden legend to visible there.
legend={expanded && legend === "hidden" ? "visible" : legend}
seriesStats={seriesStats}
tooltip={tooltip}
stacked={display.stacked}
curveType={display.curveType}
unit={display.unit}
logScale={display.yAxis?.logScale}
softMin={display.yAxis?.softMin}
softMax={display.yAxis?.softMax}
fitYAxisToData={display.yAxis?.fitYAxisToData}
showPoints={display.chartPresentation?.showPoints}
thresholds={display.thresholds}
timeZone={effectiveTimezone}
onZoomSelect={onZoomSelect}
/>
)

return (
<WidgetFrame
title={display.title || "Untitled"}
Expand All @@ -48,25 +76,11 @@ export const ChartWidget = memo(function ChartWidget({
onCreateAlert={onCreateAlert}
onFix={onFix}
loadingSkeleton={<ChartSkeleton variant={entry.category} />}
renderExpanded={() => (
<Suspense fallback={<ChartSkeleton variant={entry.category} />}>{renderChart(true)}</Suspense>
)}
>
<Suspense fallback={<ChartSkeleton variant={entry.category} />}>
<ChartComponent
data={chartData}
className="h-full w-full aspect-auto"
legend={legend}
seriesStats={seriesStats}
tooltip={tooltip}
stacked={display.stacked}
curveType={display.curveType}
unit={display.unit}
logScale={display.yAxis?.logScale}
softMin={display.yAxis?.softMin}
softMax={display.yAxis?.softMax}
fitYAxisToData={display.yAxis?.fitYAxisToData}
showPoints={display.chartPresentation?.showPoints}
thresholds={display.thresholds}
/>
</Suspense>
<Suspense fallback={<ChartSkeleton variant={entry.category} />}>{renderChart(false)}</Suspense>
</WidgetFrame>
)
})
Loading
Loading