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:
Chris Arderne
2026-06-26 13:02:53 +01:00
committed by GitHub
parent 1d6f9002e9
commit 4fde283e76
515 changed files with 6507 additions and 5633 deletions
-1
View File
@@ -12,7 +12,6 @@
"sortPackageJson": false,
"ignorePatterns": [
"node_modules",
"apps/webapp",
".env",
".env.local",
"pnpm-lock.yaml",
+1 -2
View File
@@ -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
+1 -7
View File
@@ -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>
);
}
+2 -16
View File
@@ -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"
+1 -6
View File
@@ -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"
+1 -9
View File
@@ -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" />
+20 -6
View File
@@ -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>
);
}
+1 -4
View File
@@ -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);
}
}
+1 -5
View File
@@ -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>
);
+4 -4
View File
@@ -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>
+4 -1
View File
@@ -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();
+2 -8
View File
@@ -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. Youll 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. Youll 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>
);
}
+114 -23
View File
@@ -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 (
+1 -4
View File
@@ -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