@@ -11,7 +11,7 @@ import { Form, Link, useNavigation, type MetaFunction } from "@remix-run/react";
1111import { type ActionFunctionArgs , type LoaderFunctionArgs } from "@remix-run/server-runtime" ;
1212import type { QueueItem } from "@trigger.dev/core/v3/schemas" ;
1313import type { RuntimeEnvironmentType } from "@trigger.dev/database" ;
14- import { type ReactNode , useEffect , useState } from "react" ;
14+ import { type ReactNode , useEffect , useMemo , useState } from "react" ;
1515import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
1616import { z } from "zod" ;
1717import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon" ;
@@ -72,6 +72,8 @@ import { TimeFilter, timeFilterFromTo } from "~/components/runs/v3/SharedFilters
7272import { useSearchParams } from "~/hooks/useSearchParam" ;
7373import { parseFiniteInt } from "~/utils/searchParams" ;
7474import { UsageSparkline } from "~/components/primitives/UsageSparkline" ;
75+ import { buildActivityTimeAxis } from "~/components/primitives/charts/activityTimeAxis" ;
76+ import { Chart , type ChartConfig } from "~/components/primitives/charts/ChartCompound" ;
7577import {
7678 useMetricResourceQuery ,
7779 type MetricResourceTimeRange ,
@@ -452,7 +454,28 @@ function QueuesWithMetricsView() {
452454 < div className = "grid max-h-full grid-rows-[auto_1fr] overflow-hidden" >
453455 < div className = "grid grid-cols-2 gap-3 p-3 lg:grid-cols-4" >
454456 { QUEUE_HEADER_TILES . map ( ( tile ) => (
455- < QueueEnvMetricTile key = { tile . id } tile = { tile } timeRange = { timeRange } />
457+ < QueueEnvMetricTile
458+ key = { tile . id }
459+ tile = { tile }
460+ timeRange = { timeRange }
461+ referenceLines = {
462+ tile . id === "saturation"
463+ ? [
464+ { y : 100 , label : `Limit ${ environment . concurrencyLimit } ` } ,
465+ ...( environment . burstFactor > 1
466+ ? [
467+ {
468+ y : Math . round ( environment . burstFactor * 100 ) ,
469+ label : `Burst ${ Math . round (
470+ environment . concurrencyLimit * environment . burstFactor
471+ ) } `,
472+ } ,
473+ ]
474+ : [ ] ) ,
475+ ]
476+ : undefined
477+ }
478+ />
456479 ) ) }
457480 </ div >
458481
@@ -1140,7 +1163,8 @@ type QueueHeaderTile = {
11401163 label : string ;
11411164 color : string ;
11421165 query : string ;
1143- unitLabel : { singular : string ; plural : string } ;
1166+ /** Formats a single bucket's value in the chart tooltip. */
1167+ formatValue ?: ( value : number ) => string ;
11441168 derive : ( rows : MetricTileRow [ ] ) => {
11451169 sparkline : number [ ] ;
11461170 total : number ;
@@ -1167,7 +1191,7 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
11671191 label : "Env saturation" ,
11681192 color : "#6366F1" ,
11691193 query : `SELECT timeBucket() AS t,\n max(max_env_running) AS used,\n max(max_env_limit) AS env_limit\nFROM queue_metrics\nGROUP BY t\nORDER BY t` ,
1170- unitLabel : { singular : "%" , plural : "%" } ,
1194+ formatValue : ( v ) => ` ${ v } %` ,
11711195 derive : ( rows ) => {
11721196 const sparkline = rows . map ( ( r ) => {
11731197 const limit = tileNumber ( r . env_limit ) ;
@@ -1182,7 +1206,6 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
11821206 label : "Backlog" ,
11831207 color : "#A78BFA" ,
11841208 query : `SELECT timeBucket() AS t,\n max(max_env_queued) AS queued\nFROM queue_metrics\nGROUP BY t\nORDER BY t` ,
1185- unitLabel : { singular : "queued" , plural : "queued" } ,
11861209 derive : ( rows ) => {
11871210 const sparkline = rows . map ( ( r ) => tileNumber ( r . queued ) ) ;
11881211 const peak = sparkline . reduce ( ( max , v ) => Math . max ( max , v ) , 0 ) ;
@@ -1194,7 +1217,7 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
11941217 label : "Scheduling delay p95" ,
11951218 color : "#F59E0B" ,
11961219 query : `SELECT timeBucket() AS t,\n round(quantilesMerge(0.5, 0.95, 0.99)(wait_quantiles)[2]) AS p95\nFROM queue_metrics\nGROUP BY t\nORDER BY t` ,
1197- unitLabel : { singular : "ms" , plural : "ms" } ,
1220+ formatValue : formatWaitMs ,
11981221 derive : ( rows ) => {
11991222 const sparkline = rows . map ( ( r ) => tileNumber ( r . p95 ) ) ;
12001223 const worst = sparkline . reduce ( ( max , v ) => Math . max ( max , v ) , 0 ) ;
@@ -1211,7 +1234,6 @@ const QUEUE_HEADER_TILES: QueueHeaderTile[] = [
12111234 label : "Throttled" ,
12121235 color : "#F59E0B" ,
12131236 query : `SELECT timeBucket() AS t,\n sum(throttled_count) AS throttled\nFROM queue_metrics\nGROUP BY t\nORDER BY t` ,
1214- unitLabel : { singular : "throttled bucket" , plural : "throttled buckets" } ,
12151237 derive : ( rows ) => {
12161238 const sparkline = rows . map ( ( r ) => tileNumber ( r . throttled ) ) ;
12171239 const total = sparkline . reduce ( ( sum , v ) => sum + v , 0 ) ;
@@ -1229,9 +1251,11 @@ type TileTimeRange = MetricResourceTimeRange;
12291251function QueueEnvMetricTile ( {
12301252 tile,
12311253 timeRange,
1254+ referenceLines,
12321255} : {
12331256 tile : QueueHeaderTile ;
12341257 timeRange : TileTimeRange ;
1258+ referenceLines ?: Array < { y : number ; label ?: string } > ;
12351259} ) {
12361260 const organization = useOrganization ( ) ;
12371261 const project = useProject ( ) ;
@@ -1247,37 +1271,89 @@ function QueueEnvMetricTile({
12471271 } ) ;
12481272
12491273 const { sparkline, total, formatTotal, totalClassName } = tile . derive ( rows ) ;
1250- const bucketStartMs = rows . length > 0 ? tileTimeToMs ( rows [ 0 ] . t ) : undefined ;
1251- const bucketIntervalMs =
1252- rows . length >= 2 ? tileTimeToMs ( rows [ 1 ] . t ) - tileTimeToMs ( rows [ 0 ] . t ) : undefined ;
1274+
1275+ // Same point shape the full-size charts use so the shared axis/tooltip helpers apply.
1276+ const data = rows
1277+ . map ( ( r , i ) => ( { bucket : tileTimeToMs ( r . t ) , [ tile . id ] : sparkline [ i ] ?? 0 } ) )
1278+ . filter ( ( p ) => Number . isFinite ( p . bucket ) ) ;
1279+
1280+ const chartConfig = useMemo < ChartConfig > (
1281+ ( ) => ( { [ tile . id ] : { label : tile . label , color : tile . color } } ) ,
1282+ [ tile . id , tile . label , tile . color ]
1283+ ) ;
1284+
1285+ const { tooltipLabelFormatter } = useMemo ( ( ) => buildActivityTimeAxis ( data ) , [ data ] ) ;
1286+ const hasData = data . length > 0 && sparkline . some ( ( v ) => v > 0 ) ;
12531287
12541288 return (
1255- < HeaderTile label = { tile . label } >
1289+ < HeaderTile
1290+ label = { tile . label }
1291+ value = {
1292+ showLoading ? (
1293+ < span className = "inline-block h-3 w-12 animate-pulse rounded bg-grid-bright" />
1294+ ) : failed ? undefined : formatTotal ? (
1295+ formatTotal ( total )
1296+ ) : (
1297+ total . toLocaleString ( )
1298+ )
1299+ }
1300+ valueClassName = { totalClassName }
1301+ >
12561302 < LoadingBarDivider isLoading = { isLoading } className = "bg-transparent" />
12571303 { showLoading ? (
1258- < div className = "h-6 w-full animate-pulse rounded bg-grid-bright/60" />
1304+ < div className = "h-16 w-full animate-pulse rounded bg-grid-bright/60" />
12591305 ) : failed ? (
1260- < div className = "flex h-6 items-center text-xs text-text-dimmed" > Unable to load metrics</ div >
1306+ < div className = "flex h-16 items-center text-xs text-text-dimmed" >
1307+ Unable to load metrics
1308+ </ div >
1309+ ) : hasData ? (
1310+ < div className = "h-16 w-full" >
1311+ < Chart . Root
1312+ config = { chartConfig }
1313+ data = { data }
1314+ dataKey = "bucket"
1315+ series = { [ tile . id ] }
1316+ fillContainer
1317+ >
1318+ < Chart . Line
1319+ lineType = "monotone"
1320+ showDots = { false }
1321+ referenceLines = { referenceLines }
1322+ xAxisProps = { { hide : true } }
1323+ yAxisProps = { { hide : true } }
1324+ tooltipLabelFormatter = { tooltipLabelFormatter }
1325+ tooltipValueFormatter = { tile . formatValue }
1326+ />
1327+ </ Chart . Root >
1328+ </ div >
12611329 ) : (
1262- < UsageSparkline
1263- data = { sparkline }
1264- bucketStartMs = { bucketStartMs }
1265- bucketIntervalMs = { bucketIntervalMs }
1266- color = { tile . color }
1267- unitLabel = { tile . unitLabel }
1268- total = { total }
1269- formatTotal = { formatTotal }
1270- totalClassName = { totalClassName ?? "text-text-bright" }
1271- />
1330+ < div className = "flex h-16 items-center text-xs text-text-dimmed" > No activity</ div >
12721331 ) }
12731332 </ HeaderTile >
12741333 ) ;
12751334}
12761335
1277- function HeaderTile ( { label, children } : { label : ReactNode ; children : ReactNode } ) {
1336+ function HeaderTile ( {
1337+ label,
1338+ value,
1339+ valueClassName,
1340+ children,
1341+ } : {
1342+ label : ReactNode ;
1343+ value ?: ReactNode ;
1344+ valueClassName ?: string ;
1345+ children : ReactNode ;
1346+ } ) {
12781347 return (
12791348 < div className = "flex flex-col gap-1.5 rounded-sm border border-grid-dimmed bg-background-bright px-3 py-2" >
1280- < span className = "truncate text-xs text-text-dimmed" > { label } </ span >
1349+ < div className = "flex items-baseline justify-between gap-2" >
1350+ < span className = "truncate text-xs text-text-dimmed" > { label } </ span >
1351+ { value !== undefined ? (
1352+ < span className = { cn ( "shrink-0 text-sm tabular-nums text-text-bright" , valueClassName ) } >
1353+ { value }
1354+ </ span >
1355+ ) : null }
1356+ </ div >
12811357 { children }
12821358 </ div >
12831359 ) ;
0 commit comments