Skip to content

Commit bc0d1ff

Browse files
matt-aitkensamejr
andauthored
Metrics dashboards (#3019)
Summary - Implemented metrics dashboards with a built-in dashboard and custom dashboards - Added a "Big number” display type What changed - New data format for metric layouts and saving/editing layouts (editing, saving, cancel revert) - QueryWidget usable on Query page and Metrics dashboards - Time filtering, auto-reloading and timeBucket() auto-bin support - Filters added to metrics; widget popover/improved history and blank states - Side menu: - Metrics/Insights section with icons, colors, padding, collapsible behavior and reordering of custom dashboards - Move action logic into service for reuse and API querying; refactor reordering for reuse <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/triggerdotdev/trigger.dev/pull/3019" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: James Ritchie <james@trigger.dev>
1 parent 062bcae commit bc0d1ff

File tree

89 files changed

+9403
-1943
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+9403
-1943
lines changed

.vscode/settings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,5 @@
77
"packages/cli-v3/e2e": true
88
},
99
"vitest.disableWorkspaceWarning": true,
10-
"typescript.experimental.useTsgo": true,
1110
"chat.agent.maxRequests": 10000
1211
}

apps/webapp/app/components/AlphaBadge.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,32 @@ export function AlphaTitle({ children }: { children: React.ReactNode }) {
3030
</>
3131
);
3232
}
33+
34+
export function BetaBadge({
35+
inline = false,
36+
className,
37+
}: {
38+
inline?: boolean;
39+
className?: string;
40+
}) {
41+
return (
42+
<SimpleTooltip
43+
button={
44+
<Badge variant="extra-small" className={cn(inline ? "inline-grid" : "", className)}>
45+
Beta
46+
</Badge>
47+
}
48+
content="This feature is in Beta."
49+
disableHoverableContent
50+
/>
51+
);
52+
}
53+
54+
export function BetaTitle({ children }: { children: React.ReactNode }) {
55+
return (
56+
<>
57+
<span>{children}</span>
58+
<BetaBadge />
59+
</>
60+
);
61+
}

apps/webapp/app/components/code/AIQueryInput.tsx

Lines changed: 65 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { PencilSquareIcon, PlusIcon, SparklesIcon } from "@heroicons/react/20/solid";
1+
import { CheckIcon, PencilSquareIcon, PlusIcon, XMarkIcon } from "@heroicons/react/20/solid";
22
import { AnimatePresence, motion } from "framer-motion";
33
import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react";
4-
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
4+
import { Button } from "~/components/primitives/Buttons";
5+
import { Spinner } from "~/components/primitives/Spinner";
6+
import { useEnvironment } from "~/hooks/useEnvironment";
7+
import { useOrganization } from "~/hooks/useOrganizations";
8+
import { useProject } from "~/hooks/useProject";
9+
import type { AITimeFilter } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/types";
10+
import { cn } from "~/utils/cn";
511

612
// Lazy load streamdown components to avoid SSR issues
713
const StreamdownRenderer = lazy(() =>
@@ -13,13 +19,6 @@ const StreamdownRenderer = lazy(() =>
1319
),
1420
}))
1521
);
16-
import { Button } from "~/components/primitives/Buttons";
17-
import { Spinner } from "~/components/primitives/Spinner";
18-
import { useEnvironment } from "~/hooks/useEnvironment";
19-
import { useOrganization } from "~/hooks/useOrganizations";
20-
import { useProject } from "~/hooks/useProject";
21-
import type { AITimeFilter } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/types";
22-
import { cn } from "~/utils/cn";
2322

