chore: format and lint webapp also (#4056)
#3977 added formatting and linting everywhere else. This extends it to the webapp.
This commit is contained in:
@@ -12,7 +12,6 @@
|
||||
"sortPackageJson": false,
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"apps/webapp",
|
||||
".env",
|
||||
".env.local",
|
||||
"pnpm-lock.yaml",
|
||||
|
||||
+1
-2
@@ -7,8 +7,7 @@
|
||||
"**/*.d.ts",
|
||||
"**/seed.js",
|
||||
"**/seedCloud.ts",
|
||||
"**/populate.js",
|
||||
"apps/webapp/**"
|
||||
"**/populate.js"
|
||||
],
|
||||
// All rules below are disabled because they currently fire on the existing
|
||||
// codebase (1748 warnings across these 20 rules as of this commit). Disabled
|
||||
|
||||
@@ -19,13 +19,7 @@ export function AIPenIcon({ className }: { className?: string }) {
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<line
|
||||
x1="17.6464"
|
||||
y1="10.3536"
|
||||
x2="13.6464"
|
||||
y2="6.35355"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<line x1="17.6464" y1="10.3536" x2="13.6464" y2="6.35355" stroke="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,12 @@ export function CerebrasIcon({ className }: IconProps) {
|
||||
|
||||
export function MistralIcon({ className }: IconProps) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M7.28516 3.74658H4.1416V6.8809H7.28516V3.74658Z" />
|
||||
<path d="M19.8579 3.74658H16.7144V6.8809H19.8579V3.74658Z" />
|
||||
<path d="M10.4277 6.88086H4.1416V10.0152H10.4277V6.88086Z" />
|
||||
@@ -174,4 +179,3 @@ export function AzureIcon({ className }: IconProps) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,22 +8,8 @@ export function AttemptIcon({ className }: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect
|
||||
width="6"
|
||||
height="2"
|
||||
rx="1"
|
||||
transform="matrix(1 0 0 -1 9 15)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||
<rect width="6" height="2" rx="1" transform="matrix(1 0 0 -1 9 15)" fill="currentColor" />
|
||||
<rect
|
||||
width="2.48444"
|
||||
height="2"
|
||||
|
||||
@@ -18,12 +18,7 @@ export function ClockIcon({ className }: { className?: string }) {
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M15.5 15.5L12 12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path d="M15.5 15.5L12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,15 +8,7 @@ export function FunctionIcon({ className }: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
|
||||
@@ -8,15 +8,7 @@ export function InfoIcon({ className }: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect x="3" y="3" width="18" height="18" rx="9" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M9.90005 11.7C9.90005 11.2029 10.303 10.8 10.8 10.8H11.1041C12.4476 10.8 13.4455 12.044 13.1541 13.3555L12.6031 15.8348C12.5615 16.0222 12.704 16.1999 12.896 16.1999H13.2C13.6971 16.1999 14.1 16.6029 14.1 17.0999C14.1 17.597 13.6971 17.9999 13.2 17.9999H12.896C11.5525 17.9999 10.5545 16.7558 10.846 15.4444L11.397 12.965C11.4386 12.7777 11.296 12.5999 11.1041 12.5999H10.8C10.303 12.5999 9.90005 12.197 9.90005 11.7Z"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -13,11 +13,7 @@ export function IntegrationsIcon({ className }: { className?: string }) {
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M4 7H10V14H3V8C3 7.44772 3.44772 7 4 7Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path d="M4 7H10V14H3V8C3 7.44772 3.44772 7 4 7Z" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M17 14V20C17 20.5523 16.5523 21 16 21H10V14H17Z"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -15,15 +15,7 @@ export function RightSideMenuIcon({ className }: { className?: string }) {
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<rect
|
||||
x="4"
|
||||
y="4"
|
||||
width="16"
|
||||
height="16"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||
<motion.rect
|
||||
y="6"
|
||||
height="12"
|
||||
|
||||
@@ -8,15 +8,7 @@ export function RunFunctionIcon({ className }: { className?: string }) {
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
export function SlackMonoIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z" />
|
||||
<path d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z" />
|
||||
<path d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.27 0a2.528 2.528 0 0 1-2.522 2.521 2.527 2.527 0 0 1-2.521-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.522 2.522v6.312z" />
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
export function StreamsIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 19C3 19 5.01155 17 8 17C10.9885 17 13 18.9973 16 18.9973C19 18.9973 21 17 21 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
<path d="M3 13.0001C3 13.0001 5.01155 11 8 11C10.9885 11 13 13 16 13C19 13 21 11.0001 21 11.0001" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
<path d="M3 7C3 7 5.01155 5 8 5C10.9885 5 13 6.9973 16 6.9973C19 6.9973 21 5 21 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3 19C3 19 5.01155 17 8 17C10.9885 17 13 18.9973 16 18.9973C19 18.9973 21 17 21 17"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 13.0001C3 13.0001 5.01155 11 8 11C10.9885 11 13 13 16 13C19 13 21 11.0001 21 11.0001"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 7C3 7 5.01155 5 8 5C10.9885 5 13 6.9973 16 6.9973C19 6.9973 21 5 21 5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,42 @@ export function TextSquareIcon({ className }: { className?: string }) {
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||
<line x1="7" y1="8" x2="13" y2="8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<line x1="7" y1="12" x2="10" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<line x1="13" y1="12" x2="17" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<line x1="7" y1="16" x2="15" y2="16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
<line
|
||||
x1="7"
|
||||
y1="8"
|
||||
x2="13"
|
||||
y2="8"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="7"
|
||||
y1="12"
|
||||
x2="10"
|
||||
y2="12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="13"
|
||||
y1="12"
|
||||
x2="17"
|
||||
y2="12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="7"
|
||||
y1="16"
|
||||
x2="15"
|
||||
y2="16"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,10 +22,7 @@ function cleanupLegacyResizablePanelStorage() {
|
||||
const toRemove: string[] = [];
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = window.localStorage.key(i);
|
||||
if (
|
||||
key &&
|
||||
(key.startsWith("panel-group-react-aria") || key === "panel-run-parent-v2")
|
||||
) {
|
||||
if (key && (key.startsWith("panel-group-react-aria") || key === "panel-run-parent-v2")) {
|
||||
toRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +134,7 @@ function AskAIProvider({ websiteId, isCollapsed = false }: AskAIProviderProps) {
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
sideOffset={8}
|
||||
className="flex items-center gap-2 text-xs"
|
||||
>
|
||||
<TooltipContent side="right" sideOffset={8} className="flex items-center gap-2 text-xs">
|
||||
Ask AI
|
||||
<span className="flex items-center">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
|
||||
|
||||
@@ -208,17 +208,17 @@ export function SessionsNone() {
|
||||
}
|
||||
>
|
||||
<Paragraph spacing variant="small">
|
||||
A session is a stateful execution of an agent, with two-way streaming and durable
|
||||
compute. A single session can have multiple runs associated with it, so one conversation
|
||||
can span many task triggers. The input stream carries incoming user messages, and the
|
||||
output stream carries everything the agent produces, including AI generation parts (text,
|
||||
reasoning, tool calls, etc.) and any custom data parts your task emits.
|
||||
A session is a stateful execution of an agent, with two-way streaming and durable compute. A
|
||||
single session can have multiple runs associated with it, so one conversation can span many
|
||||
task triggers. The input stream carries incoming user messages, and the output stream
|
||||
carries everything the agent produces, including AI generation parts (text, reasoning, tool
|
||||
calls, etc.) and any custom data parts your task emits.
|
||||
</Paragraph>
|
||||
<Paragraph spacing variant="small">
|
||||
The easiest way to create one is to trigger a <InlineCode>chat.agent</InlineCode> task,
|
||||
which is built on sessions and handles the chat turn loop for you. You can also call{" "}
|
||||
<InlineCode>sessions.start()</InlineCode> directly for non-chat patterns like agent
|
||||
inboxes, approval flows, or server-to-server streaming.
|
||||
<InlineCode>sessions.start()</InlineCode> directly for non-chat patterns like agent inboxes,
|
||||
approval flows, or server-to-server streaming.
|
||||
</Paragraph>
|
||||
</InfoPanel>
|
||||
);
|
||||
|
||||
@@ -154,8 +154,8 @@ export function DevPresencePanel({ isConnected }: { isConnected: boolean | undef
|
||||
{isConnected === undefined
|
||||
? "Checking connection..."
|
||||
: isConnected
|
||||
? "Your dev server is connected"
|
||||
: "Your dev server is not connected"}
|
||||
? "Your dev server is connected"
|
||||
: "Your dev server is not connected"}
|
||||
</DialogHeader>
|
||||
<div className="mt-2 flex flex-col gap-3 px-2">
|
||||
<div className="flex flex-col items-center justify-center gap-6 px-6 py-10">
|
||||
@@ -169,8 +169,8 @@ export function DevPresencePanel({ isConnected }: { isConnected: boolean | undef
|
||||
{isConnected === undefined
|
||||
? "Checking connection..."
|
||||
: isConnected
|
||||
? "Your local dev server is connected to Trigger.dev"
|
||||
: "Your local dev server is not connected to Trigger.dev"}
|
||||
? "Your local dev server is connected to Trigger.dev"
|
||||
: "Your local dev server is not connected to Trigger.dev"}
|
||||
</Paragraph>
|
||||
</div>
|
||||
{isConnected ? null : (
|
||||
|
||||
@@ -63,8 +63,8 @@ export function LoginPageLayout({ children }: { children: React.ReactNode }) {
|
||||
</div>
|
||||
<div className="flex h-full max-w-sm items-center justify-center">{children}</div>
|
||||
<Paragraph variant="small" className="text-center">
|
||||
Having login issues? <TextLink href="https://trigger.dev/contact">Email us</TextLink>{" "}
|
||||
or <TextLink href="https://trigger.dev/discord">ask us in Discord</TextLink>
|
||||
Having login issues? <TextLink href="https://trigger.dev/contact">Email us</TextLink> or{" "}
|
||||
<TextLink href="https://trigger.dev/discord">ask us in Discord</TextLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -209,7 +209,10 @@ export function TriggerLoginStepV3({ title }: TabsProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerDeployStep({ title, environment }: TabsProps & { environment: { type: string } }) {
|
||||
export function TriggerDeployStep({
|
||||
title,
|
||||
environment,
|
||||
}: TabsProps & { environment: { type: string } }) {
|
||||
const triggerCliTag = useTriggerCliTag();
|
||||
const { activePackageManager, setActivePackageManager } = usePackageManager();
|
||||
|
||||
|
||||
@@ -4,13 +4,7 @@ import { useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { Button } from "./primitives/Buttons";
|
||||
import { Header3 } from "./primitives/Headers";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger
|
||||
} from "./primitives/SheetV3";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "./primitives/SheetV3";
|
||||
import { ShortcutKey } from "./primitives/ShortcutKey";
|
||||
|
||||
export function Shortcuts() {
|
||||
@@ -83,7 +77,7 @@ function ShortcutContent() {
|
||||
<ShortcutKey shortcut={{ key: "f" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Toggle side menu">
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"]}} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ modifiers: ["mod"] }} variant="medium/bright" />
|
||||
<ShortcutKey shortcut={{ key: "b" }} variant="medium/bright" />
|
||||
</Shortcut>
|
||||
<Shortcut name="Select filter">
|
||||
|
||||
@@ -40,9 +40,7 @@ export const apiRateLimitDomain: RateLimitDomain = {
|
||||
},
|
||||
};
|
||||
|
||||
export function resolveEffectiveApiRateLimit(
|
||||
override: unknown
|
||||
): EffectiveRateLimit {
|
||||
export function resolveEffectiveApiRateLimit(override: unknown): EffectiveRateLimit {
|
||||
return resolveEffectiveRateLimit(override, apiRateLimitDomain);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import {
|
||||
RateLimitSection,
|
||||
type RateLimitWrapperProps,
|
||||
} from "./RateLimitSection";
|
||||
import { RateLimitSection, type RateLimitWrapperProps } from "./RateLimitSection";
|
||||
|
||||
export const API_RATE_LIMIT_INTENT = "set-rate-limit";
|
||||
export const API_RATE_LIMIT_SAVED_VALUE = "rate-limit";
|
||||
|
||||
export function ApiRateLimitSection(props: RateLimitWrapperProps) {
|
||||
return (
|
||||
<RateLimitSection
|
||||
title="API rate limit"
|
||||
intent={API_RATE_LIMIT_INTENT}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <RateLimitSection title="API rate limit" intent={API_RATE_LIMIT_INTENT} {...props} />;
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ export const batchRateLimitDomain: RateLimitDomain = {
|
||||
},
|
||||
};
|
||||
|
||||
export function resolveEffectiveBatchRateLimit(
|
||||
override: unknown
|
||||
): EffectiveRateLimit {
|
||||
export function resolveEffectiveBatchRateLimit(override: unknown): EffectiveRateLimit {
|
||||
return resolveEffectiveRateLimit(override, batchRateLimitDomain);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import {
|
||||
RateLimitSection,
|
||||
type RateLimitWrapperProps,
|
||||
} from "./RateLimitSection";
|
||||
import { RateLimitSection, type RateLimitWrapperProps } from "./RateLimitSection";
|
||||
|
||||
export const BATCH_RATE_LIMIT_INTENT = "set-batch-rate-limit";
|
||||
export const BATCH_RATE_LIMIT_SAVED_VALUE = "batch-rate-limit";
|
||||
|
||||
export function BatchRateLimitSection(props: RateLimitWrapperProps) {
|
||||
return (
|
||||
<RateLimitSection
|
||||
title="Batch rate limit"
|
||||
intent={BATCH_RATE_LIMIT_INTENT}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <RateLimitSection title="Batch rate limit" intent={BATCH_RATE_LIMIT_INTENT} {...props} />;
|
||||
}
|
||||
|
||||
@@ -68,9 +68,7 @@ export function MaxProjectsSection({
|
||||
<Property.Table>
|
||||
<Property.Item>
|
||||
<Property.Label>Limit</Property.Label>
|
||||
<Property.Value>
|
||||
{maximumProjectCount.toLocaleString()}
|
||||
</Property.Value>
|
||||
<Property.Value>{maximumProjectCount.toLocaleString()}</Property.Value>
|
||||
</Property.Item>
|
||||
</Property.Table>
|
||||
) : (
|
||||
@@ -89,11 +87,7 @@ export function MaxProjectsSection({
|
||||
<FormError>{fieldError("maximumProjectCount")}</FormError>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
disabled={isSubmitting || !value.trim()}
|
||||
>
|
||||
<Button type="submit" variant="primary/medium" disabled={isSubmitting || !value.trim()}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -3,19 +3,12 @@ import {
|
||||
RateLimitTokenBucketConfig,
|
||||
RateLimiterConfig,
|
||||
} from "~/services/authorizationRateLimitMiddleware.server";
|
||||
import {
|
||||
parseDurationToMs,
|
||||
type EffectiveRateLimit,
|
||||
} from "./RateLimitSection";
|
||||
import { parseDurationToMs, type EffectiveRateLimit } from "./RateLimitSection";
|
||||
|
||||
export type RateLimitDomain = {
|
||||
intent: string;
|
||||
systemDefault: () => RateLimiterConfig;
|
||||
apply: (
|
||||
orgId: string,
|
||||
next: RateLimitTokenBucketConfig,
|
||||
adminUserId: string
|
||||
) => Promise<void>;
|
||||
apply: (orgId: string, next: RateLimitTokenBucketConfig, adminUserId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export function resolveEffectiveRateLimit(
|
||||
|
||||
@@ -57,19 +57,12 @@ export function RateLimitSection({
|
||||
const fieldError = (field: string) =>
|
||||
errors && field in errors ? errors[field]?.[0] : undefined;
|
||||
|
||||
const current =
|
||||
effective.config.type === "tokenBucket" ? effective.config : null;
|
||||
const current = effective.config.type === "tokenBucket" ? effective.config : null;
|
||||
|
||||
const [isEditing, setIsEditing] = useState(hasFieldErrors);
|
||||
const [refillRate, setRefillRate] = useState(
|
||||
current ? String(current.refillRate) : ""
|
||||
);
|
||||
const [intervalStr, setIntervalStr] = useState(
|
||||
current ? String(current.interval) : ""
|
||||
);
|
||||
const [maxTokens, setMaxTokens] = useState(
|
||||
current ? String(current.maxTokens) : ""
|
||||
);
|
||||
const [refillRate, setRefillRate] = useState(current ? String(current.refillRate) : "");
|
||||
const [intervalStr, setIntervalStr] = useState(current ? String(current.interval) : "");
|
||||
const [maxTokens, setMaxTokens] = useState(current ? String(current.maxTokens) : "");
|
||||
|
||||
useEffect(() => {
|
||||
if (hasFieldErrors) setIsEditing(true);
|
||||
@@ -125,9 +118,7 @@ export function RateLimitSection({
|
||||
|
||||
<Paragraph variant="small">
|
||||
Status:{" "}
|
||||
{effective.source === "override"
|
||||
? "Custom override active."
|
||||
: "Using system default."}
|
||||
{effective.source === "override" ? "Custom override active." : "Using system default."}
|
||||
</Paragraph>
|
||||
|
||||
{!isEditing ? (
|
||||
@@ -138,9 +129,7 @@ export function RateLimitSection({
|
||||
<>
|
||||
<Property.Item>
|
||||
<Property.Label>Sustained rate</Property.Label>
|
||||
<Property.Value>
|
||||
{currentDescription.sustained}
|
||||
</Property.Value>
|
||||
<Property.Value>{currentDescription.sustained}</Property.Value>
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Burst allowance</Property.Label>
|
||||
@@ -149,9 +138,7 @@ export function RateLimitSection({
|
||||
</>
|
||||
) : (
|
||||
<Property.Item>
|
||||
<Property.Value>
|
||||
Invalid interval on the stored config.
|
||||
</Property.Value>
|
||||
<Property.Value>Invalid interval on the stored config.</Property.Value>
|
||||
</Property.Item>
|
||||
)
|
||||
) : (
|
||||
@@ -166,17 +153,15 @@ export function RateLimitSection({
|
||||
</Property.Item>
|
||||
<Property.Item>
|
||||
<Property.Label>Tokens</Property.Label>
|
||||
<Property.Value>
|
||||
{effective.config.tokens.toLocaleString()}
|
||||
</Property.Value>
|
||||
<Property.Value>{effective.config.tokens.toLocaleString()}</Property.Value>
|
||||
</Property.Item>
|
||||
</>
|
||||
)}
|
||||
</Property.Table>
|
||||
{effective.config.type !== "tokenBucket" && (
|
||||
<Paragraph variant="small" className="text-amber-500">
|
||||
This override is a {effective.config.type} limit and can't be
|
||||
edited from this form. Change it in the database directly.
|
||||
This override is a {effective.config.type} limit and can't be edited from this form.
|
||||
Change it in the database directly.
|
||||
</Paragraph>
|
||||
)}
|
||||
</>
|
||||
@@ -233,10 +218,7 @@ export function RateLimitSection({
|
||||
type="submit"
|
||||
variant="primary/medium"
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
!refillRate.trim() ||
|
||||
!intervalStr.trim() ||
|
||||
!maxTokens.trim()
|
||||
isSubmitting || !refillRate.trim() || !intervalStr.trim() || !maxTokens.trim()
|
||||
}
|
||||
>
|
||||
Save
|
||||
|
||||
@@ -356,10 +356,10 @@ export function AIQueryInput({
|
||||
{isLoading
|
||||
? "AI is thinking…"
|
||||
: lastResult === "success"
|
||||
? "Query generated"
|
||||
: lastResult === "error"
|
||||
? "Generation failed"
|
||||
: "AI response"}
|
||||
? "Query generated"
|
||||
: lastResult === "error"
|
||||
? "Generation failed"
|
||||
: "AI response"}
|
||||
</span>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
|
||||
@@ -872,10 +872,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
|
||||
);
|
||||
|
||||
// Create value formatter for tooltips and legend based on column format
|
||||
const tooltipValueFormatter = useMemo(
|
||||
() => createValueFormatter(yAxisFormat),
|
||||
[yAxisFormat]
|
||||
);
|
||||
const tooltipValueFormatter = useMemo(() => createValueFormatter(yAxisFormat), [yAxisFormat]);
|
||||
|
||||
// Check if the group-by column has a runStatus customRenderType
|
||||
const groupByIsRunStatus = useMemo(() => {
|
||||
@@ -1181,9 +1178,7 @@ function createYAxisFormatter(
|
||||
if (format === "bytes" || format === "decimalBytes") {
|
||||
const divisor = format === "bytes" ? 1024 : 1000;
|
||||
const units =
|
||||
format === "bytes"
|
||||
? ["B", "KiB", "MiB", "GiB", "TiB"]
|
||||
: ["B", "KB", "MB", "GB", "TB"];
|
||||
format === "bytes" ? ["B", "KiB", "MiB", "GiB", "TiB"] : ["B", "KB", "MB", "GB", "TB"];
|
||||
return (value: number): string => {
|
||||
if (value === 0) return "0 B";
|
||||
// Use consistent unit for all ticks based on max value
|
||||
@@ -1205,8 +1200,7 @@ function createYAxisFormatter(
|
||||
}
|
||||
|
||||
if (format === "durationSeconds") {
|
||||
return (value: number): string =>
|
||||
formatDurationMilliseconds(value * 1000, { style: "short" });
|
||||
return (value: number): string => formatDurationMilliseconds(value * 1000, { style: "short" });
|
||||
}
|
||||
|
||||
if (format === "durationNs") {
|
||||
|
||||
@@ -186,10 +186,10 @@ const fuzzyFilter: FilterFn<RowData> = (row, columnId, value, addMeta) => {
|
||||
cellValue === null
|
||||
? "NULL"
|
||||
: cellValue === undefined
|
||||
? ""
|
||||
: typeof cellValue === "object"
|
||||
? JSON.stringify(cellValue)
|
||||
: String(cellValue);
|
||||
? ""
|
||||
: typeof cellValue === "object"
|
||||
? JSON.stringify(cellValue)
|
||||
: String(cellValue);
|
||||
|
||||
// Build searchable strings - formatted value (if we have column metadata)
|
||||
const formattedValue = meta?.outputColumn
|
||||
@@ -569,12 +569,8 @@ function CellValue({
|
||||
if (typeof value === "string") {
|
||||
const spanId = row?.["span_id"];
|
||||
const runPath = v3RunPathFromFriendlyId(value);
|
||||
const href = typeof spanId === "string" && spanId
|
||||
? `${runPath}?span=${spanId}`
|
||||
: runPath;
|
||||
const tooltip = typeof spanId === "string" && spanId
|
||||
? "Jump to span"
|
||||
: "Jump to run";
|
||||
const href = typeof spanId === "string" && spanId ? `${runPath}?span=${spanId}` : runPath;
|
||||
const tooltip = typeof spanId === "string" && spanId ? "Jump to span" : "Jump to run";
|
||||
return (
|
||||
<SimpleTooltip
|
||||
content={tooltip}
|
||||
@@ -590,8 +586,8 @@ function CellValue({
|
||||
const status = isTaskRunStatus(value)
|
||||
? value
|
||||
: isRunFriendlyStatus(value)
|
||||
? runStatusFromFriendlyTitle(value)
|
||||
: undefined;
|
||||
? runStatusFromFriendlyTitle(value)
|
||||
: undefined;
|
||||
if (status) {
|
||||
return (
|
||||
<SimpleTooltip
|
||||
@@ -1047,7 +1043,8 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
|
||||
// Create TanStack Table column definitions from OutputColumnMetadata
|
||||
// Calculate column widths based on content
|
||||
const visibleColumns = useMemo(
|
||||
() => hiddenColumns?.length ? columns.filter((col) => !hiddenColumns.includes(col.name)) : columns,
|
||||
() =>
|
||||
hiddenColumns?.length ? columns.filter((col) => !hiddenColumns.includes(col.name)) : columns,
|
||||
[columns, hiddenColumns]
|
||||
);
|
||||
const columnDefs = useMemo<ColumnDef<RowData, unknown>[]>(
|
||||
|
||||
@@ -67,10 +67,9 @@ export function darkTheme(): Extension {
|
||||
},
|
||||
|
||||
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{
|
||||
backgroundColor: selection,
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
".cm-panels": { backgroundColor: darkBackground, color: ivory },
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
@@ -167,20 +166,14 @@ export function darkTheme(): Extension {
|
||||
backgroundColor: scrollbarBg,
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
{ dark: true }
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the JSON Hero theme.
|
||||
const jsonHeroHighlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: violet },
|
||||
{
|
||||
tag: [
|
||||
tags.name,
|
||||
tags.deleted,
|
||||
tags.character,
|
||||
tags.propertyName,
|
||||
tags.macroName,
|
||||
],
|
||||
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
|
||||
color: lilac,
|
||||
},
|
||||
{ tag: [tags.function(tags.variableName), tags.labelName], color: malibu },
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
// Provides syntax highlighting, autocompletion, and linting for TSQL queries
|
||||
|
||||
export { createTSQLCompletion } from "./tsqlCompletion";
|
||||
export { createTSQLLinter, isValidTSQLQuery, getTSQLError, type TSQLLinterConfig } from "./tsqlLinter";
|
||||
|
||||
export {
|
||||
createTSQLLinter,
|
||||
isValidTSQLQuery,
|
||||
getTSQLError,
|
||||
type TSQLLinterConfig,
|
||||
} from "./tsqlLinter";
|
||||
|
||||
@@ -92,8 +92,8 @@ function createFunctionCompletions(): Completion[] {
|
||||
meta.maxArgs === 0
|
||||
? "()"
|
||||
: meta.minArgs === meta.maxArgs
|
||||
? `(${meta.minArgs} args)`
|
||||
: `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`;
|
||||
? `(${meta.minArgs} args)`
|
||||
: `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`;
|
||||
|
||||
functions.push({
|
||||
label: name,
|
||||
@@ -111,8 +111,8 @@ function createFunctionCompletions(): Completion[] {
|
||||
meta.maxArgs === 0
|
||||
? "()"
|
||||
: meta.minArgs === meta.maxArgs
|
||||
? `(${meta.minArgs} args)`
|
||||
: `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`;
|
||||
? `(${meta.minArgs} args)`
|
||||
: `(${meta.minArgs}${meta.maxArgs ? `-${meta.maxArgs}` : "+"} args)`;
|
||||
|
||||
functions.push({
|
||||
label: name,
|
||||
|
||||
@@ -28,9 +28,7 @@ describe("tsqlLinter", () => {
|
||||
true
|
||||
);
|
||||
expect(
|
||||
isValidTSQLQuery(
|
||||
"SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id"
|
||||
)
|
||||
isValidTSQLQuery("SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -76,4 +74,3 @@ describe("tsqlLinter", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -31,11 +31,7 @@ function parseErrorPosition(message: string): { line: number; column: number } |
|
||||
/**
|
||||
* Convert line/column to a document position
|
||||
*/
|
||||
function positionToOffset(
|
||||
doc: string,
|
||||
line: number,
|
||||
column: number
|
||||
): number {
|
||||
function positionToOffset(doc: string, line: number, column: number): number {
|
||||
const lines = doc.split("\n");
|
||||
|
||||
// line is 1-indexed
|
||||
@@ -210,4 +206,3 @@ export function getTSQLError(query: string): string | null {
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,14 @@ function DiagnosisActions({ actions }: { actions: NonNullable<DiagnosisBlock["ac
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{actions.map((action, i) => {
|
||||
if (action.kind === "view_run" && /^run_[a-z0-9]+$/i.test(action.target)) {
|
||||
return <RunActionButton key={i} runId={action.target} label={action.label} className={buttonClass} />;
|
||||
return (
|
||||
<RunActionButton
|
||||
key={i}
|
||||
runId={action.target}
|
||||
label={action.label}
|
||||
className={buttonClass}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (action.kind === "docs") {
|
||||
const safeUrl = toSafeUrl(action.target);
|
||||
|
||||
@@ -135,12 +135,7 @@ export function ConfigureErrorAlerts({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
action={formAction}
|
||||
{...form.props}
|
||||
className="contents"
|
||||
>
|
||||
<fetcher.Form method="post" action={formAction} {...form.props} className="contents">
|
||||
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<Fieldset className="flex flex-col gap-4 p-4">
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -113,9 +113,7 @@ export function BuildSettingsFields({
|
||||
</div>
|
||||
);
|
||||
if (disabled && disabledReason) {
|
||||
return (
|
||||
<SimpleTooltip key={slug} button={row} content={disabledReason} side="left" />
|
||||
);
|
||||
return <SimpleTooltip key={slug} button={row} content={disabledReason} side="left" />;
|
||||
}
|
||||
return row;
|
||||
})}
|
||||
@@ -140,9 +138,7 @@ export function BuildSettingsFields({
|
||||
disabled={!enabledSlugs.some((s) => pullEnvVarsBeforeBuild.includes(s))}
|
||||
onCheckedChange={(checked) => {
|
||||
onDiscoverEnvVarsChange(
|
||||
checked
|
||||
? enabledSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s))
|
||||
: []
|
||||
checked ? enabledSlugs.filter((s) => pullEnvVarsBeforeBuild.includes(s)) : []
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -185,9 +181,7 @@ export function BuildSettingsFields({
|
||||
</div>
|
||||
);
|
||||
if (disabled && disabledReason) {
|
||||
return (
|
||||
<SimpleTooltip key={slug} button={row} content={disabledReason} side="left" />
|
||||
);
|
||||
return <SimpleTooltip key={slug} button={row} content={disabledReason} side="left" />;
|
||||
}
|
||||
return row;
|
||||
})}
|
||||
@@ -208,10 +202,12 @@ export function BuildSettingsFields({
|
||||
</div>
|
||||
<Hint className="pr-6">
|
||||
When enabled, production deployments wait for Vercel deployment to complete before
|
||||
promoting the Trigger.dev deployment. This will disable the "Auto-assign Custom
|
||||
Production Domains" option in your Vercel project settings to perform staged
|
||||
deployments.{" "}
|
||||
<TextLink href="https://trigger.dev/docs/vercel-integration#atomic-deployments" target="_blank">
|
||||
promoting the Trigger.dev deployment. This will disable the "Auto-assign Custom Production
|
||||
Domains" option in your Vercel project settings to perform staged deployments.{" "}
|
||||
<TextLink
|
||||
href="https://trigger.dev/docs/vercel-integration#atomic-deployments"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</TextLink>
|
||||
.
|
||||
@@ -225,9 +221,8 @@ export function BuildSettingsFields({
|
||||
)}
|
||||
{!currentTriggerVersion && currentTriggerVersionFetchFailed && (
|
||||
<Hint className="pr-6 text-warning">
|
||||
Couldn't read{" "}
|
||||
<span className="font-mono text-text-bright">TRIGGER_VERSION</span> from Vercel —
|
||||
check the Vercel dashboard to confirm the production pin.
|
||||
Couldn't read <span className="font-mono text-text-bright">TRIGGER_VERSION</span> from
|
||||
Vercel — check the Vercel dashboard to confirm the production pin.
|
||||
</Hint>
|
||||
)}
|
||||
</div>
|
||||
@@ -244,9 +239,9 @@ export function BuildSettingsFields({
|
||||
/>
|
||||
</div>
|
||||
<Hint className="pr-6">
|
||||
When enabled, the integration automatically promotes the Vercel deployment after
|
||||
the Trigger.dev build completes. Turn off to manually promote from your Vercel
|
||||
dashboard — Trigger.dev will then promote automatically once you do.
|
||||
When enabled, the integration automatically promotes the Vercel deployment after the
|
||||
Trigger.dev build completes. Turn off to manually promote from your Vercel dashboard —
|
||||
Trigger.dev will then promote automatically once you do.
|
||||
</Hint>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
export function VercelLogo({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 76 65"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<svg viewBox="0 0 76 65" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -4,11 +4,7 @@ import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import {
|
||||
useFetcher,
|
||||
useNavigation,
|
||||
useSearchParams,
|
||||
} from "@remix-run/react";
|
||||
import { useFetcher, useNavigation, useSearchParams } from "@remix-run/react";
|
||||
import { useTypedFetcher } from "remix-typedjson";
|
||||
import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
@@ -31,9 +27,7 @@ import {
|
||||
import { VercelLogo } from "~/components/integrations/VercelLogo";
|
||||
import { BuildSettingsFields } from "~/components/integrations/VercelBuildSettings";
|
||||
import { OctoKitty } from "~/components/GitHubLoginButton";
|
||||
import {
|
||||
ConnectGitHubRepoModal,
|
||||
} from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github";
|
||||
import { ConnectGitHubRepoModal } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github";
|
||||
import {
|
||||
type SyncEnvVarsMapping,
|
||||
type EnvSlug,
|
||||
@@ -44,7 +38,12 @@ import {
|
||||
} from "~/v3/vercel/vercelProjectIntegrationSchema";
|
||||
import { type VercelCustomEnvironment } from "~/models/vercelIntegration.server";
|
||||
import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresenter.server";
|
||||
import { vercelAppInstallPath, v3ProjectSettingsIntegrationsPath, githubAppInstallPath, vercelResourcePath } from "~/utils/pathBuilder";
|
||||
import {
|
||||
vercelAppInstallPath,
|
||||
v3ProjectSettingsIntegrationsPath,
|
||||
githubAppInstallPath,
|
||||
vercelResourcePath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import type { loader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel";
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { usePostHogTracking } from "~/hooks/usePostHog";
|
||||
@@ -73,9 +72,7 @@ function formatVercelTargets(targets: string[]): string {
|
||||
staging: "Staging",
|
||||
};
|
||||
|
||||
return targets
|
||||
.map((t) => targetLabels[t.toLowerCase()] || t)
|
||||
.join(", ");
|
||||
return targets.map((t) => targetLabels[t.toLowerCase()] || t).join(", ");
|
||||
}
|
||||
|
||||
type OnboardingState =
|
||||
@@ -153,7 +150,8 @@ export function VercelOnboardingModal({
|
||||
}
|
||||
// For marketplace origin, skip env-mapping step and go directly to env-var-sync
|
||||
if (!fromMarketplaceContext) {
|
||||
const customEnvs = (onboardingData?.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment;
|
||||
const customEnvs =
|
||||
(onboardingData?.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment;
|
||||
if (customEnvs) {
|
||||
return "env-mapping";
|
||||
}
|
||||
@@ -218,14 +216,18 @@ export function VercelOnboardingModal({
|
||||
environmentId: string;
|
||||
displayName: string;
|
||||
} | null>(null);
|
||||
const availableEnvSlugsForOnboarding = getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment);
|
||||
const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings(hasStagingEnvironment, hasPreviewEnvironment);
|
||||
const availableEnvSlugsForOnboarding = getAvailableEnvSlugs(
|
||||
hasStagingEnvironment,
|
||||
hasPreviewEnvironment
|
||||
);
|
||||
const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings(
|
||||
hasStagingEnvironment,
|
||||
hasPreviewEnvironment
|
||||
);
|
||||
const [pullEnvVarsBeforeBuild, setPullEnvVarsBeforeBuild] = useState<EnvSlug[]>(
|
||||
() => availableEnvSlugsForOnboardingBuildSettings
|
||||
);
|
||||
const [atomicBuilds, setAtomicBuilds] = useState<EnvSlug[]>(
|
||||
() => ["prod"]
|
||||
);
|
||||
const [atomicBuilds, setAtomicBuilds] = useState<EnvSlug[]>(() => ["prod"]);
|
||||
const [discoverEnvVars, setDiscoverEnvVars] = useState<EnvSlug[]>(
|
||||
() => availableEnvSlugsForOnboardingBuildSettings
|
||||
);
|
||||
@@ -309,7 +311,13 @@ export function VercelOnboardingModal({
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [isOpen, fromMarketplaceContext, nextUrl, isOnboardingComplete, isGitHubConnectedForOnboarding]);
|
||||
}, [
|
||||
isOpen,
|
||||
fromMarketplaceContext,
|
||||
nextUrl,
|
||||
isOnboardingComplete,
|
||||
isGitHubConnectedForOnboarding,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@@ -336,7 +344,6 @@ export function VercelOnboardingModal({
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
|
||||
case "loading-projects":
|
||||
loadingStateRef.current = state;
|
||||
if (onDataReload) {
|
||||
@@ -371,19 +378,33 @@ export function VercelOnboardingModal({
|
||||
}, [isOpen, state, onboardingData?.authInvalid, vercelStagingEnvironment, onDataReload, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onboardingData?.authInvalid && state === "loading-projects" && onboardingData?.availableProjects !== undefined) {
|
||||
if (
|
||||
!onboardingData?.authInvalid &&
|
||||
state === "loading-projects" &&
|
||||
onboardingData?.availableProjects !== undefined
|
||||
) {
|
||||
setState("project-selection");
|
||||
}
|
||||
}, [state, onboardingData?.availableProjects, onboardingData?.authInvalid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onboardingData?.authInvalid && state === "loading-env-vars" && onboardingData?.environmentVariables) {
|
||||
if (
|
||||
!onboardingData?.authInvalid &&
|
||||
state === "loading-env-vars" &&
|
||||
onboardingData?.environmentVariables
|
||||
) {
|
||||
setState("env-var-sync");
|
||||
}
|
||||
}, [state, onboardingData?.environmentVariables, onboardingData?.authInvalid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === "project-selection" && fetcher.data && "success" in fetcher.data && fetcher.data.success && fetcher.state === "idle") {
|
||||
if (
|
||||
state === "project-selection" &&
|
||||
fetcher.data &&
|
||||
"success" in fetcher.data &&
|
||||
fetcher.data.success &&
|
||||
fetcher.state === "idle"
|
||||
) {
|
||||
trackOnboarding("vercel onboarding project selected", {
|
||||
vercel_project_name: selectedVercelProject?.name,
|
||||
});
|
||||
@@ -394,12 +415,20 @@ export function VercelOnboardingModal({
|
||||
} else if (fetcher.data && "error" in fetcher.data && typeof fetcher.data.error === "string") {
|
||||
setProjectSelectionError(fetcher.data.error);
|
||||
}
|
||||
}, [state, fetcher.data, fetcher.state, onDataReload, trackOnboarding, selectedVercelProject?.name]);
|
||||
}, [
|
||||
state,
|
||||
fetcher.data,
|
||||
fetcher.state,
|
||||
onDataReload,
|
||||
trackOnboarding,
|
||||
selectedVercelProject?.name,
|
||||
]);
|
||||
|
||||
// For marketplace origin, skip env-mapping step
|
||||
useEffect(() => {
|
||||
if (state === "loading-env-mapping" && onboardingData) {
|
||||
const hasCustomEnvs = (onboardingData.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment;
|
||||
const hasCustomEnvs =
|
||||
(onboardingData.customEnvironments?.length ?? 0) > 0 && hasStagingEnvironment;
|
||||
if (hasCustomEnvs && !fromMarketplaceContext) {
|
||||
setState("env-mapping");
|
||||
} else {
|
||||
@@ -410,14 +439,13 @@ export function VercelOnboardingModal({
|
||||
|
||||
const secretEnvVars = envVars.filter((v) => v.isSecret);
|
||||
const syncableEnvVars = envVars.filter((v) => !v.isSecret);
|
||||
const enabledEnvVars = syncableEnvVars.filter(
|
||||
(v) => shouldSyncEnvVarForAnyEnvironment(syncEnvVarsMapping, v.key)
|
||||
const enabledEnvVars = syncableEnvVars.filter((v) =>
|
||||
shouldSyncEnvVarForAnyEnvironment(syncEnvVarsMapping, v.key)
|
||||
);
|
||||
|
||||
const overlappingEnvVarsCount = enabledEnvVars.filter((v) => existingVars[v.key]).length;
|
||||
|
||||
const isSubmitting =
|
||||
navigation.state === "submitting" || navigation.state === "loading";
|
||||
const isSubmitting = navigation.state === "submitting" || navigation.state === "loading";
|
||||
|
||||
const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug);
|
||||
|
||||
@@ -529,7 +557,6 @@ export function VercelOnboardingModal({
|
||||
method: "post",
|
||||
action: actionUrl,
|
||||
});
|
||||
|
||||
}, [vercelStagingEnvironment, envMappingFetcher, actionUrl, trackOnboarding]);
|
||||
|
||||
const handleBuildSettingsNext = useCallback(() => {
|
||||
@@ -541,7 +568,10 @@ export function VercelOnboardingModal({
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("action", "complete-onboarding");
|
||||
formData.append("vercelStagingEnvironment", vercelStagingEnvironment ? JSON.stringify(vercelStagingEnvironment) : "");
|
||||
formData.append(
|
||||
"vercelStagingEnvironment",
|
||||
vercelStagingEnvironment ? JSON.stringify(vercelStagingEnvironment) : ""
|
||||
);
|
||||
formData.append("pullEnvVarsBeforeBuild", JSON.stringify(pullEnvVarsBeforeBuild));
|
||||
formData.append("atomicBuilds", JSON.stringify(atomicBuilds));
|
||||
formData.append("discoverEnvVars", JSON.stringify(discoverEnvVars));
|
||||
@@ -572,24 +602,52 @@ export function VercelOnboardingModal({
|
||||
github_app_installed: gitHubAppInstallations.length > 0,
|
||||
});
|
||||
}
|
||||
}, [vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, nextUrl, fromMarketplaceContext, isGitHubConnectedForOnboarding, completeOnboardingFetcher, actionUrl, trackOnboarding, capture, organizationSlug, projectSlug, gitHubAppInstallations.length]);
|
||||
}, [
|
||||
vercelStagingEnvironment,
|
||||
pullEnvVarsBeforeBuild,
|
||||
atomicBuilds,
|
||||
discoverEnvVars,
|
||||
syncEnvVarsMapping,
|
||||
nextUrl,
|
||||
fromMarketplaceContext,
|
||||
isGitHubConnectedForOnboarding,
|
||||
completeOnboardingFetcher,
|
||||
actionUrl,
|
||||
trackOnboarding,
|
||||
capture,
|
||||
organizationSlug,
|
||||
projectSlug,
|
||||
gitHubAppInstallations.length,
|
||||
]);
|
||||
|
||||
const handleFinishOnboarding = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
completeOnboardingFetcher.submit(formData, {
|
||||
method: "post",
|
||||
action: actionUrl,
|
||||
});
|
||||
}, [completeOnboardingFetcher, actionUrl]);
|
||||
const handleFinishOnboarding = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
completeOnboardingFetcher.submit(formData, {
|
||||
method: "post",
|
||||
action: actionUrl,
|
||||
});
|
||||
},
|
||||
[completeOnboardingFetcher, actionUrl]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (completeOnboardingFetcher.data && typeof completeOnboardingFetcher.data === "object" && "success" in completeOnboardingFetcher.data && completeOnboardingFetcher.data.success && completeOnboardingFetcher.state === "idle") {
|
||||
if (
|
||||
completeOnboardingFetcher.data &&
|
||||
typeof completeOnboardingFetcher.data === "object" &&
|
||||
"success" in completeOnboardingFetcher.data &&
|
||||
completeOnboardingFetcher.data.success &&
|
||||
completeOnboardingFetcher.state === "idle"
|
||||
) {
|
||||
if (state === "github-connection") {
|
||||
return;
|
||||
}
|
||||
if ("redirectTo" in completeOnboardingFetcher.data && typeof completeOnboardingFetcher.data.redirectTo === "string") {
|
||||
if (
|
||||
"redirectTo" in completeOnboardingFetcher.data &&
|
||||
typeof completeOnboardingFetcher.data.redirectTo === "string"
|
||||
) {
|
||||
const validRedirect = safeRedirectUrl(completeOnboardingFetcher.data.redirectTo);
|
||||
if (validRedirect) {
|
||||
window.location.href = validRedirect;
|
||||
@@ -632,7 +690,13 @@ export function VercelOnboardingModal({
|
||||
}, [state, organizationSlug, projectSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
if (envMappingFetcher.data && typeof envMappingFetcher.data === "object" && "success" in envMappingFetcher.data && envMappingFetcher.data.success && envMappingFetcher.state === "idle") {
|
||||
if (
|
||||
envMappingFetcher.data &&
|
||||
typeof envMappingFetcher.data === "object" &&
|
||||
"success" in envMappingFetcher.data &&
|
||||
envMappingFetcher.data.success &&
|
||||
envMappingFetcher.state === "idle"
|
||||
) {
|
||||
setState("loading-env-vars");
|
||||
}
|
||||
}, [envMappingFetcher.data, envMappingFetcher.state]);
|
||||
@@ -644,9 +708,7 @@ export function VercelOnboardingModal({
|
||||
if (customEnvironments.length === 1) {
|
||||
selectedEnv = customEnvironments[0];
|
||||
} else {
|
||||
const stagingEnv = customEnvironments.find(
|
||||
(env) => env.slug.toLowerCase() === "staging"
|
||||
);
|
||||
const stagingEnv = customEnvironments.find((env) => env.slug.toLowerCase() === "staging");
|
||||
selectedEnv = stagingEnv ?? customEnvironments[0];
|
||||
}
|
||||
|
||||
@@ -673,14 +735,17 @@ export function VercelOnboardingModal({
|
||||
|
||||
if (isLoadingState) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => {
|
||||
if (!open && !fromMarketplaceContext) {
|
||||
if (state as string !== "completed") {
|
||||
trackOnboarding("vercel onboarding abandoned");
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && !fromMarketplaceContext) {
|
||||
if ((state as string) !== "completed") {
|
||||
trackOnboarding("vercel onboarding abandoned");
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-lg" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -704,18 +769,23 @@ export function VercelOnboardingModal({
|
||||
|
||||
const disabledEnvSlugsForBuildSettings =
|
||||
hasStagingEnvironment && !vercelStagingEnvironment
|
||||
? ({ stg: "Map a custom Vercel environment to Staging to enable this" } as Partial<Record<EnvSlug, string>>)
|
||||
? ({ stg: "Map a custom Vercel environment to Staging to enable this" } as Partial<
|
||||
Record<EnvSlug, string>
|
||||
>)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => {
|
||||
if (!open && !fromMarketplaceContext) {
|
||||
if (state !== "completed") {
|
||||
trackOnboarding("vercel onboarding abandoned");
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && !fromMarketplaceContext) {
|
||||
if (state !== "completed") {
|
||||
trackOnboarding("vercel onboarding abandoned");
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-lg" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -729,8 +799,8 @@ export function VercelOnboardingModal({
|
||||
<div className="flex flex-col gap-4">
|
||||
<Header3>Select Vercel Project</Header3>
|
||||
<Paragraph className="text-sm">
|
||||
Choose which Vercel project to connect with this Trigger.dev project.
|
||||
Your API keys will be automatically synced to Vercel.
|
||||
Choose which Vercel project to connect with this Trigger.dev project. Your API keys
|
||||
will be automatically synced to Vercel.
|
||||
</Paragraph>
|
||||
|
||||
{availableProjects.length === 0 ? (
|
||||
@@ -763,13 +833,14 @@ export function VercelOnboardingModal({
|
||||
</Select>
|
||||
)}
|
||||
|
||||
{projectSelectionError && (
|
||||
<FormError>{projectSelectionError}</FormError>
|
||||
)}
|
||||
{projectSelectionError && <FormError>{projectSelectionError}</FormError>}
|
||||
|
||||
<Hint>
|
||||
Once connected, your <code className="text-xs rounded bg-charcoal-700 px-1 py-0.5 text-text-bright">TRIGGER_SECRET_KEY</code> will be
|
||||
automatically synced to Vercel for each environment.
|
||||
Once connected, your{" "}
|
||||
<code className="text-xs rounded bg-charcoal-700 px-1 py-0.5 text-text-bright">
|
||||
TRIGGER_SECRET_KEY
|
||||
</code>{" "}
|
||||
will be automatically synced to Vercel for each environment.
|
||||
</Hint>
|
||||
|
||||
<FormButtons
|
||||
@@ -795,10 +866,7 @@ export function VercelOnboardingModal({
|
||||
</div>
|
||||
}
|
||||
cancelButton={
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
onClick={handleSkipOnboarding}
|
||||
>
|
||||
<Button variant="tertiary/medium" onClick={handleSkipOnboarding}>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
@@ -811,11 +879,13 @@ export function VercelOnboardingModal({
|
||||
<Header3>Map Vercel Environment to Staging</Header3>
|
||||
<Paragraph className="text-sm">
|
||||
Select which custom Vercel environment should map to Trigger.dev's Staging
|
||||
environment. Production and Preview environments are mapped automatically.
|
||||
If you skip this step, the{" "}
|
||||
<code className="rounded bg-charcoal-700 px-1 py-0.5 text-text-bright">TRIGGER_SECRET_KEY</code>{" "}
|
||||
will not be installed for the staging environment in Vercel. You can configure this later in
|
||||
project settings.
|
||||
environment. Production and Preview environments are mapped automatically. If you
|
||||
skip this step, the{" "}
|
||||
<code className="rounded bg-charcoal-700 px-1 py-0.5 text-text-bright">
|
||||
TRIGGER_SECRET_KEY
|
||||
</code>{" "}
|
||||
will not be installed for the staging environment in Vercel. You can configure this
|
||||
later in project settings.
|
||||
</Paragraph>
|
||||
|
||||
<Select
|
||||
@@ -842,23 +912,17 @@ export function VercelOnboardingModal({
|
||||
</Select>
|
||||
|
||||
<Paragraph className="text-xs text-text-dimmed">
|
||||
Make sure the staging branch in your Vercel project's Git settings matches the staging branch
|
||||
configured in your GitHub integration.
|
||||
Make sure the staging branch in your Vercel project's Git settings matches the
|
||||
staging branch configured in your GitHub integration.
|
||||
</Paragraph>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
onClick={handleSkipOnboarding}
|
||||
>
|
||||
<Button variant="tertiary/medium" onClick={handleSkipOnboarding}>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{!fromMarketplaceContext && (
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
onClick={handleSkipEnvMapping}
|
||||
>
|
||||
<Button variant="tertiary/medium" onClick={handleSkipEnvMapping}>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
@@ -880,8 +944,18 @@ export function VercelOnboardingModal({
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3>Pull Environment Variables</Header3>
|
||||
<Paragraph className="text-sm">
|
||||
Choose which environment variables to import from Vercel. This runs as a one time pull to prefill your project with the variables it needs. You’ll be able to pull again later, or enable automatic syncing before each build if you prefer.
|
||||
If you are using Supabase or Neon branching, <TextLink href="https://trigger.dev/docs/vercel-integration#supabase-and-neon-database-branching" target="_blank" rel="noopener noreferrer">read the docs</TextLink> for the recommended setup.
|
||||
Choose which environment variables to import from Vercel. This runs as a one time
|
||||
pull to prefill your project with the variables it needs. You’ll be able to pull
|
||||
again later, or enable automatic syncing before each build if you prefer. If you
|
||||
are using Supabase or Neon branching,{" "}
|
||||
<TextLink
|
||||
href="https://trigger.dev/docs/vercel-integration#supabase-and-neon-database-branching"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
read the docs
|
||||
</TextLink>{" "}
|
||||
for the recommended setup.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded border bg-charcoal-800 p-3">
|
||||
@@ -948,10 +1022,11 @@ export function VercelOnboardingModal({
|
||||
</div>
|
||||
<Switch
|
||||
variant="small"
|
||||
checked={shouldSyncEnvVarForAnyEnvironment(syncEnvVarsMapping, envVar.key)}
|
||||
onCheckedChange={(checked) =>
|
||||
handleToggleEnvVar(envVar.key, checked)
|
||||
}
|
||||
checked={shouldSyncEnvVarForAnyEnvironment(
|
||||
syncEnvVarsMapping,
|
||||
envVar.key
|
||||
)}
|
||||
onCheckedChange={(checked) => handleToggleEnvVar(envVar.key, checked)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -968,7 +1043,8 @@ export function VercelOnboardingModal({
|
||||
onClick={() => setExpandedSecretEnvVars(!expandedSecretEnvVars)}
|
||||
>
|
||||
<span className="text-sm text-text-dimmed">
|
||||
{secretEnvVars.length} secret {secretEnvVars.length === 1 ? "variable" : "variables"} (cannot be pulled)
|
||||
{secretEnvVars.length} secret{" "}
|
||||
{secretEnvVars.length === 1 ? "variable" : "variables"} (cannot be pulled)
|
||||
</span>
|
||||
{expandedSecretEnvVars ? (
|
||||
<ChevronUpIcon className="size-4" />
|
||||
@@ -1029,18 +1105,27 @@ export function VercelOnboardingModal({
|
||||
setState("build-settings");
|
||||
}
|
||||
}}
|
||||
disabled={fromMarketplaceContext && (completeOnboardingFetcher.state !== "idle" || isRedirecting)}
|
||||
LeadingIcon={fromMarketplaceContext && (completeOnboardingFetcher.state !== "idle" || isRedirecting) ? SpinnerWhite : undefined}
|
||||
disabled={
|
||||
fromMarketplaceContext &&
|
||||
(completeOnboardingFetcher.state !== "idle" || isRedirecting)
|
||||
}
|
||||
LeadingIcon={
|
||||
fromMarketplaceContext &&
|
||||
(completeOnboardingFetcher.state !== "idle" || isRedirecting)
|
||||
? SpinnerWhite
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{fromMarketplaceContext ? (isGitHubConnectedForOnboarding ? "Finish" : "Next") : "Next"}
|
||||
{fromMarketplaceContext
|
||||
? isGitHubConnectedForOnboarding
|
||||
? "Finish"
|
||||
: "Next"
|
||||
: "Next"}
|
||||
</Button>
|
||||
}
|
||||
cancelButton={
|
||||
hasCustomEnvs && !fromMarketplaceContext ? (
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
onClick={() => setState("env-mapping")}
|
||||
>
|
||||
<Button variant="tertiary/medium" onClick={() => setState("env-mapping")}>
|
||||
Back
|
||||
</Button>
|
||||
) : (
|
||||
@@ -1081,16 +1166,17 @@ export function VercelOnboardingModal({
|
||||
variant="primary/medium"
|
||||
onClick={handleBuildSettingsNext}
|
||||
disabled={completeOnboardingFetcher.state !== "idle" || isRedirecting}
|
||||
LeadingIcon={completeOnboardingFetcher.state !== "idle" || isRedirecting ? SpinnerWhite : undefined}
|
||||
LeadingIcon={
|
||||
completeOnboardingFetcher.state !== "idle" || isRedirecting
|
||||
? SpinnerWhite
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isGitHubConnectedForOnboarding ? "Finish" : "Next"}
|
||||
</Button>
|
||||
}
|
||||
cancelButton={
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
onClick={() => setState("env-var-sync")}
|
||||
>
|
||||
<Button variant="tertiary/medium" onClick={() => setState("env-var-sync")}>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
@@ -1102,14 +1188,14 @@ export function VercelOnboardingModal({
|
||||
<div className="flex flex-col gap-4">
|
||||
<Header3>Connect GitHub Repository</Header3>
|
||||
<Paragraph className="text-sm">
|
||||
To fully integrate with Vercel, Trigger.dev needs access to your source code.
|
||||
This allows automatic deployments and build synchronization.
|
||||
To fully integrate with Vercel, Trigger.dev needs access to your source code. This
|
||||
allows automatic deployments and build synchronization.
|
||||
</Paragraph>
|
||||
|
||||
<Callout variant="info">
|
||||
<p className="text-xs">
|
||||
Connecting your GitHub repository enables Trigger.dev to read your source code
|
||||
and automatically create deployments when you push changes to Vercel.
|
||||
Connecting your GitHub repository enables Trigger.dev to read your source code and
|
||||
automatically create deployments when you push changes to Vercel.
|
||||
</p>
|
||||
</Callout>
|
||||
|
||||
@@ -1139,7 +1225,9 @@ export function VercelOnboardingModal({
|
||||
)}
|
||||
variant="secondary/medium"
|
||||
LeadingIcon={OctoKitty}
|
||||
onClick={() => trackOnboarding("vercel onboarding github app install clicked")}
|
||||
onClick={() =>
|
||||
trackOnboarding("vercel onboarding github app install clicked")
|
||||
}
|
||||
>
|
||||
Install GitHub app
|
||||
</LinkButton>
|
||||
|
||||
@@ -148,7 +148,12 @@ function ModelsDropdown({
|
||||
<ComboBox placeholder="Filter by model..." value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((m) => (
|
||||
<SelectItem key={m.model} value={m.model} className="text-text-bright" icon={modelIcon(m.system, m.model)}>
|
||||
<SelectItem
|
||||
key={m.model}
|
||||
value={m.model}
|
||||
className="text-text-bright"
|
||||
icon={modelIcon(m.system, m.model)}
|
||||
>
|
||||
{m.model}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -126,7 +126,12 @@ function OperationsDropdown({
|
||||
<ComboBox placeholder="Filter by operation..." value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((op) => (
|
||||
<SelectItem key={op} value={op} className="text-text-bright" icon={<CommandLineIcon className="size-4 text-text-dimmed" />}>
|
||||
<SelectItem
|
||||
key={op}
|
||||
value={op}
|
||||
className="text-text-bright"
|
||||
icon={<CommandLineIcon className="size-4 text-text-dimmed" />}
|
||||
>
|
||||
{formatOperation(op)}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -114,7 +114,12 @@ function PromptsDropdown({
|
||||
<ComboBox placeholder="Filter by prompt..." value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((slug) => (
|
||||
<SelectItem key={slug} value={slug} className="text-text-bright" icon={<DocumentTextIcon className="size-4 text-text-dimmed" />}>
|
||||
<SelectItem
|
||||
key={slug}
|
||||
value={slug}
|
||||
className="text-text-bright"
|
||||
icon={<DocumentTextIcon className="size-4 text-text-dimmed" />}
|
||||
>
|
||||
{slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -112,7 +112,12 @@ function ProvidersDropdown({
|
||||
<ComboBox placeholder="Filter by provider..." value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((provider) => (
|
||||
<SelectItem key={provider} value={provider} className="text-text-bright" icon={<ServerIcon className="size-4 text-text-dimmed" />}>
|
||||
<SelectItem
|
||||
key={provider}
|
||||
value={provider}
|
||||
className="text-text-bright"
|
||||
icon={<ServerIcon className="size-4 text-text-dimmed" />}
|
||||
>
|
||||
{provider}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -73,7 +73,16 @@ export function SaveToDashboardDialog({
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [fetcher.state, fetcher.data, selectedDashboardId, onOpenChange, navigate, organization.slug, project.slug, environment.slug]);
|
||||
}, [
|
||||
fetcher.state,
|
||||
fetcher.data,
|
||||
selectedDashboardId,
|
||||
onOpenChange,
|
||||
navigate,
|
||||
organization.slug,
|
||||
project.slug,
|
||||
environment.slug,
|
||||
]);
|
||||
|
||||
// Update selection if dashboards change
|
||||
useEffect(() => {
|
||||
@@ -134,8 +143,8 @@ export function SaveToDashboardDialog({
|
||||
isAtLimit
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: selectedDashboardId === dashboard.friendlyId
|
||||
? "bg-charcoal-700 text-text-bright"
|
||||
: "text-text-dimmed hover:bg-charcoal-750 hover:text-text-bright"
|
||||
? "bg-charcoal-700 text-text-bright"
|
||||
: "text-text-dimmed hover:bg-charcoal-750 hover:text-text-bright"
|
||||
)}
|
||||
>
|
||||
{selectedDashboardId === dashboard.friendlyId ? (
|
||||
|
||||
@@ -170,8 +170,8 @@ function DashboardChildMenuItem({
|
||||
? item.activeColor
|
||||
: undefined
|
||||
: isCollapsed
|
||||
? "text-text-bright"
|
||||
: undefined;
|
||||
? "text-text-bright"
|
||||
: undefined;
|
||||
|
||||
const inactiveIconColor = isCollapsed ? "text-text-dimmed" : "text-charcoal-700";
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ import { useOrganization, type MatchedOrganization } from "~/hooks/useOrganizati
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { branchesPath, branchesDevPath, docsPath, v3BillingPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentCombo, EnvironmentIcon, EnvironmentLabel, environmentFullTitle, environmentTextClassName } from "../environments/EnvironmentLabel";
|
||||
import {
|
||||
EnvironmentCombo,
|
||||
EnvironmentIcon,
|
||||
EnvironmentLabel,
|
||||
environmentFullTitle,
|
||||
environmentTextClassName,
|
||||
} from "../environments/EnvironmentLabel";
|
||||
import { ButtonContent } from "../primitives/Buttons";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
@@ -113,14 +119,14 @@ export function EnvironmentSelector({
|
||||
// multi-branch dev flag on. Without it, dev renders as a plain
|
||||
// selector button (the original behavior). PREVIEW is unaffected.
|
||||
const renderAsBranchable =
|
||||
isBranchableEnvironment(env) &&
|
||||
(env.type !== "DEVELOPMENT" || devBranchesEnabled);
|
||||
isBranchableEnvironment(env) && (env.type !== "DEVELOPMENT" || devBranchesEnabled);
|
||||
|
||||
if (renderAsBranchable) {
|
||||
const branchEnvironments = project.environments.filter(
|
||||
(e) => e.parentEnvironmentId === env.id
|
||||
);
|
||||
const allBranchEnvironments = env.type === "DEVELOPMENT" ? [env, ...branchEnvironments] : branchEnvironments;
|
||||
const allBranchEnvironments =
|
||||
env.type === "DEVELOPMENT" ? [env, ...branchEnvironments] : branchEnvironments;
|
||||
return (
|
||||
<Branches
|
||||
key={env.id}
|
||||
@@ -135,9 +141,7 @@ export function EnvironmentSelector({
|
||||
<PopoverMenuItem
|
||||
key={env.id}
|
||||
to={urlForEnvironment(env)}
|
||||
title={
|
||||
<EnvironmentCombo environment={env} className="mx-auto grow text-2sm" />
|
||||
}
|
||||
title={<EnvironmentCombo environment={env} className="mx-auto grow text-2sm" />}
|
||||
isSelected={env.id === environment.id}
|
||||
/>
|
||||
);
|
||||
@@ -232,8 +236,8 @@ function Branches({
|
||||
branchEnvironments.length === 0
|
||||
? "no-branches"
|
||||
: activeBranches.length === 0
|
||||
? "no-active-branches"
|
||||
: "has-branches";
|
||||
? "no-active-branches"
|
||||
: "has-branches";
|
||||
|
||||
// Only surface the active environment's archived-branch item in the submenu it
|
||||
// actually belongs to. Both Development and Preview render this component, so
|
||||
@@ -276,11 +280,15 @@ function Branches({
|
||||
to={urlForEnvironment(environment)}
|
||||
title={
|
||||
<>
|
||||
<span className={cn("block w-full", envTextClassName)}>{environment.branchName}</span>
|
||||
<span className={cn("block w-full", envTextClassName)}>
|
||||
{environment.branchName}
|
||||
</span>
|
||||
<Badge variant="extra-small">Archived</Badge>
|
||||
</>
|
||||
}
|
||||
icon={<BranchEnvironmentIconSmall className={cn("size-4 shrink-0", envTextClassName)} />}
|
||||
icon={
|
||||
<BranchEnvironmentIconSmall className={cn("size-4 shrink-0", envTextClassName)} />
|
||||
}
|
||||
isSelected={environment.id === currentEnvironment.id}
|
||||
/>
|
||||
)}
|
||||
@@ -292,8 +300,16 @@ function Branches({
|
||||
<PopoverMenuItem
|
||||
key={env.id}
|
||||
to={urlForEnvironment(env)}
|
||||
title={<span className={cn("block w-full", envTextClassName)}>{env.branchName ?? DEFAULT_DEV_BRANCH}</span>}
|
||||
icon={<BranchEnvironmentIconSmall className={cn("size-4 shrink-0", envTextClassName)} />}
|
||||
title={
|
||||
<span className={cn("block w-full", envTextClassName)}>
|
||||
{env.branchName ?? DEFAULT_DEV_BRANCH}
|
||||
</span>
|
||||
}
|
||||
icon={
|
||||
<BranchEnvironmentIconSmall
|
||||
className={cn("size-4 shrink-0", envTextClassName)}
|
||||
/>
|
||||
}
|
||||
isSelected={env.id === currentEnvironment.id}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -58,131 +58,131 @@ export function HelpAndFeedback({
|
||||
className={isCollapsed ? undefined : "flex-1"}
|
||||
>
|
||||
<Popover open={isHelpMenuOpen} onOpenChange={setHelpMenuOpen}>
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
"group flex h-8 items-center gap-1.5 rounded pl-[0.4375rem] pr-2 transition-colors hover:bg-charcoal-750 focus-custom",
|
||||
isCollapsed ? "w-full" : "w-full justify-between"
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-1.5 overflow-hidden">
|
||||
<QuestionMarkIcon className="size-5 min-w-5 shrink-0 text-success" />
|
||||
<span
|
||||
className={cn(
|
||||
"overflow-hidden whitespace-nowrap text-2sm text-text-bright transition-all duration-150",
|
||||
isCollapsed ? "max-w-0 opacity-0" : "max-w-[150px] opacity-100"
|
||||
)}
|
||||
>
|
||||
Help & Feedback
|
||||
</span>
|
||||
</span>
|
||||
<ShortcutKey
|
||||
<SimpleTooltip
|
||||
button={
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
"size-4 flex-none transition-all duration-150",
|
||||
isCollapsed ? "hidden" : ""
|
||||
"group flex h-8 items-center gap-1.5 rounded pl-[0.4375rem] pr-2 transition-colors hover:bg-charcoal-750 focus-custom",
|
||||
isCollapsed ? "w-full" : "w-full justify-between"
|
||||
)}
|
||||
shortcut={{ key: "h" }}
|
||||
variant="medium/bright"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
}
|
||||
content={
|
||||
<span className="flex items-center gap-1">
|
||||
Help & Feedback
|
||||
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
|
||||
</span>
|
||||
}
|
||||
side="right"
|
||||
sideOffset={8}
|
||||
hidden={!isCollapsed}
|
||||
buttonClassName="!h-8 w-full"
|
||||
asChild
|
||||
disableHoverableContent
|
||||
/>
|
||||
<PopoverContent
|
||||
className="min-w-[14rem] divide-y divide-grid-bright overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
side={isCollapsed ? "right" : "top"}
|
||||
sideOffset={isCollapsed ? 8 : 4}
|
||||
align="start"
|
||||
>
|
||||
<Fragment>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Documentation"
|
||||
icon={BookIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Status"
|
||||
icon={RadarPulseIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://status.trigger.dev/"
|
||||
data-action="status"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Suggest a feature"
|
||||
icon={BulbIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://feedback.trigger.dev/"
|
||||
data-action="suggest-a-feature"
|
||||
target="_blank"
|
||||
/>
|
||||
<Shortcuts />
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
className="pl-2"
|
||||
LeadingIcon={EnvelopeIcon}
|
||||
leadingIconClassName="pr-1 text-text-dimmed group-hover/button:text-text-bright"
|
||||
data-action="contact-us"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
<span className="flex items-center gap-1.5 overflow-hidden">
|
||||
<QuestionMarkIcon className="size-5 min-w-5 shrink-0 text-success" />
|
||||
<span
|
||||
className={cn(
|
||||
"overflow-hidden whitespace-nowrap text-2sm text-text-bright transition-all duration-150",
|
||||
isCollapsed ? "max-w-0 opacity-0" : "max-w-[150px] opacity-100"
|
||||
)}
|
||||
>
|
||||
Contact us…
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<Paragraph className="pb-1 pl-1.5 pt-1.5 text-xs">What's new</Paragraph>
|
||||
{changelogs.map((entry) => (
|
||||
Help & Feedback
|
||||
</span>
|
||||
</span>
|
||||
<ShortcutKey
|
||||
className={cn(
|
||||
"size-4 flex-none transition-all duration-150",
|
||||
isCollapsed ? "hidden" : ""
|
||||
)}
|
||||
shortcut={{ key: "h" }}
|
||||
variant="medium/bright"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
}
|
||||
content={
|
||||
<span className="flex items-center gap-1">
|
||||
Help & Feedback
|
||||
<ShortcutKey shortcut={{ key: "h" }} variant="medium/bright" />
|
||||
</span>
|
||||
}
|
||||
side="right"
|
||||
sideOffset={8}
|
||||
hidden={!isCollapsed}
|
||||
buttonClassName="!h-8 w-full"
|
||||
asChild
|
||||
disableHoverableContent
|
||||
/>
|
||||
<PopoverContent
|
||||
className="min-w-[14rem] divide-y divide-grid-bright overflow-y-auto p-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
side={isCollapsed ? "right" : "top"}
|
||||
sideOffset={isCollapsed ? 8 : 4}
|
||||
align="start"
|
||||
>
|
||||
<Fragment>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
key={entry.id}
|
||||
name={entry.title}
|
||||
icon={GrayDotIcon}
|
||||
name="Documentation"
|
||||
icon={BookIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://trigger.dev/docs"
|
||||
data-action="documentation"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<SideMenuItem
|
||||
name="Status"
|
||||
icon={RadarPulseIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://status.trigger.dev/"
|
||||
data-action="status"
|
||||
target="_blank"
|
||||
/>
|
||||
<SideMenuItem
|
||||
name="Suggest a feature"
|
||||
icon={BulbIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
to="https://feedback.trigger.dev/"
|
||||
data-action="suggest-a-feature"
|
||||
target="_blank"
|
||||
/>
|
||||
<Shortcuts />
|
||||
<Feedback
|
||||
button={
|
||||
<Button
|
||||
variant="small-menu-item"
|
||||
className="pl-2"
|
||||
LeadingIcon={EnvelopeIcon}
|
||||
leadingIconClassName="pr-1 text-text-dimmed group-hover/button:text-text-bright"
|
||||
data-action="contact-us"
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
>
|
||||
Contact us…
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 p-1">
|
||||
<Paragraph className="pb-1 pl-1.5 pt-1.5 text-xs">What's new</Paragraph>
|
||||
{changelogs.map((entry) => (
|
||||
<SideMenuItem
|
||||
key={entry.id}
|
||||
name={entry.title}
|
||||
icon={GrayDotIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-text-dimmed"
|
||||
activeIconColor="text-text-dimmed"
|
||||
to={entry.actionUrl ?? "https://trigger.dev/changelog"}
|
||||
target="_blank"
|
||||
/>
|
||||
))}
|
||||
<SideMenuItem
|
||||
name="Full changelog"
|
||||
icon={StarIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-text-dimmed"
|
||||
activeIconColor="text-text-dimmed"
|
||||
to={entry.actionUrl ?? "https://trigger.dev/changelog"}
|
||||
to="https://trigger.dev/changelog"
|
||||
data-action="full-changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
))}
|
||||
<SideMenuItem
|
||||
name="Full changelog"
|
||||
icon={StarIcon}
|
||||
trailingIcon={ArrowUpRightIcon}
|
||||
trailingIconClassName="text-text-dimmed"
|
||||
inactiveIconColor="text-text-dimmed"
|
||||
activeIconColor="text-text-dimmed"
|
||||
to="https://trigger.dev/changelog"
|
||||
data-action="full-changelog"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
</Fragment>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -374,8 +374,8 @@ export function SideMenu({
|
||||
{isConnected === undefined
|
||||
? "Checking connection…"
|
||||
: isConnected
|
||||
? "Your dev server is connected"
|
||||
: "Your dev server is not connected"}
|
||||
? "Your dev server is connected"
|
||||
: "Your dev server is not connected"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function SideMenuItem({
|
||||
icon={icon}
|
||||
className={cn(
|
||||
"size-5 shrink-0",
|
||||
isActive ? activeIconColor : inactiveIconColor ?? "text-text-dimmed",
|
||||
isActive ? activeIconColor : (inactiveIconColor ?? "text-text-dimmed"),
|
||||
!isActive &&
|
||||
!disableIconHover &&
|
||||
"group-hover/menuitem:text-text-bright group-hover/menulink:text-text-bright",
|
||||
|
||||
@@ -32,9 +32,7 @@ export function useReorderableList<T>({
|
||||
}) {
|
||||
const orderFetcher = useFetcher();
|
||||
|
||||
const [order, setOrder] = useState<string[]>(
|
||||
() => initialOrder ?? items.map(itemKey)
|
||||
);
|
||||
const [order, setOrder] = useState<string[]>(() => initialOrder ?? items.map(itemKey));
|
||||
|
||||
// Sync order when organizationId changes (component may not remount)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -367,12 +367,11 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={cn("flex", isDisabled && "cursor-default")}>
|
||||
{buttonElement}
|
||||
</span>
|
||||
<span className={cn("flex", isDisabled && "cursor-default")}>{buttonElement}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-1.5 py-1.5 pl-2.5 pr-2 text-xs text-text-bright">
|
||||
{props.tooltip} {props.shortcut && !props.hideShortcutKey && (
|
||||
{props.tooltip}{" "}
|
||||
{props.shortcut && !props.hideShortcutKey && (
|
||||
<ShortcutKey shortcut={props.shortcut} variant="medium" />
|
||||
)}
|
||||
</TooltipContent>
|
||||
@@ -401,7 +400,7 @@ export const LinkButton = ({
|
||||
...props
|
||||
}: LinkPropsType) => {
|
||||
const innerRef = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
|
||||
useShortcutKeys({
|
||||
shortcut: props.shortcut,
|
||||
action: () => {
|
||||
|
||||
@@ -184,7 +184,11 @@ export const Checkbox = forwardRef<HTMLInputElement, SimpleCheckboxProps>(
|
||||
<input
|
||||
type="checkbox"
|
||||
className={cn(
|
||||
props.disabled ? "cursor-not-allowed" : props.readOnly ? "cursor-default" : "cursor-pointer",
|
||||
props.disabled
|
||||
? "cursor-not-allowed"
|
||||
: props.readOnly
|
||||
? "cursor-default"
|
||||
: "cursor-pointer",
|
||||
"read-only:border-charcoal-650 disabled:border-charcoal-650 disabled:opacity-50 rounded-sm border border-charcoal-600 bg-transparent transition checked:!bg-indigo-500 read-only:!bg-charcoal-700 group-hover:bg-charcoal-900 group-hover:checked:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-none focus-visible:ring-indigo-500 disabled:!bg-charcoal-700"
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -42,8 +42,8 @@ const ClientTabs = React.forwardRef<
|
||||
valueProp !== undefined
|
||||
? { value: valueProp }
|
||||
: defaultValue !== undefined
|
||||
? { defaultValue }
|
||||
: {};
|
||||
? { defaultValue }
|
||||
: {};
|
||||
|
||||
const contextValue = React.useMemo<ClientTabsContextValue>(() => ({ value }), [value]);
|
||||
|
||||
@@ -193,11 +193,7 @@ const ClientTabsContent = React.forwardRef<
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
"mt-1 outline-none",
|
||||
className,
|
||||
"data-[state=inactive]:hidden"
|
||||
)}
|
||||
className={cn("mt-1 outline-none", className, "data-[state=inactive]:hidden")}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -16,18 +16,12 @@ export function CopyTextLink({ value, className }: CopyTextLinkProps) {
|
||||
onClick={copy}
|
||||
className={cn(
|
||||
"inline-flex cursor-pointer items-center gap-1 text-xs transition-colors",
|
||||
copied
|
||||
? "text-success"
|
||||
: "text-text-dimmed hover:text-text-bright",
|
||||
copied ? "text-success" : "text-text-dimmed hover:text-text-bright",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
{copied ? (
|
||||
<ClipboardCheckIcon className="size-3" />
|
||||
) : (
|
||||
<ClipboardIcon className="size-3" />
|
||||
)}
|
||||
{copied ? <ClipboardCheckIcon className="size-3" /> : <ClipboardIcon className="size-3" />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,10 +275,10 @@ const DateTimeAccurateInner = ({
|
||||
return hideDate
|
||||
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
|
||||
: realPrevDate
|
||||
? isSameDay(realDate, realPrevDate)
|
||||
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
|
||||
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12)
|
||||
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12);
|
||||
? isSameDay(realDate, realPrevDate)
|
||||
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
|
||||
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12)
|
||||
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12);
|
||||
}, [realDate, displayTimeZone, locales, hour12, hideDate, previousDate]);
|
||||
|
||||
if (!showTooltip)
|
||||
|
||||
@@ -122,5 +122,5 @@ export {
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogPortal,
|
||||
DialogOverlay
|
||||
DialogOverlay,
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ export function InputNumberStepper({
|
||||
const isMaxDisabled = max !== undefined && !Number.isNaN(numericValue) && numericValue >= max;
|
||||
|
||||
function clamp(val: number): number {
|
||||
if (Number.isNaN(val)) return typeof value === "number" ? value : min ?? 0;
|
||||
if (Number.isNaN(val)) return typeof value === "number" ? value : (min ?? 0);
|
||||
let next = val;
|
||||
if (min !== undefined) next = Math.max(min, next);
|
||||
if (max !== undefined) next = Math.min(max, next);
|
||||
|
||||
@@ -139,16 +139,9 @@ export function MiddleTruncate({ text, className }: MiddleTruncateProps) {
|
||||
}, [calculateTruncation]);
|
||||
|
||||
const content = (
|
||||
<span
|
||||
ref={containerRef}
|
||||
className={cn("block", isTruncated && "min-w-[360px]", className)}
|
||||
>
|
||||
<span ref={containerRef} className={cn("block", isTruncated && "min-w-[360px]", className)}>
|
||||
{/* Hidden span for measuring text width */}
|
||||
<span
|
||||
ref={measureRef}
|
||||
className="invisible absolute whitespace-nowrap"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span ref={measureRef} className="invisible absolute whitespace-nowrap" aria-hidden="true" />
|
||||
{displayText}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ export function SearchInput({
|
||||
const { value, replace, del } = useSearchParams();
|
||||
const isControlled = onValueChange !== undefined;
|
||||
|
||||
const initialSearch = isControlled ? controlledValue ?? "" : value(paramName) ?? "";
|
||||
const initialSearch = isControlled ? (controlledValue ?? "") : (value(paramName) ?? "");
|
||||
|
||||
const [text, setText] = useState(initialSearch);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
@@ -70,8 +70,10 @@ function isSection<TItem>(data: TItem[] | Section<TItem>[]): data is Section<TIt
|
||||
}
|
||||
|
||||
type ItemFromSection<TItemOrSection> = TItemOrSection extends Section<infer U> ? U : TItemOrSection;
|
||||
export interface SelectProps<TValue extends string | string[], TItem>
|
||||
extends Omit<Ariakit.SelectProps, "children"> {
|
||||
export interface SelectProps<TValue extends string | string[], TItem> extends Omit<
|
||||
Ariakit.SelectProps,
|
||||
"children"
|
||||
> {
|
||||
icon?: React.ReactNode;
|
||||
text?: React.ReactNode | ((value: TValue) => React.ReactNode);
|
||||
placeholder?: React.ReactNode;
|
||||
@@ -314,10 +316,10 @@ export function SelectTrigger({
|
||||
{(value) => (
|
||||
<>
|
||||
{typeof value === "string"
|
||||
? value ?? placeholder
|
||||
? (value ?? placeholder)
|
||||
: value.length === 0
|
||||
? placeholder
|
||||
: value.join(", ")}
|
||||
? placeholder
|
||||
: value.join(", ")}
|
||||
</>
|
||||
)}
|
||||
</SelectValue>
|
||||
@@ -376,8 +378,9 @@ export function SelectTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectProviderProps<TValue extends string | string[]>
|
||||
extends Ariakit.SelectProviderProps<TValue> {}
|
||||
export interface SelectProviderProps<
|
||||
TValue extends string | string[],
|
||||
> extends Ariakit.SelectProviderProps<TValue> {}
|
||||
export function SelectProvider<TValue extends string | string[]>(
|
||||
props: SelectProviderProps<TValue>
|
||||
) {
|
||||
@@ -469,9 +472,7 @@ export function SelectItem({
|
||||
// SelectLinkItem (which uses render to swap in a <Link>) get their
|
||||
// render prop silently dropped, which is why those rows looked
|
||||
// clickable but didn't navigate.
|
||||
const render = combobox
|
||||
? <Ariakit.ComboboxItem render={props.render} />
|
||||
: props.render;
|
||||
const render = combobox ? <Ariakit.ComboboxItem render={props.render} /> : props.render;
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const select = Ariakit.useSelectContext();
|
||||
const selectValue = select?.useState("value");
|
||||
|
||||
@@ -25,8 +25,7 @@ const portalVariants = cva("fixed inset-0 z-50 flex", {
|
||||
});
|
||||
|
||||
interface SheetPortalProps
|
||||
extends SheetPrimitive.DialogPortalProps,
|
||||
VariantProps<typeof portalVariants> {}
|
||||
extends SheetPrimitive.DialogPortalProps, VariantProps<typeof portalVariants> {}
|
||||
|
||||
const SheetPortal = ({ position, children, ...props }: SheetPortalProps) => (
|
||||
<SheetPrimitive.Portal {...props}>
|
||||
@@ -139,7 +138,8 @@ const sheetVariants = cva(
|
||||
);
|
||||
|
||||
export interface DialogContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
|
||||
@@ -48,7 +48,8 @@ const sheetVariants = cva(
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
|
||||
@@ -15,7 +15,10 @@ const medium =
|
||||
"justify-center min-w-[1.25rem] min-h-[1.25rem] text-[0.65rem] font-mono font-medium rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1.5 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase";
|
||||
|
||||
export const variants = {
|
||||
small: cn(small, "border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-text-dimmed/60"),
|
||||
small: cn(
|
||||
small,
|
||||
"border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-text-dimmed/60"
|
||||
),
|
||||
"small/bright": cn(small, "bg-charcoal-750 text-text-bright border-charcoal-650"),
|
||||
medium: cn(medium, "group-hover:border-charcoal-550"),
|
||||
"medium/bright": cn(medium, "bg-charcoal-750 text-text-bright border-charcoal-650"),
|
||||
|
||||
@@ -298,8 +298,8 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
|
||||
alignment === "left"
|
||||
? "justify-start text-left"
|
||||
: alignment === "center"
|
||||
? "justify-center text-center"
|
||||
: "justify-end text-right"
|
||||
? "justify-center text-center"
|
||||
: "justify-end text-right"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -456,7 +456,9 @@ export const TableCellMenu = forwardRef<
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { variant } = useContext(TableContext);
|
||||
const resolvedContent =
|
||||
typeof popoverContent === "function" ? popoverContent(() => setIsOpen(false)) : popoverContent;
|
||||
typeof popoverContent === "function"
|
||||
? popoverContent(() => setIsOpen(false))
|
||||
: popoverContent;
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
|
||||
@@ -207,9 +207,7 @@ export function TabButton({
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className={"text-sm transition duration-200 text-text-bright"}
|
||||
>
|
||||
<span className={"text-sm transition duration-200 text-text-bright"}>
|
||||
{props.children}
|
||||
</span>
|
||||
{shortcut && <ShortcutKey className={cn("")} shortcut={shortcut} variant={"small"} />}
|
||||
|
||||
@@ -114,7 +114,10 @@ export function ToastUI({
|
||||
<Action action={action} toastId={t} className="my-2" />
|
||||
</div>
|
||||
<button
|
||||
className={cn("-mr-1 ms-auto rounded p-2 text-text-dimmed transition hover:text-text-bright", title && "-mt-1")}
|
||||
className={cn(
|
||||
"-mr-1 ms-auto rounded p-2 text-text-dimmed transition hover:text-text-bright",
|
||||
title && "-mt-1"
|
||||
)}
|
||||
onClick={() => toast.dismiss(t)}
|
||||
>
|
||||
<XMarkIcon className="size-4" />
|
||||
|
||||
@@ -232,7 +232,10 @@ export function useTree<TData, TFilterValue>({
|
||||
if (selectedId === undefined) {
|
||||
dispatch({ type: "DESELECT_ALL_NODES" });
|
||||
} else {
|
||||
dispatch({ type: "SELECT_NODE", payload: { id: selectedId, scrollToNode: false, scrollToNodeFn } });
|
||||
dispatch({
|
||||
type: "SELECT_NODE",
|
||||
payload: { id: selectedId, scrollToNode: false, scrollToNodeFn },
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedId]);
|
||||
@@ -623,10 +626,13 @@ export function createTreeFromFlatItems<TData>(
|
||||
rootId: string
|
||||
): Tree<TData> | undefined {
|
||||
// Index items by id
|
||||
const indexedItems: { [id: string]: Tree<TData> } = withoutChildren.reduce((acc, item) => {
|
||||
acc[item.id] = { id: item.id, runId: item.runId, data: item.data, children: [] };
|
||||
return acc;
|
||||
}, {} as { [id: string]: Tree<TData> });
|
||||
const indexedItems: { [id: string]: Tree<TData> } = withoutChildren.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.id] = { id: item.id, runId: item.runId, data: item.data, children: [] };
|
||||
return acc;
|
||||
},
|
||||
{} as { [id: string]: Tree<TData> }
|
||||
);
|
||||
|
||||
// Add items to parent's children array
|
||||
withoutChildren.forEach((item) => {
|
||||
|
||||
@@ -53,14 +53,17 @@ export function concreteStateFromPartialState<TData>(
|
||||
tree: FlatTree<TData>,
|
||||
state: PartialNodeState
|
||||
): NodesState {
|
||||
const concreteState = tree.reduce((acc, node) => {
|
||||
acc[node.id] = {
|
||||
selected: acc[node.id]?.selected ?? defaultSelected,
|
||||
expanded: acc[node.id]?.expanded ?? defaultExpanded,
|
||||
visible: acc[node.id]?.visible ?? true,
|
||||
};
|
||||
return acc;
|
||||
}, state as Record<string, NodeState>);
|
||||
const concreteState = tree.reduce(
|
||||
(acc, node) => {
|
||||
acc[node.id] = {
|
||||
selected: acc[node.id]?.selected ?? defaultSelected,
|
||||
expanded: acc[node.id]?.expanded ?? defaultExpanded,
|
||||
visible: acc[node.id]?.visible ?? true,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
state as Record<string, NodeState>
|
||||
);
|
||||
|
||||
return applyVisibility(tree, concreteState);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { cn } from "~/utils/cn";
|
||||
import { type ParagraphVariant } from "./Paragraph";
|
||||
|
||||
const listVariants: Record<
|
||||
ParagraphVariant,
|
||||
{ text: string; spacing: string; items: string }
|
||||
> = {
|
||||
const listVariants: Record<ParagraphVariant, { text: string; spacing: string; items: string }> = {
|
||||
base: {
|
||||
text: "font-sans text-base font-normal text-text-dimmed",
|
||||
spacing: "mb-3",
|
||||
|
||||
@@ -164,83 +164,83 @@ const ChartTooltipContent = React.forwardRef<
|
||||
|
||||
return (
|
||||
<TooltipPortal active={active}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-grid-bright bg-background-bright px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-grid-bright bg-background-bright px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value != null && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{valueFormatter && typeof item.value === "number"
|
||||
? valueFormatter(item.value)
|
||||
: item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.value != null && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{valueFormatter && typeof item.value === "number"
|
||||
? valueFormatter(item.value)
|
||||
: item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,18 @@ export function ChartBarRenderer({
|
||||
width,
|
||||
height,
|
||||
}: ChartBarRendererProps) {
|
||||
const { config, data, dataKey, dataKeys, visibleSeries, state, highlight, setActivePayload, zoom, showLegend } = useChartContext();
|
||||
const {
|
||||
config,
|
||||
data,
|
||||
dataKey,
|
||||
dataKeys,
|
||||
visibleSeries,
|
||||
state,
|
||||
highlight,
|
||||
setActivePayload,
|
||||
zoom,
|
||||
showLegend,
|
||||
} = useChartContext();
|
||||
const hasNoData = useHasNoData();
|
||||
const zoomHandlers = useZoomHandlers();
|
||||
const enableZoom = zoom !== null;
|
||||
@@ -196,9 +207,7 @@ export function ChartBarRenderer({
|
||||
|
||||
{visibleSeries.map((key, index, array) => {
|
||||
const dimmed =
|
||||
!zoom?.isSelecting &&
|
||||
highlight.activeBarKey !== null &&
|
||||
highlight.activeBarKey !== key;
|
||||
!zoom?.isSelecting && highlight.activeBarKey !== null && highlight.activeBarKey !== key;
|
||||
|
||||
return (
|
||||
<Bar
|
||||
|
||||
@@ -104,17 +104,14 @@ export function ChartProvider({
|
||||
const [activePayload, setActivePayloadRaw] = useState<any[] | null>(null);
|
||||
const activeTooltipIndexRef = useRef<number | null>(null);
|
||||
|
||||
const setActivePayload = useCallback(
|
||||
(payload: any[] | null, tooltipIndex?: number | null) => {
|
||||
const idx = tooltipIndex ?? null;
|
||||
if (idx !== null && idx === activeTooltipIndexRef.current) {
|
||||
return;
|
||||
}
|
||||
activeTooltipIndexRef.current = idx;
|
||||
setActivePayloadRaw(payload);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const setActivePayload = useCallback((payload: any[] | null, tooltipIndex?: number | null) => {
|
||||
const idx = tooltipIndex ?? null;
|
||||
if (idx !== null && idx === activeTooltipIndexRef.current) {
|
||||
return;
|
||||
}
|
||||
activeTooltipIndexRef.current = idx;
|
||||
setActivePayloadRaw(payload);
|
||||
}, []);
|
||||
|
||||
// Reset the tooltip index ref when highlight resets (mouse leaves chart)
|
||||
const originalReset = highlight.reset;
|
||||
@@ -135,10 +132,7 @@ export function ChartProvider({
|
||||
[series, config, dataKey]
|
||||
);
|
||||
|
||||
const visibleSeries = useMemo(
|
||||
() => visibleSeriesProp ?? dataKeys,
|
||||
[visibleSeriesProp, dataKeys]
|
||||
);
|
||||
const visibleSeries = useMemo(() => visibleSeriesProp ?? dataKeys, [visibleSeriesProp, dataKeys]);
|
||||
|
||||
const value = useMemo<ChartContextValue>(
|
||||
() => ({
|
||||
@@ -155,7 +149,21 @@ export function ChartProvider({
|
||||
onZoomChange: enableZoom ? onZoomChange : undefined,
|
||||
showLegend,
|
||||
}),
|
||||
[config, data, dataKey, dataKeys, visibleSeries, state, labelFormatter, highlightWithReset, setActivePayload, zoomState, enableZoom, onZoomChange, showLegend]
|
||||
[
|
||||
config,
|
||||
data,
|
||||
dataKey,
|
||||
dataKeys,
|
||||
visibleSeries,
|
||||
state,
|
||||
labelFormatter,
|
||||
highlightWithReset,
|
||||
setActivePayload,
|
||||
zoomState,
|
||||
enableZoom,
|
||||
onZoomChange,
|
||||
showLegend,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -61,7 +61,8 @@ export function ChartLegendCompound({
|
||||
const totals = useSeriesTotal(aggregation);
|
||||
|
||||
// Derive the effective label from the aggregation type when no explicit label is provided
|
||||
const effectiveTotalLabel = totalLabel ?? (aggregation ? aggregationLabels[aggregation] : "Total");
|
||||
const effectiveTotalLabel =
|
||||
totalLabel ?? (aggregation ? aggregationLabels[aggregation] : "Total");
|
||||
|
||||
// Calculate grand total by aggregating across all per-series values
|
||||
const grandTotal = useMemo(() => {
|
||||
@@ -83,9 +84,7 @@ export function ChartLegendCompound({
|
||||
|
||||
const rawValues = dataKeys.map((key) => dataRow[key]);
|
||||
|
||||
const values = rawValues
|
||||
.filter((v): v is number => v != null)
|
||||
.map((v) => Number(v) || 0);
|
||||
const values = rawValues.filter((v): v is number => v != null).map((v) => Number(v) || 0);
|
||||
|
||||
// All null → gap-filled point, return null to show dash
|
||||
if (values.length === 0) return null;
|
||||
@@ -170,7 +169,11 @@ export function ChartLegendCompound({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col px-2 pb-2 pt-4 text-sm", scrollable && "max-h-[50%] min-h-0", className)}
|
||||
className={cn(
|
||||
"flex flex-col px-2 pb-2 pt-4 text-sm",
|
||||
scrollable && "max-h-[50%] min-h-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Total row */}
|
||||
<div
|
||||
@@ -329,7 +332,12 @@ type HoveredHiddenItemRowProps = {
|
||||
valueFormatter?: (value: number) => string;
|
||||
};
|
||||
|
||||
function HoveredHiddenItemRow({ item, value, remainingCount, valueFormatter }: HoveredHiddenItemRowProps) {
|
||||
function HoveredHiddenItemRow({
|
||||
item,
|
||||
value,
|
||||
remainingCount,
|
||||
valueFormatter,
|
||||
}: HoveredHiddenItemRowProps) {
|
||||
return (
|
||||
<div className="relative flex w-full items-center justify-between gap-2 rounded px-2 py-1">
|
||||
{/* Active highlight background */}
|
||||
|
||||
@@ -82,7 +82,17 @@ export function ChartLineRenderer({
|
||||
width,
|
||||
height,
|
||||
}: ChartLineRendererProps) {
|
||||
const { config, data, dataKey, dataKeys, visibleSeries, state, highlight, setActivePayload, showLegend } = useChartContext();
|
||||
const {
|
||||
config,
|
||||
data,
|
||||
dataKey,
|
||||
dataKeys,
|
||||
visibleSeries,
|
||||
state,
|
||||
highlight,
|
||||
setActivePayload,
|
||||
showLegend,
|
||||
} = useChartContext();
|
||||
const hasNoData = useHasNoData();
|
||||
const computedYAxisWidth = useYAxisWidth(data, visibleSeries, yAxisPropsProp?.tickFormatter);
|
||||
|
||||
@@ -218,11 +228,7 @@ export function ChartLineRenderer({
|
||||
<ChartTooltip
|
||||
cursor={{ stroke: "rgba(255, 255, 255, 0.1)", strokeWidth: 1 }}
|
||||
content={
|
||||
showLegend ? (
|
||||
() => null
|
||||
) : (
|
||||
<ChartTooltipContent valueFormatter={tooltipValueFormatter} />
|
||||
)
|
||||
showLegend ? () => null : <ChartTooltipContent valueFormatter={tooltipValueFormatter} />
|
||||
}
|
||||
labelFormatter={tooltipLabelFormatter}
|
||||
/>
|
||||
|
||||
@@ -123,65 +123,66 @@ function ChartBarLoadingBackground() {
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<ClientOnly fallback={<div />}>
|
||||
{() => (<>
|
||||
<motion.div
|
||||
className="flex flex-1 items-end gap-1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{Array.from({ length: 30 }).map((_, i) => {
|
||||
const height = Math.max(15, Math.floor(Math.random() * 100));
|
||||
return (
|
||||
{() => (
|
||||
<>
|
||||
<motion.div
|
||||
key={i}
|
||||
className="flex-1 rounded-sm bg-charcoal-750/50"
|
||||
style={{ height: `${height}%` }}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: `${height}%`, opacity: 1 }}
|
||||
className="flex flex-1 items-end gap-1"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
{Array.from({ length: 30 }).map((_, i) => {
|
||||
const height = Math.max(15, Math.floor(Math.random() * 100));
|
||||
return (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="flex-1 rounded-sm bg-charcoal-750/50"
|
||||
style={{ height: `${height}%` }}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: `${height}%`, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 120,
|
||||
damping: 14,
|
||||
mass: 1,
|
||||
delay: 0.1 + i * 0.02,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="mt-2 flex flex-col justify-center gap-1"
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 120,
|
||||
damping: 14,
|
||||
mass: 1,
|
||||
delay: 0.1 + i * 0.02,
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
delay: 0.2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="mt-2 flex flex-col justify-center gap-1"
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 15,
|
||||
delay: 0.2,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<motion.div
|
||||
className="flex items-center gap-1"
|
||||
key={i}
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 12,
|
||||
delay: 0.3 + i * 0.08,
|
||||
}}
|
||||
>
|
||||
<div className="h-5 w-2 rounded-sm bg-charcoal-750/50" />
|
||||
<div className="h-5 w-full rounded-sm bg-charcoal-750/50" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</>)}
|
||||
</ClientOnly>
|
||||
|
||||
>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<motion.div
|
||||
className="flex items-center gap-1"
|
||||
key={i}
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 100,
|
||||
damping: 12,
|
||||
delay: 0.3 + i * 0.08,
|
||||
}}
|
||||
>
|
||||
<div className="h-5 w-2 rounded-sm bg-charcoal-750/50" />
|
||||
<div className="h-5 w-full rounded-sm bg-charcoal-750/50" />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</ClientOnly>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,13 +162,7 @@ function ChartRootInner({
|
||||
const enableZoom = zoom !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-col",
|
||||
fillContainer && "h-full",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cn("relative flex w-full flex-col", fillContainer && "h-full", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
fillContainer ? "min-h-0 flex-1" : "h-full w-full",
|
||||
@@ -182,7 +176,7 @@ function ChartRootInner({
|
||||
"h-full w-full",
|
||||
fillContainer && "!aspect-auto",
|
||||
enableZoom &&
|
||||
"[&_.recharts-surface]:cursor-crosshair [&_.recharts-wrapper]:cursor-crosshair"
|
||||
"[&_.recharts-surface]:cursor-crosshair [&_.recharts-wrapper]:cursor-crosshair"
|
||||
)}
|
||||
style={fillContainer ? undefined : minHeight ? { minHeight } : undefined}
|
||||
>
|
||||
|
||||
@@ -186,6 +186,15 @@ export function useZoomSelection(): UseZoomSelectionReturn {
|
||||
clearInspectionLine,
|
||||
reset,
|
||||
}),
|
||||
[state, startSelection, updateSelection, finishSelection, cancelSelection, toggleInspectionLine, clearInspectionLine, reset]
|
||||
[
|
||||
state,
|
||||
startSelection,
|
||||
updateSelection,
|
||||
finishSelection,
|
||||
cancelSelection,
|
||||
toggleInspectionLine,
|
||||
clearInspectionLine,
|
||||
reset,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ export function useYAxisWidth(
|
||||
}
|
||||
}
|
||||
|
||||
const fmt = tickFormatter ?? ((v: any) => (typeof v === "number" ? v.toLocaleString() : String(v)));
|
||||
const fmt =
|
||||
tickFormatter ?? ((v: any) => (typeof v === "number" ? v.toLocaleString() : String(v)));
|
||||
const label = fmt(max, 0);
|
||||
// Add one char of slack because recharts "nices" the domain up beyond data max.
|
||||
const charCount = label.length + 1;
|
||||
|
||||
@@ -284,11 +284,7 @@ const QueryEditorForm = forwardRef<
|
||||
Explain
|
||||
</Button>
|
||||
)}
|
||||
<ScopeFilter
|
||||
value={scope}
|
||||
onValueChange={setScope}
|
||||
shortcut={{ key: "e" }}
|
||||
/>
|
||||
<ScopeFilter value={scope} onValueChange={setScope} shortcut={{ key: "e" }} />
|
||||
{queryHasTriggeredAt ? (
|
||||
<SimpleTooltip
|
||||
asChild
|
||||
@@ -425,7 +421,7 @@ export function QueryEditor({
|
||||
const isTitleLoading = titleFetcher.state !== "idle";
|
||||
const generatedTitle = titleFetcher.data?.title;
|
||||
const [historyTitle, setHistoryTitle] = useState<string | null>(
|
||||
history.length > 0 ? history[0].title ?? null : null
|
||||
history.length > 0 ? (history[0].title ?? null) : null
|
||||
);
|
||||
|
||||
// For edit mode, use the widget name as initial title
|
||||
@@ -439,8 +435,8 @@ export function QueryEditor({
|
||||
const queryTitle =
|
||||
userTitle ??
|
||||
(mode.type === "dashboard-edit"
|
||||
? editModeTitle ?? historyTitle ?? generatedTitle ?? null
|
||||
: historyTitle ?? generatedTitle ?? null);
|
||||
? (editModeTitle ?? historyTitle ?? generatedTitle ?? null)
|
||||
: (historyTitle ?? generatedTitle ?? null));
|
||||
|
||||
// Track if user has manually set a title (disables AI regeneration)
|
||||
const hasUserTitle = userTitle !== null;
|
||||
@@ -572,8 +568,8 @@ export function QueryEditor({
|
||||
resultsView === "table"
|
||||
? { type: "table", prettyFormatting, sorting: [] }
|
||||
: resultsView === "bignumber"
|
||||
? { type: "bignumber", ...bigNumberConfig }
|
||||
: { type: "chart", ...chartConfig },
|
||||
? { type: "bignumber", ...bigNumberConfig }
|
||||
: { type: "chart", ...chartConfig },
|
||||
};
|
||||
|
||||
// Render NavBar based on mode
|
||||
@@ -959,8 +955,8 @@ export function QueryEditor({
|
||||
resultsView === "table"
|
||||
? { type: "table", prettyFormatting, sorting: [] }
|
||||
: resultsView === "bignumber"
|
||||
? { type: "bignumber", ...bigNumberConfig }
|
||||
: { type: "chart", ...chartConfig }
|
||||
? { type: "bignumber", ...bigNumberConfig }
|
||||
: { type: "chart", ...chartConfig }
|
||||
}
|
||||
isOpen={isSaveDialogOpen}
|
||||
onOpenChange={setIsSaveDialogOpen}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
XCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { CheckCircleIcon, ExclamationTriangleIcon, XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import type { BatchTaskRunStatus } from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
|
||||
@@ -73,7 +73,9 @@ export function PromptSpanDetails({
|
||||
<div className="scrollbar-gutter-stable min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
{tab === "overview" && (
|
||||
<div className="flex flex-col px-3">
|
||||
{startTime && <SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} />}
|
||||
{startTime && (
|
||||
<SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} />
|
||||
)}
|
||||
<div className="flex flex-col gap-1 py-2.5">
|
||||
<div className="flex flex-col text-xs @container">
|
||||
<MetricRow
|
||||
@@ -132,9 +134,7 @@ export function PromptSpanDetails({
|
||||
<div className="rounded-md border border-grid-bright bg-charcoal-750/50 px-3.5 py-2">
|
||||
<div className="font-sans text-sm font-normal text-text-dimmed streamdown-container">
|
||||
<Suspense
|
||||
fallback={
|
||||
<span className="whitespace-pre-wrap">{promptData.template!}</span>
|
||||
}
|
||||
fallback={<span className="whitespace-pre-wrap">{promptData.template!}</span>}
|
||||
>
|
||||
<StreamdownRenderer>{promptData.template!}</StreamdownRenderer>
|
||||
</Suspense>
|
||||
@@ -146,4 +146,3 @@ export function PromptSpanDetails({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,12 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) return <Squares2X2Icon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
if (!name)
|
||||
return (
|
||||
<Squares2X2Icon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
if (tablerIcons.has(name)) {
|
||||
return <TablerIcon name={name} className={className} />;
|
||||
}
|
||||
@@ -79,13 +84,25 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
||||
case "scheduled":
|
||||
return <ClockIcon className={cn(className, "text-schedules")} />;
|
||||
case "attempt":
|
||||
return <AttemptIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<AttemptIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "wait":
|
||||
return <PauseIcon className={cn(className, "text-sky-500")} />;
|
||||
case "trace":
|
||||
return <TraceIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<TraceIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "tag":
|
||||
return <TagIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<TagIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "queue":
|
||||
return <RectangleStackIcon className={cn(className, "text-queues")} />;
|
||||
case "trigger":
|
||||
@@ -95,14 +112,22 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
||||
case "wait-token":
|
||||
return <WaitpointTokenIcon className={cn(className, "text-sky-500")} />;
|
||||
case "function":
|
||||
return <FunctionIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<FunctionIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "query":
|
||||
return <TableCellsIcon className={cn(className, "text-query")} />;
|
||||
//log levels
|
||||
case "debug":
|
||||
case "log":
|
||||
case "info":
|
||||
return <InfoIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<InfoIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "warn":
|
||||
return <InfoIcon className={cn(className, "text-amber-400")} />;
|
||||
case "error":
|
||||
@@ -110,9 +135,17 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
||||
case "fatal":
|
||||
return <HandRaisedIcon className={cn(className, "text-error")} />;
|
||||
case "task-middleware":
|
||||
return <MiddlewareIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<MiddlewareIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "task-fn-run":
|
||||
return <RunFunctionIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<RunFunctionIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "task-hook-init":
|
||||
case "task-hook-onStart":
|
||||
case "task-hook-onStartAttempt":
|
||||
@@ -122,40 +155,98 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) {
|
||||
case "task-hook-onComplete":
|
||||
case "task-hook-cleanup":
|
||||
case "task-hook-onCancel":
|
||||
return <FunctionIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<FunctionIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "task-hook-onFailure":
|
||||
case "task-hook-catchError":
|
||||
return <FunctionIcon className={cn(className, "text-error")} />;
|
||||
case "streams":
|
||||
return <StreamsIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<StreamsIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "hero-sparkles":
|
||||
return <SparklesIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<SparklesIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "hero-wrench":
|
||||
return <WrenchIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<WrenchIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "tabler-brand-anthropic":
|
||||
case "ai-provider-anthropic":
|
||||
return <AnthropicIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<AnthropicIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-openai":
|
||||
return <OpenAIIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<OpenAIIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-gemini":
|
||||
return <GeminiIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<GeminiIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-llama":
|
||||
return <LlamaIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<LlamaIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-deepseek":
|
||||
return <DeepseekIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<DeepseekIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-xai":
|
||||
return <XAIIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<XAIIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-perplexity":
|
||||
return <PerplexityIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<PerplexityIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-cerebras":
|
||||
return <CerebrasIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<CerebrasIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-mistral":
|
||||
return <MistralIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<MistralIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
case "ai-provider-azure":
|
||||
return <AzureIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<AzureIcon
|
||||
className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <InfoIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />;
|
||||
return (
|
||||
<InfoIcon className={cn(className, "text-text-dimmed group-hover/spannode:text-text-bright")} />
|
||||
);
|
||||
}
|
||||
|
||||
function TablerIcon({ name, className }: { name: string; className?: string }) {
|
||||
|
||||
@@ -329,8 +329,8 @@ export function timeFilterRenderValues({
|
||||
rangeType === "range" || rangeType === "period"
|
||||
? labelName
|
||||
: rangeType === "from"
|
||||
? `${labelName} after`
|
||||
: `${labelName} before`;
|
||||
? `${labelName} after`
|
||||
: `${labelName} before`;
|
||||
|
||||
return { label, valueLabel, rangeType };
|
||||
}
|
||||
@@ -379,9 +379,9 @@ export function TimeFilter({
|
||||
// state, so don't fall back to the URL — otherwise selecting a custom date range (which
|
||||
// sets period to undefined) would read the page-level URL period and override the range.
|
||||
const controlled = onValueChange !== undefined;
|
||||
const periodValue = controlled ? period : period ?? value("period");
|
||||
const fromValue = controlled ? from : from ?? value("from");
|
||||
const toValue = controlled ? to : to ?? value("to");
|
||||
const periodValue = controlled ? period : (period ?? value("period"));
|
||||
const fromValue = controlled ? from : (from ?? value("from"));
|
||||
const toValue = controlled ? to : (to ?? value("to"));
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useShortcutKeys({
|
||||
@@ -503,7 +503,7 @@ export function TimeDropdown({
|
||||
const isInitialCustom =
|
||||
period && !timePeriods.some((p) => p.value === period) && initialCustom.value !== "";
|
||||
const [selectedPeriod, setSelectedPeriod] = useState<string>(
|
||||
isInitialCustom ? "custom" : period ?? defaultPeriod
|
||||
isInitialCustom ? "custom" : (period ?? defaultPeriod)
|
||||
);
|
||||
|
||||
// Custom duration state
|
||||
@@ -517,7 +517,7 @@ export function TimeDropdown({
|
||||
setCustomUnit(parsed.unit);
|
||||
|
||||
const isCustom = period && !timePeriods.some((p) => p.value === period) && parsed.value !== "";
|
||||
setSelectedPeriod(isCustom ? "custom" : period ?? defaultPeriod);
|
||||
setSelectedPeriod(isCustom ? "custom" : (period ?? defaultPeriod));
|
||||
setActiveSection(from || to ? "dateRange" : "duration");
|
||||
}, [period, from, to, defaultPeriod]);
|
||||
|
||||
|
||||
@@ -32,9 +32,7 @@ export function SpanTitle(event: SpanTitleProps) {
|
||||
finalTextClass === "text-text-dimmed" ? "group-hover/spannode:text-text-bright" : undefined;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("flex items-center gap-x-2 overflow-x-hidden", finalTextClass, hoverClass)}
|
||||
>
|
||||
<span className={cn("flex items-center gap-x-2 overflow-x-hidden", finalTextClass, hoverClass)}>
|
||||
<span className="truncate">{event.message}</span>{" "}
|
||||
{!event.hideAccessory && (
|
||||
<SpanAccessory accessory={event.style.accessory} size={event.size} />
|
||||
|
||||
@@ -65,7 +65,7 @@ export function WaitpointDetailTable({
|
||||
<div>
|
||||
<div>
|
||||
{waitpoint.userProvidedIdempotencyKey
|
||||
? waitpoint.inactiveIdempotencyKey ?? waitpoint.idempotencyKey
|
||||
? (waitpoint.inactiveIdempotencyKey ?? waitpoint.idempotencyKey)
|
||||
: "–"}
|
||||
</div>
|
||||
<div>
|
||||
@@ -99,8 +99,8 @@ export function WaitpointDetailTable({
|
||||
{waitpoint.status === "TIMED_OUT"
|
||||
? "The waitpoint timed out"
|
||||
: waitpoint.status === "COMPLETED"
|
||||
? "The waitpoint completed before this timeout was reached"
|
||||
: "The waitpoint is still waiting"}
|
||||
? "The waitpoint completed before this timeout was reached"
|
||||
: "The waitpoint is still waiting"}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property.Value>
|
||||
|
||||
@@ -320,7 +320,9 @@ function useAgentSessionMessages({
|
||||
if (!res) return false;
|
||||
for (const [mid, msg] of pendingRef.current) {
|
||||
const parts = (msg.parts ?? []) as Array<Record<string, unknown>>;
|
||||
const idx = parts.findIndex((p) => (p as { toolCallId?: string }).toolCallId === toolCallId);
|
||||
const idx = parts.findIndex(
|
||||
(p) => (p as { toolCallId?: string }).toolCallId === toolCallId
|
||||
);
|
||||
if (idx < 0) continue;
|
||||
const cur = parts[idx]!;
|
||||
const terminal =
|
||||
@@ -424,7 +426,7 @@ function useAgentSessionMessages({
|
||||
signal: abort.signal,
|
||||
timeoutInSeconds: 120,
|
||||
...(lastEventId !== undefined ? { lastEventId } : {}),
|
||||
} as const);
|
||||
}) as const;
|
||||
|
||||
const commonSubOptions = {
|
||||
signal: abort.signal,
|
||||
@@ -600,7 +602,11 @@ function useAgentSessionMessages({
|
||||
|
||||
// New user turns — merge in (dedupe by id).
|
||||
for (const m of candidates) {
|
||||
if (m == null || (m as { role?: string }).role !== "user" || typeof m.id !== "string") {
|
||||
if (
|
||||
m == null ||
|
||||
(m as { role?: string }).role !== "user" ||
|
||||
typeof m.id !== "string"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (pendingRef.current.has(m.id)) continue;
|
||||
|
||||
@@ -473,8 +473,8 @@ function SubAgentContent({ parts }: { parts: any[] }) {
|
||||
part.state === "input-streaming" || part.state === "input-available"
|
||||
? "calling..."
|
||||
: part.state === "output-error"
|
||||
? `error: ${part.errorText ?? "unknown"}`
|
||||
: undefined,
|
||||
? `error: ${part.errorText ?? "unknown"}`
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -63,4 +63,3 @@ export function AIEmbedSpanDetails({ data }: { data: AIEmbedData }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,10 @@ export function AITagsRow({ aiData }: { aiData: AISpanData }) {
|
||||
<div className="flex flex-col gap-1 py-2.5">
|
||||
<div className="flex flex-col text-xs @container">
|
||||
{aiData.responseId && (
|
||||
<MetricRow label="Response ID" value={<TruncatedCopyableValue value={aiData.responseId} />} />
|
||||
<MetricRow
|
||||
label="Response ID"
|
||||
value={<TruncatedCopyableValue value={aiData.responseId} />}
|
||||
/>
|
||||
)}
|
||||
<MetricRow label="Model" value={aiData.model} />
|
||||
{aiData.provider !== "unknown" && <MetricRow label="Provider" value={aiData.provider} />}
|
||||
@@ -145,4 +148,3 @@ function formatTtfc(ms: number): string {
|
||||
}
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,13 @@ export function AISpanDetails({
|
||||
? {
|
||||
slug: aiData.promptSlug,
|
||||
version: aiData.promptVersion,
|
||||
path: v3PromptPath(organization, project, environment, aiData.promptSlug, aiData.promptVersion),
|
||||
path: v3PromptPath(
|
||||
organization,
|
||||
project,
|
||||
environment,
|
||||
aiData.promptSlug,
|
||||
aiData.promptVersion
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -106,7 +112,11 @@ export function AISpanDetails({
|
||||
<div className="scrollbar-gutter-stable min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
{tab === "overview" && (
|
||||
<>
|
||||
{startTime && <div className="px-3"><SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} /></div>}
|
||||
{startTime && (
|
||||
<div className="px-3">
|
||||
<SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} />
|
||||
</div>
|
||||
)}
|
||||
<OverviewTab aiData={aiData} />
|
||||
</>
|
||||
)}
|
||||
@@ -168,13 +178,7 @@ function OverviewTab({ aiData }: { aiData: AISpanData }) {
|
||||
);
|
||||
}
|
||||
|
||||
function MessagesTab({
|
||||
aiData,
|
||||
promptLink,
|
||||
}: {
|
||||
aiData: AISpanData;
|
||||
promptLink?: PromptLink;
|
||||
}) {
|
||||
function MessagesTab({ aiData, promptLink }: { aiData: AISpanData; promptLink?: PromptLink }) {
|
||||
const showFallbackText = aiData.responseText && !hasAssistantItem(aiData.items);
|
||||
const showFallbackObject =
|
||||
!showFallbackText && aiData.responseObject && !hasAssistantItem(aiData.items);
|
||||
@@ -238,7 +242,10 @@ function PromptTab({
|
||||
promptLink?: PromptLink;
|
||||
}) {
|
||||
const labels = promptData.labels
|
||||
? promptData.labels.split(",").map((l) => l.trim()).filter(Boolean)
|
||||
? promptData.labels
|
||||
.split(",")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
|
||||
return (
|
||||
@@ -295,7 +302,6 @@ function PromptTab({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -73,4 +73,3 @@ export function AIToolCallSpanDetails({ data }: { data: AIToolCallData }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,14 @@ export function formatDuration(ms: number): string {
|
||||
* Parse provider metadata from a JSON string.
|
||||
* Handles Anthropic, Azure, OpenAI, Gateway, and OpenRouter formats.
|
||||
*/
|
||||
export function parseProviderMetadata(
|
||||
raw: unknown
|
||||
): {
|
||||
serviceTier?: string;
|
||||
resolvedProvider?: string;
|
||||
gatewayCost?: string;
|
||||
responseId?: string;
|
||||
} | undefined {
|
||||
export function parseProviderMetadata(raw: unknown):
|
||||
| {
|
||||
serviceTier?: string;
|
||||
resolvedProvider?: string;
|
||||
gatewayCost?: string;
|
||||
responseId?: string;
|
||||
}
|
||||
| undefined {
|
||||
if (typeof raw !== "string") return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
|
||||
@@ -234,9 +234,7 @@ function parseMessagesToDisplayItems(raw: unknown): DisplayItem[] | undefined {
|
||||
});
|
||||
|
||||
const result = resultMsg
|
||||
? extractToolResults(resultMsg.content).find(
|
||||
(r) => r.toolCallId === tc.toolCallId
|
||||
)
|
||||
? extractToolResults(resultMsg.content).find((r) => r.toolCallId === tc.toolCallId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
@@ -430,9 +428,7 @@ function parseToolDefinitions(raw: unknown): ToolDefinition[] | undefined {
|
||||
name,
|
||||
description: str(o.description),
|
||||
parametersJson:
|
||||
schema && typeof schema === "object"
|
||||
? JSON.stringify(schema, null, 2)
|
||||
: undefined,
|
||||
schema && typeof schema === "object" ? JSON.stringify(schema, null, 2) : undefined,
|
||||
});
|
||||
}
|
||||
return defs.length > 0 ? defs : undefined;
|
||||
@@ -684,4 +680,3 @@ function appendGenAiMessages(items: DisplayItem[], messages: GenAiMessage[]): vo
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,8 @@ export function extractAISummarySpanData(
|
||||
const operationName = str(ai.operationId) ?? "";
|
||||
|
||||
// Token usage
|
||||
const inputTokens =
|
||||
num(aiUsage.inputTokens) ?? num(aiUsage.promptTokens) ?? 0;
|
||||
const outputTokens =
|
||||
num(aiUsage.outputTokens) ?? num(aiUsage.completionTokens) ?? 0;
|
||||
const inputTokens = num(aiUsage.inputTokens) ?? num(aiUsage.promptTokens) ?? 0;
|
||||
const outputTokens = num(aiUsage.outputTokens) ?? num(aiUsage.completionTokens) ?? 0;
|
||||
const totalTokens = num(aiUsage.totalTokens) ?? inputTokens + outputTokens;
|
||||
|
||||
const tokensPerSecond =
|
||||
@@ -63,7 +61,9 @@ export function extractAISummarySpanData(
|
||||
|
||||
// Parse the prompt JSON to build display items
|
||||
const promptJson = str(ai.prompt);
|
||||
const items = promptJson ? parsePromptToDisplayItems(promptJson, str(aiResponse.text)) : undefined;
|
||||
const items = promptJson
|
||||
? parsePromptToDisplayItems(promptJson, str(aiResponse.text))
|
||||
: undefined;
|
||||
|
||||
// Count messages from the parsed prompt
|
||||
let messageCount: number | undefined;
|
||||
@@ -194,4 +194,3 @@ function extractMessageContent(content: unknown): string | undefined {
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,7 @@ import { useFetcher } from "@remix-run/react";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { Fieldset } from "~/components/primitives/Fieldset";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
|
||||
@@ -52,9 +52,7 @@ export function ScheduleLimitActions({
|
||||
maxQuota={maxScheduleQuota}
|
||||
planScheduleLimit={planScheduleLimit}
|
||||
triggerButton={
|
||||
variant === "dialog" ? (
|
||||
<Button variant="primary/small">Purchase more…</Button>
|
||||
) : undefined
|
||||
variant === "dialog" ? <Button variant="primary/small">Purchase more…</Button> : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -202,8 +202,8 @@ function SessionDuration({ session }: { session: SessionListItem }) {
|
||||
session.status === "CLOSED"
|
||||
? session.closedAt
|
||||
: session.status === "EXPIRED"
|
||||
? session.expiresAt
|
||||
: undefined;
|
||||
? session.expiresAt
|
||||
: undefined;
|
||||
|
||||
if (endedAt) {
|
||||
return (
|
||||
|
||||
@@ -114,10 +114,7 @@ async function $transactionInner<R>(
|
||||
|
||||
export { Prisma };
|
||||
|
||||
function tagDatasource<T extends PrismaClient>(
|
||||
datasource: "writer" | "replica",
|
||||
client: T
|
||||
): T {
|
||||
function tagDatasource<T extends PrismaClient>(datasource: "writer" | "replica", client: T): T {
|
||||
return client.$extends({
|
||||
name: "datasource-tagger",
|
||||
query: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user