2423
type StreamEventType =
2524
| { type: "thinking"; content: string }
@@ -179,21 +178,7 @@ export function AIQueryInput({
179178
setThinking((prev) => prev + event.content);
180179
break;
181180
case "tool_call":
182-
if (event.tool === "setTimeFilter") {
183-
setThinking((prev) => {
184-
if (prev.trimEnd().endsWith("Setting time filter...")) {
185-
return prev;
186-
}
187-
return prev + `\nSetting time filter...\n`;
188-
});
189-
} else {
190-
setThinking((prev) => {
191-
if (prev.trimEnd().endsWith("Validating query...")) {
192-
return prev;
193-
}
194-
return prev + `\nValidating query...\n`;
195-
});
196-
}
181+
// Tool calls are handled silently — no UI text needed
197182
break;
198183
case "time_filter":
199184
// Apply time filter immediately when the AI sets it
@@ -262,13 +247,13 @@ export function AIQueryInput({
262247
}, [error]);
263248

264249
return (
265-
<div className="flex flex-col gap-3">
250+
<div className="flex flex-col">
266251
{/* Gradient border wrapper like the schedules AI input */}
267252
<div
268-
className="rounded-md p-px"
253+
className="overflow-hidden rounded-md p-px"
269254
style={{ background: "linear-gradient(to bottom right, #E543FF, #286399)" }}
270255
>
271-
<div className="overflow-hidden rounded-[5px] bg-background-bright">
256+
<div className="overflow-hidden rounded-md bg-background-bright">
272257
<form onSubmit={handleSubmit}>
273258
<textarea
274259
ref={textareaRef}
@@ -297,10 +282,10 @@ export function AIQueryInput({
297282
variant="tertiary/small"
298283
disabled={true}
299284
LeadingIcon={Spinner}
300-
className="pl-1.5"
285+
className="pl-2"
301286
iconSpacing="gap-1.5"
302287
>
303-
{mode === "edit" ? "Editing..." : "Generating..."}
288+
{mode === "edit" ? "Editing" : "Generating"}
304289
</Button>
305290
) : (
306291
<>
@@ -366,64 +351,60 @@ export function AIQueryInput({
366351
transition={{ duration: 0.2 }}
367352
className="overflow-hidden"
368353
>
369-
<div className="rounded-md border border-grid-dimmed bg-charcoal-850 p-3">
370-
<div className="mb-2 flex items-center justify-between">
371-
<div className="flex items-center gap-2">
372-
{isLoading ? (
373-
<Spinner
374-
color={{
375-
background: "rgba(99, 102, 241, 0.3)",
376-
foreground: "rgba(99, 102, 241, 1)",
377-
}}
378-
className="size-3"
379-
/>
380-
) : lastResult === "success" ? (
381-
<div className="size-3 rounded-full bg-success" />
382-
) : lastResult === "error" ? (
383-
<div className="size-3 rounded-full bg-error" />
384-
) : null}
385-
<span className="text-xs font-medium text-text-dimmed">
386-
{isLoading
387-
? "AI is thinking..."
388-
: lastResult === "success"
354+
<div className="px-1">
355+
<div className="rounded-b-lg border-x border-b border-grid-dimmed bg-charcoal-850 p-3 pb-1">
356+
<div className="mb-1 flex items-center justify-between">
357+
<div className="flex items-center gap-1">
358+
{isLoading ? (
359+
<Spinner className="size-4" />
360+
) : lastResult === "success" ? (
361+
<CheckIcon className="size-4 text-success" />
362+
) : lastResult === "error" ? (
363+
<XMarkIcon className="size-4 text-error" />
364+
) : null}
365+
<span className="text-xs font-medium text-text-dimmed">
366+
{isLoading
367+
? "AI is thinking…"
368+
: lastResult === "success"
389369
? "Query generated"
390370
: lastResult === "error"
391-
? "Generation failed"
392-
: "AI response"}
393-
</span>
371+
? "Generation failed"
372+
: "AI response"}
373+
</span>
374+
</div>
375+
{isLoading ? (
376+
<Button
377+
variant="minimal/small"
378+
onClick={() => {
379+
if (abortControllerRef.current) {
380+
abortControllerRef.current.abort();
381+
}
382+
setIsLoading(false);
383+
setShowThinking(false);
384+
setThinking("");
385+
}}
386+
className="text-xs"
387+
>
388+
Cancel
389+
</Button>
390+
) : (
391+
<Button
392+
variant="minimal/small"
393+
onClick={() => {
394+
setShowThinking(false);
395+
setThinking("");
396+
}}
397+
className="text-xs"
398+
>
399+
Dismiss
400+
</Button>
401+
)}
402+
</div>
403+
<div className="streamdown-container max-h-96 overflow-y-auto text-xs text-text-dimmed scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
404+
<Suspense fallback={<p className="whitespace-pre-wrap">{thinking}</p>}>
405+
<StreamdownRenderer isAnimating={isLoading}>{thinking}</StreamdownRenderer>
406+
</Suspense>
394407
</div>
395-
{isLoading ? (
396-
<Button
397-
variant="minimal/small"
398-
onClick={() => {
399-
if (abortControllerRef.current) {
400-
abortControllerRef.current.abort();
401-
}
402-
setIsLoading(false);
403-
setShowThinking(false);
404-
setThinking("");
405-
}}
406-
className="text-xs"
407-
>
408-
Cancel
409-
</Button>
410-
) : (
411-
<Button
412-
variant="minimal/small"
413-
onClick={() => {
414-
setShowThinking(false);
415-
setThinking("");
416-
}}
417-
className="text-xs"
418-
>
419-
Dismiss
420-
</Button>
421-
)}
422-
</div>
423-
<div className="streamdown-container max-h-96 overflow-y-auto text-xs text-text-dimmed scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
424-
<Suspense fallback={<p className="whitespace-pre-wrap">{thinking}</p>}>
425-
<StreamdownRenderer isAnimating={isLoading}>{thinking}</StreamdownRenderer>
426-
</Suspense>
427408
</div>
428409
</div>
429410
</motion.div>

0 commit comments

Comments
 (0)