Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfe4b5409a | |||
| 223768a025 | |||
| 1fd26ff98b | |||
| 8d34c63c2d | |||
| ff04c986a1 | |||
| 7d6430dc46 | |||
| b289aa31da | |||
| 0508de6258 | |||
| 56a063e586 | |||
| d490bc5cbf | |||
| 6d9dfbc75d | |||
| 6406924b02 | |||
| 2156e15266 | |||
| 49b4d47726 | |||
| 2e4f5d893c | |||
| 203e002087 | |||
| 1670c4c41d | |||
| 9d6c424236 | |||
| cffc88aeac | |||
| ae13757703 | |||
| 0ee39e6101 | |||
| bce398b892 | |||
| e667028d4a | |||
| ae99807047 | |||
| bf4e505c70 | |||
| c8538c236c | |||
| ff6e2461d5 | |||
| 3ed016bc2b | |||
| 68dd3804a7 | |||
| 95f0c7b780 | |||
| 339c39d739 | |||
| bc0ca01319 | |||
| c37c822314 | |||
| 2dfe191c55 | |||
| e337b21650 | |||
| 9e5382951b |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/shopify": patch
|
||||
---
|
||||
|
||||
improved error messages when a shopify webhook fails to register
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Remove "log" Log Level, unify log and info messages under the "info" log level
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Strip out server-only package from worker builds
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add runs.retrieve management API method to get info about a run by run ID
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Adding some additional telemetry during deploy to help debug issues
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add the "log" level back in as an alias to "info"
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add configure function to be able to configure the SDK manually
|
||||
@@ -47,18 +47,22 @@
|
||||
"angry-eagles-trade",
|
||||
"beige-pens-dance",
|
||||
"big-tomatoes-deliver",
|
||||
"blue-pumas-whisper",
|
||||
"breezy-gorillas-mate",
|
||||
"chilled-hornets-move",
|
||||
"clean-pianos-listen",
|
||||
"clever-apes-collect",
|
||||
"clever-carrots-travel",
|
||||
"clever-donkeys-hunt",
|
||||
"cool-glasses-bake",
|
||||
"cuddly-feet-approve",
|
||||
"dry-walls-check",
|
||||
"dull-mangos-press",
|
||||
"eight-pumas-float",
|
||||
"eleven-paws-join",
|
||||
"few-students-share",
|
||||
"funny-swans-destroy",
|
||||
"gorgeous-gorillas-compete",
|
||||
"green-bags-wink",
|
||||
"khaki-apricots-design",
|
||||
"khaki-poems-lay",
|
||||
@@ -68,7 +72,9 @@
|
||||
"light-bulldogs-press",
|
||||
"light-dragons-complain",
|
||||
"little-crabs-cross",
|
||||
"long-fireants-search",
|
||||
"loud-actors-remember",
|
||||
"lovely-drinks-flash",
|
||||
"many-ligers-pump",
|
||||
"mighty-camels-joke",
|
||||
"mighty-flowers-train",
|
||||
@@ -76,10 +82,12 @@
|
||||
"new-rivers-tell",
|
||||
"ninety-pets-travel",
|
||||
"odd-poets-own",
|
||||
"pink-pumas-rhyme",
|
||||
"polite-ducks-switch",
|
||||
"polite-rockets-matter",
|
||||
"poor-flowers-cross",
|
||||
"purple-garlics-shop",
|
||||
"rare-lamps-promise",
|
||||
"rare-roses-float",
|
||||
"real-planets-stare",
|
||||
"rich-kangaroos-unite",
|
||||
@@ -91,6 +99,7 @@
|
||||
"sharp-zebras-serve",
|
||||
"shiny-coats-cry",
|
||||
"silly-suits-switch",
|
||||
"six-ligers-exist",
|
||||
"slow-buses-own",
|
||||
"smart-needles-move",
|
||||
"smart-olives-eat",
|
||||
@@ -102,7 +111,9 @@
|
||||
"swift-dragons-peel",
|
||||
"tall-bees-wave",
|
||||
"tame-guests-know",
|
||||
"tender-moose-tell",
|
||||
"tender-oranges-rhyme",
|
||||
"thin-parents-heal",
|
||||
"tidy-balloons-suffer",
|
||||
"tidy-dryers-sleep",
|
||||
"tiny-doors-type",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add a postInstall option to allow running scripts after dependencies have been installed in deployed images
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Improve the display of non-object return types in the run trace viewer
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Use locked package versions when resolving dependencies in deployed workers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Ensure @trigger.dev/sdk and @trigger.dev/core are always in the list of deployed dependencies
|
||||
@@ -5,7 +5,7 @@
|
||||
<img alt="Trigger.dev logo" src="https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/a45d1fa2-0ae8-4a39-4409-f4f934bfae00/public">
|
||||
</picture>
|
||||
|
||||
### The open source background jobs framework
|
||||
### The open source background jobs platform
|
||||
|
||||
[Discord](https://trigger.dev/discord) | [Website](https://trigger.dev) | [Issues](https://github.com/triggerdotdev/trigger.dev/issues) | [Docs](https://trigger.dev/docs)
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
# About Trigger.dev
|
||||
> The Trigger.dev v3 developer preview is now open. For more information and to get early access, check out our [developer preview launch post](https://trigger.dev/blog/v3-developer-preview-launch/).
|
||||
|
||||
## About Trigger.dev
|
||||
|
||||
Create long-running jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.
|
||||
|
||||
@@ -48,30 +50,6 @@ View every Task in every Run so you can tell exactly what happened.
|
||||
|
||||
Easily integrate with hundreds of third-party APIs – including your own. Use API keys (which never leave your server) or let us handle OAuth for you. Install our integration packages and easily subscribe to webhooks and perform common tasks, or you can easily use your existing favorite Node.JS SDKs and get resumability and idempotency through our `runTask` function.
|
||||
|
||||
## Our progress
|
||||
|
||||
We’re building the most comprehensive and easy-to-use background jobs framework for developers.
|
||||
|
||||
Click the links to join the discussions about our upcoming features.
|
||||
|
||||
| Feature | What it does | Status |
|
||||
| ------------------------------------------------------------------------------------ | --------------------------------------------------- | ------ |
|
||||
| Integration kit | Official Trigger.dev integrations or build your own | ✅ |
|
||||
| Self-hosting | Host the platform yourself | ✅ |
|
||||
| Cloud | Just write code, no deployment required | ✅ |
|
||||
| Dashboard | View every Task in every Run | ✅ |
|
||||
| Serverless | Long-running Jobs on your serverless backend | ✅ |
|
||||
| React hooks | Easily update your UI with Job progress | ✅ |
|
||||
| React frameworks | Support for Remix, Astro, RedwoodJS & more | ✅ |
|
||||
| [Background tasks](https://github.com/triggerdotdev/trigger.dev/discussions/400) | Offload long or intense Tasks to our infrastructure | 🛠️ |
|
||||
| [Long-running servers](https://github.com/triggerdotdev/trigger.dev/discussions/430) | Run Jobs on your long-running backend | 🛠️ |
|
||||
| Polling Triggers | Subscribe to changes without webhooks | 🕝 |
|
||||
| Vercel integration | Easy deploy and preview environment support | 🕝 |
|
||||
| Streaming | Receive data from your Jobs in realtime | 🕝 |
|
||||
| 100+ integrations | Comprehensive support for popular APIs | 🕝 |
|
||||
| [Trigger.dev Connect](https://github.com/triggerdotdev/trigger.dev/discussions/441) | Use integrations signed in as your users | 🕝 |
|
||||
| File IO | Create Tasks that have file outputs | 🕝 |
|
||||
|
||||
# Getting started
|
||||
|
||||
Visit our docs [here](https://trigger.dev/docs).
|
||||
|
||||
@@ -80,8 +80,8 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
],
|
||||
resources: {
|
||||
limits: {
|
||||
cpu: "250m",
|
||||
memory: "0.5G",
|
||||
cpu: "1",
|
||||
memory: "1G",
|
||||
"ephemeral-storage": "2Gi",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { conform, useForm } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { DiscordIcon, GitHubLightIcon } from "@trigger.dev/companyicons";
|
||||
import { ActivityIcon } from "lucide-react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { FeedbackType, feedbackTypeLabel, schema } from "~/routes/resources.feedback";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { docsPath } from "~/utils/pathBuilder";
|
||||
import { Button, LinkButton } from "./primitives/Buttons";
|
||||
import { Fieldset } from "./primitives/Fieldset";
|
||||
import { FormButtons } from "./primitives/FormButtons";
|
||||
@@ -13,20 +17,9 @@ import { Header1, Header2 } from "./primitives/Headers";
|
||||
import { InputGroup } from "./primitives/InputGroup";
|
||||
import { Label } from "./primitives/Label";
|
||||
import { Paragraph } from "./primitives/Paragraph";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./primitives/Select";
|
||||
import { Select, SelectItem } from "./primitives/Select";
|
||||
import { Sheet, SheetBody, SheetContent, SheetTrigger } from "./primitives/Sheet";
|
||||
import { TextArea } from "./primitives/TextArea";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { ActivityIcon, HeartPulseIcon } from "lucide-react";
|
||||
import { docsPath } from "~/utils/pathBuilder";
|
||||
|
||||
type FeedbackProps = {
|
||||
button: ReactNode;
|
||||
@@ -78,20 +71,20 @@ export function Feedback({ button, defaultValue = "bug" }: FeedbackProps) {
|
||||
<Fieldset className="max-w-full gap-y-3">
|
||||
<input value={location.pathname} {...conform.input(path, { type: "hidden" })} />
|
||||
<InputGroup className="max-w-full">
|
||||
<SelectGroup>
|
||||
<Select {...conform.input(feedbackType)} defaultValue={defaultValue}>
|
||||
<SelectTrigger size="medium" width="full">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(feedbackTypeLabel).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Select
|
||||
{...conform.select(feedbackType)}
|
||||
variant="tertiary/medium"
|
||||
defaultValue={defaultValue}
|
||||
placeholder="Select type"
|
||||
text={(value) => feedbackTypeLabel[value]}
|
||||
dropdownIcon
|
||||
>
|
||||
{Object.entries(feedbackTypeLabel).map(([name, title]) => (
|
||||
<SelectItem key={name} value={name}>
|
||||
{title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormError id={feedbackType.errorId}>{feedbackType.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup className="max-w-full">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/primitives/Tooltip";
|
||||
import { useIsImpersonating } from "~/hooks/useOrganizations";
|
||||
import { useHasAdminAccess } from "~/hooks/useUser";
|
||||
|
||||
export function AdminDebugTooltip({ children }: { children: React.ReactNode }) {
|
||||
const hasAdminAccess = useHasAdminAccess();
|
||||
const isImpersonating = useIsImpersonating();
|
||||
|
||||
if (!hasAdminAccess && !isImpersonating) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ShieldCheckIcon className="h-5 w-5" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-1">{children}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../primitives/Select";
|
||||
} from "../primitives/SimpleSelect";
|
||||
import { EventListSearchSchema } from "./EventStatuses";
|
||||
import { environmentKeys, FilterableEnvironment } from "~/components/runs/RunStatuses";
|
||||
import { TimeFrameFilter } from "../runs/TimeFrameFilter";
|
||||
|
||||
@@ -41,7 +41,7 @@ export function PageBody({
|
||||
<div
|
||||
className={cn(
|
||||
scrollable
|
||||
? "overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
? "overflow-y-auto p-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
: "overflow-hidden",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { ReactNode } from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variants = {
|
||||
"tertiary/small": {
|
||||
box: "h-6 bg-tertiary rounded pl-1.5 gap-1.5 text-xs divide-x divide-black/15 group-hover:bg-charcoal-600",
|
||||
clear: "size-6 text-text-dimmed hover:text-text-bright transition-colors",
|
||||
},
|
||||
"minimal/small": {
|
||||
box: "h-6 hover:bg-tertiary rounded pl-1.5 gap-1.5 text-xs",
|
||||
clear: "size-6 text-text-dimmed hover:text-text-bright transition-colors",
|
||||
},
|
||||
};
|
||||
|
||||
type Variant = keyof typeof variants;
|
||||
|
||||
type AppliedFilterProps = {
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
removable?: boolean;
|
||||
onRemove?: () => void;
|
||||
variant?: Variant;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AppliedFilter({
|
||||
label,
|
||||
value,
|
||||
removable = true,
|
||||
onRemove,
|
||||
variant = "tertiary/small",
|
||||
className,
|
||||
}: AppliedFilterProps) {
|
||||
const variantClassName = variants[variant];
|
||||
return (
|
||||
<div className={cn("flex items-center transition", variantClassName.box, className)}>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="text-text-dimmed">
|
||||
<span>{label}</span>:
|
||||
</div>
|
||||
<div className="text-text-bright">
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
{removable && (
|
||||
<button
|
||||
className={cn("group flex size-6 items-center justify-center", variantClassName.clear)}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<XMarkIcon className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,143 +1,624 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import * as Ariakit from "@ariakit/react";
|
||||
import { SelectProps as AriaSelectProps } from "@ariakit/react";
|
||||
import { SelectValue } from "@ariakit/react-core/select/select-value";
|
||||
import { Link } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
const sizes = {
|
||||
"secondary/small":
|
||||
"text-xs h-6 bg-tertiary border border-tertiary group-hover:text-text-bright hover:border-charcoal-600 pr-2 pl-1.5",
|
||||
medium: "text-sm h-8 bg-tertiary border border-tertiary hover:border-charcoal-600 px-2.5",
|
||||
minimal: "text-xs h-6 bg-transparent hover:bg-tertiary pl-1.5 pr-2",
|
||||
small: {
|
||||
button: "h-6 rounded text-xs px-2 ",
|
||||
},
|
||||
medium: {
|
||||
button: "h-8 rounded text-xs px-3 text-sm",
|
||||
},
|
||||
};
|
||||
|
||||
export type SelectProps = {
|
||||
size?: keyof typeof sizes;
|
||||
width?: "content" | "full";
|
||||
const style = {
|
||||
tertiary: {
|
||||
button:
|
||||
"bg-tertiary focus-within:ring-charcoal-500 border border-tertiary hover:text-text-bright hover:border-charcoal-600",
|
||||
},
|
||||
minimal: {
|
||||
button:
|
||||
"bg-transparent focus-within:ring-charcoal-500 hover:bg-tertiary disabled:bg-transparent disabled:pointer-events-none",
|
||||
},
|
||||
};
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & SelectProps
|
||||
>(({ className, children, width = "content", size = "secondary/small", ...props }, ref) => {
|
||||
const sizeClassName = sizes[size];
|
||||
const variants = {
|
||||
"tertiary/small": {
|
||||
button: cn(sizes.small.button, style.tertiary.button),
|
||||
},
|
||||
"tertiary/medium": {
|
||||
button: cn(sizes.medium.button, style.tertiary.button),
|
||||
},
|
||||
"minimal/small": {
|
||||
button: cn(sizes.small.button, style.minimal.button),
|
||||
},
|
||||
"minimal/medium": {
|
||||
button: cn(sizes.medium.button, style.minimal.button),
|
||||
},
|
||||
};
|
||||
|
||||
type Variant = keyof typeof variants;
|
||||
|
||||
type Section<TItem> = {
|
||||
type: "section";
|
||||
title?: string;
|
||||
items: TItem[];
|
||||
};
|
||||
|
||||
function isSection<TItem>(data: TItem[] | Section<TItem>[]): data is Section<TItem>[] {
|
||||
const firstItem = data[0];
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:ring-ring group flex items-center justify-between gap-x-1 rounded text-text-dimmed transition placeholder:text-text-dimmed hover:text-text-bright focus-visible:bg-tertiary focus-visible:text-text-bright focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
width === "full" ? "w-full" : "w-min",
|
||||
sizeClassName,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"size-4 text-text-dimmed transition group-hover:text-text-bright group-focus:text-text-bright"
|
||||
)}
|
||||
/>
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
(firstItem as Section<TItem>).type === "section" &&
|
||||
(firstItem as Section<TItem>).items !== undefined &&
|
||||
Array.isArray((firstItem as Section<TItem>).items)
|
||||
);
|
||||
});
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
}
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-max overflow-hidden rounded-md border border-charcoal-700 bg-background-dimmed text-text-bright shadow-md animate-in fade-in-40",
|
||||
position === "popper" && "translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
type ItemFromSection<TItemOrSection> = TItemOrSection extends Section<infer U> ? U : TItemOrSection;
|
||||
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;
|
||||
value?: Ariakit.SelectProviderProps<TValue>["value"];
|
||||
setValue?: Ariakit.SelectProviderProps<TValue>["setValue"];
|
||||
defaultValue?: Ariakit.SelectProviderProps<TValue>["defaultValue"];
|
||||
label?: string | Ariakit.SelectLabelProps["render"];
|
||||
heading?: string;
|
||||
showHeading?: boolean;
|
||||
items?: TItem[] | Section<TItem>[];
|
||||
empty?: React.ReactNode;
|
||||
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean;
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((
|
||||
items: ItemFromSection<TItem>[],
|
||||
meta: {
|
||||
shortcutsEnabled?: boolean;
|
||||
section?: {
|
||||
title?: string;
|
||||
startIndex: number;
|
||||
count: number;
|
||||
};
|
||||
}
|
||||
) => React.ReactNode);
|
||||
variant?: Variant;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
shortcut?: ShortcutDefinition;
|
||||
allowItemShortcuts?: boolean;
|
||||
clearSearchOnSelection?: boolean;
|
||||
dropdownIcon?: boolean | React.ReactNode;
|
||||
}
|
||||
|
||||
export function Select<TValue extends string | string[], TItem>({
|
||||
children,
|
||||
icon,
|
||||
text,
|
||||
placeholder,
|
||||
value,
|
||||
setValue,
|
||||
defaultValue,
|
||||
label,
|
||||
heading,
|
||||
showHeading = false,
|
||||
items,
|
||||
filter,
|
||||
empty = null,
|
||||
variant = "tertiary/small",
|
||||
open,
|
||||
setOpen,
|
||||
shortcut,
|
||||
allowItemShortcuts = true,
|
||||
disabled,
|
||||
clearSearchOnSelection = true,
|
||||
dropdownIcon,
|
||||
...props
|
||||
}: SelectProps<TValue, TItem>) {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const searchable = items !== undefined && filter !== undefined;
|
||||
|
||||
const matches = useMemo(() => {
|
||||
if (!items) return [];
|
||||
if (!searchValue || !filter) return items;
|
||||
|
||||
if (isSection(items)) {
|
||||
return items
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: section.items.filter((item) =>
|
||||
filter(item as ItemFromSection<TItem>, searchValue, section.title)
|
||||
),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
}
|
||||
|
||||
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
|
||||
}, [searchValue, items]);
|
||||
|
||||
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
|
||||
|
||||
const select = (
|
||||
<SelectProvider
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
virtualFocus={searchable}
|
||||
value={value}
|
||||
setValue={(v) => {
|
||||
if (clearSearchOnSelection) {
|
||||
setSearchValue("");
|
||||
}
|
||||
|
||||
if (setValue) {
|
||||
setValue(v as any);
|
||||
}
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"space-y-0.5 px-1 py-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
{label && <SelectLabel render={typeof label === "string" ? <div>{label}</div> : label} />}
|
||||
<SelectTrigger
|
||||
icon={icon}
|
||||
variant={variant}
|
||||
text={text}
|
||||
placeholder={placeholder}
|
||||
shortcut={shortcut}
|
||||
tooltipTitle={heading}
|
||||
disabled={disabled}
|
||||
dropdownIcon={dropdownIcon}
|
||||
{...props}
|
||||
/>
|
||||
<SelectPopover>
|
||||
{!searchable && showHeading && heading && <SelectHeading render={<>{heading}</>} />}
|
||||
{searchable && <ComboBox placeholder={heading} shortcut={shortcut} value={searchValue} />}
|
||||
|
||||
<SelectList>
|
||||
{typeof children === "function" ? (
|
||||
matches.length > 0 ? (
|
||||
isSection(matches) ? (
|
||||
<SelectGroupedRenderer
|
||||
items={matches}
|
||||
children={children}
|
||||
enableItemShortcuts={enableItemShortcuts}
|
||||
/>
|
||||
) : (
|
||||
children(matches as ItemFromSection<TItem>[], {
|
||||
shortcutsEnabled: enableItemShortcuts,
|
||||
})
|
||||
)
|
||||
) : (
|
||||
empty
|
||||
)
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
|
||||
if (searchable) {
|
||||
return (
|
||||
<ComboboxProvider
|
||||
resetValueOnHide
|
||||
setValue={(value) => {
|
||||
React.startTransition(() => {
|
||||
setSearchValue(value);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
{select}
|
||||
</ComboboxProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"-ml-1 -mr-1 mb-1 bg-charcoal-900 py-1.5 pl-2 pr-2 font-sans text-xxs font-normal uppercase leading-normal tracking-wider text-text-dimmed first-of-type:-mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
return select;
|
||||
}
|
||||
|
||||
type SelectItemProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
|
||||
contentClassName?: string;
|
||||
};
|
||||
export interface SelectTriggerProps<TValue = any> extends AriaSelectProps {
|
||||
icon?: React.ReactNode;
|
||||
text?: React.ReactNode | ((value: TValue) => React.ReactNode);
|
||||
placeholder?: React.ReactNode;
|
||||
variant?: Variant;
|
||||
shortcut?: ShortcutDefinition;
|
||||
tooltipTitle?: string;
|
||||
dropdownIcon?: boolean | React.ReactNode;
|
||||
}
|
||||
|
||||
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
|
||||
({ className, children, contentClassName, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
export function SelectTrigger({
|
||||
icon,
|
||||
variant = "tertiary/small",
|
||||
text,
|
||||
shortcut,
|
||||
tooltipTitle,
|
||||
disabled,
|
||||
placeholder,
|
||||
dropdownIcon = false,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: SelectTriggerProps) {
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
useShortcutKeys({
|
||||
shortcut: shortcut,
|
||||
action: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (ref.current) {
|
||||
ref.current.click();
|
||||
}
|
||||
},
|
||||
disabled,
|
||||
});
|
||||
|
||||
const showTooltip = tooltipTitle || shortcut;
|
||||
const variantClasses = variants[variant];
|
||||
|
||||
let content: React.ReactNode = "";
|
||||
if (children) {
|
||||
content = children;
|
||||
} else if (text !== undefined) {
|
||||
if (typeof text === "function") {
|
||||
content = <SelectValue>{(value) => <>{text(value) ?? placeholder}</>}</SelectValue>;
|
||||
} else {
|
||||
content = text;
|
||||
}
|
||||
} else {
|
||||
content = (
|
||||
<SelectValue>
|
||||
{(value) => (
|
||||
<>
|
||||
{typeof value === "string"
|
||||
? value ?? placeholder
|
||||
: value.length === 0
|
||||
? placeholder
|
||||
: value.join(", ")}
|
||||
</>
|
||||
)}
|
||||
</SelectValue>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Ariakit.TooltipProvider timeout={200}>
|
||||
<Ariakit.TooltipAnchor
|
||||
className="button"
|
||||
render={
|
||||
<Ariakit.Select
|
||||
className={cn(
|
||||
"group flex items-center gap-1 outline-offset-0 focus-within:outline-none focus-within:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
variantClasses.button,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex grow items-center gap-0.5">
|
||||
{icon && <div className="-ml-1 flex-none">{icon}</div>}
|
||||
<div className="truncate">{content}</div>
|
||||
</div>
|
||||
{dropdownIcon === true ? (
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"size-4 flex-none text-text-dimmed transition group-hover:text-text-bright group-focus:text-text-bright"
|
||||
)}
|
||||
/>
|
||||
) : !dropdownIcon ? null : (
|
||||
dropdownIcon
|
||||
)}
|
||||
</Ariakit.TooltipAnchor>
|
||||
{showTooltip && (
|
||||
<Ariakit.Tooltip
|
||||
disabled={shortcut === undefined}
|
||||
className="z-40 cursor-default rounded border border-charcoal-700 bg-background-bright px-2 py-1.5 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{tooltipTitle ?? "Open menu"}</span>
|
||||
{shortcut && (
|
||||
<ShortcutKey
|
||||
className={cn("size-4 flex-none")}
|
||||
shortcut={shortcut}
|
||||
variant={"small"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Ariakit.Tooltip>
|
||||
)}
|
||||
</Ariakit.TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectProviderProps<TValue extends string | string[]>
|
||||
extends Ariakit.SelectProviderProps<TValue> {}
|
||||
export function SelectProvider<TValue extends string | string[]>(
|
||||
props: SelectProviderProps<TValue>
|
||||
) {
|
||||
return <Ariakit.SelectProvider {...props} />;
|
||||
}
|
||||
|
||||
export interface ComboboxProviderProps extends Ariakit.ComboboxProviderProps {}
|
||||
export function ComboboxProvider(props: ComboboxProviderProps) {
|
||||
return <Ariakit.ComboboxProvider {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroupedRenderer<TItem>({
|
||||
items,
|
||||
children,
|
||||
enableItemShortcuts,
|
||||
}: {
|
||||
items: Section<TItem>[];
|
||||
children: (
|
||||
items: ItemFromSection<TItem>[],
|
||||
meta: {
|
||||
shortcutsEnabled?: boolean;
|
||||
section?: { title?: string; startIndex: number; count: number };
|
||||
}
|
||||
) => React.ReactNode;
|
||||
enableItemShortcuts: boolean;
|
||||
}) {
|
||||
let count = 0;
|
||||
return (
|
||||
<>
|
||||
{items.map((section, index) => {
|
||||
const previousItem = items.at(index - 1);
|
||||
count += previousItem ? previousItem.items.length : 0;
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{children(section.items as ItemFromSection<TItem>[], {
|
||||
shortcutsEnabled: enableItemShortcuts,
|
||||
section: {
|
||||
title: section.title,
|
||||
startIndex: count - 1,
|
||||
count: section.items.length,
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectListProps extends Omit<Ariakit.SelectListProps, "store"> {}
|
||||
export function SelectList(props: SelectListProps) {
|
||||
const combobox = Ariakit.useComboboxContext();
|
||||
const Component = combobox ? Ariakit.ComboboxList : Ariakit.SelectList;
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-charcoal-750 focus:bg-charcoal-750/50",
|
||||
"overflow-y-auto overscroll-contain outline-none scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600",
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectItemProps extends Ariakit.SelectItemProps {
|
||||
icon?: React.ReactNode;
|
||||
checkIcon?: React.ReactNode;
|
||||
shortcut?: ShortcutDefinition;
|
||||
}
|
||||
|
||||
const selectItemClasses =
|
||||
"group cursor-pointer px-1 pt-1 text-xs text-text-dimmed outline-none last:pb-1";
|
||||
|
||||
export function SelectItem({
|
||||
icon,
|
||||
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-white" />,
|
||||
shortcut,
|
||||
...props
|
||||
}: SelectItemProps) {
|
||||
const combobox = Ariakit.useComboboxContext();
|
||||
const render = combobox ? <Ariakit.ComboboxItem render={props.render} /> : undefined;
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useShortcutKeys({
|
||||
shortcut: shortcut,
|
||||
action: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (ref.current) {
|
||||
ref.current.click();
|
||||
}
|
||||
},
|
||||
disabled: props.disabled,
|
||||
enabledOnInputElements: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Ariakit.SelectItem
|
||||
{...props}
|
||||
render={render}
|
||||
blurOnHoverEnd={false}
|
||||
className={cn(
|
||||
selectItemClasses,
|
||||
"[--padding-block:0.5rem] sm:[--padding-block:0.25rem]",
|
||||
props.className
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="flex h-7 w-full items-center gap-1 rounded-sm px-2 group-data-[active-item=true]:bg-tertiary">
|
||||
{icon}
|
||||
<div className="grow truncate">{props.children || props.value}</div>
|
||||
{checkIcon}
|
||||
{shortcut && (
|
||||
<ShortcutKey className={cn("size-4 flex-none")} shortcut={shortcut} variant={"small"} />
|
||||
)}
|
||||
</div>
|
||||
</Ariakit.SelectItem>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectLinkItemProps extends Ariakit.SelectItemProps {
|
||||
icon?: React.ReactNode;
|
||||
checkIcon?: React.ReactNode;
|
||||
shortcut?: ShortcutDefinition;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export function SelectLinkItem({
|
||||
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-white" />,
|
||||
to,
|
||||
...props
|
||||
}: SelectLinkItemProps) {
|
||||
const render = <Link to={to} className={cn("block", selectItemClasses, props.className)} />;
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
{...props}
|
||||
render={render}
|
||||
blurOnHoverEnd={false}
|
||||
className={cn(selectItemClasses, props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectButtonItemProps extends Omit<Ariakit.SelectItemProps, "onClick"> {
|
||||
icon?: React.ReactNode;
|
||||
checkIcon?: React.ReactNode;
|
||||
shortcut?: ShortcutDefinition;
|
||||
onClick: React.ComponentProps<"button">["onClick"];
|
||||
}
|
||||
|
||||
export function SelectButtonItem({
|
||||
checkIcon = <Ariakit.SelectItemCheck className="size-8 flex-none text-white" />,
|
||||
onClick,
|
||||
...props
|
||||
}: SelectButtonItemProps) {
|
||||
const render = (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn("block w-full text-left", selectItemClasses, props.className)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectItem
|
||||
{...props}
|
||||
render={render}
|
||||
blurOnHoverEnd={false}
|
||||
className={cn(selectItemClasses, props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function shortcutFromIndex(
|
||||
index: number,
|
||||
meta: {
|
||||
shortcutsEnabled?: boolean;
|
||||
section?: { startIndex: number };
|
||||
}
|
||||
): ShortcutDefinition | undefined {
|
||||
if (!meta.shortcutsEnabled) return;
|
||||
|
||||
let adjustedIndex = index + (meta.section?.startIndex ?? 0);
|
||||
|
||||
if (adjustedIndex > 9) return;
|
||||
if (adjustedIndex === 9) {
|
||||
adjustedIndex = -1;
|
||||
}
|
||||
|
||||
return { key: String(adjustedIndex + 1) };
|
||||
}
|
||||
|
||||
export interface SelectSeparatorProps extends React.ComponentProps<"div"> {}
|
||||
|
||||
export function SelectSeparator(props: SelectSeparatorProps) {
|
||||
return <div {...props} className={cn("h-px bg-charcoal-700", props.className)} />;
|
||||
}
|
||||
|
||||
export interface SelectGroupProps extends Ariakit.SelectGroupProps {}
|
||||
|
||||
export function SelectGroup(props: SelectGroupProps) {
|
||||
return <Ariakit.SelectGroup {...props} />;
|
||||
}
|
||||
|
||||
export interface SelectGroupLabelProps extends Ariakit.SelectGroupLabelProps {}
|
||||
|
||||
export function SelectGroupLabel(props: SelectGroupLabelProps) {
|
||||
return (
|
||||
<Ariakit.SelectGroupLabel
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex h-[1.375rem] items-center border-b border-charcoal-700 bg-charcoal-750 px-2.5 text-xxs uppercase text-text-bright",
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectHeadingProps extends Ariakit.SelectHeadingProps {}
|
||||
export function SelectHeading({ render, ...props }: SelectHeadingProps) {
|
||||
return (
|
||||
<div className="flex h-[1.375rem] flex-none cursor-default items-center gap-2 border-b border-charcoal-700 bg-charcoal-750 px-2.5 text-xxs uppercase text-text-bright">
|
||||
<Ariakit.SelectHeading render={<>{render}</>} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SelectPopoverProps extends Ariakit.SelectPopoverProps {}
|
||||
export function SelectPopover({
|
||||
gutter = 5,
|
||||
shift = 0,
|
||||
unmountOnHide = true,
|
||||
className,
|
||||
...props
|
||||
}: SelectPopoverProps) {
|
||||
return (
|
||||
<Ariakit.SelectPopover
|
||||
gutter={gutter}
|
||||
shift={shift}
|
||||
unmountOnHide={unmountOnHide}
|
||||
className={cn(
|
||||
"z-50 flex flex-col overflow-clip rounded border border-charcoal-700 bg-background-bright shadow-md outline-none animate-in fade-in-40",
|
||||
"min-w-[max(180px,calc(var(--popover-anchor-width)+0.5rem))]",
|
||||
"max-w-[min(480px,var(--popover-available-width))]",
|
||||
"max-h-[min(480px,var(--popover-available-height))]",
|
||||
"origin-[var(--popover-transform-origin)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
);
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
export interface SelectLabelProps extends Ariakit.SelectLabelProps {}
|
||||
//currently unstyled
|
||||
export function SelectLabel(props: SelectLabelProps) {
|
||||
return <Ariakit.SelectLabel {...props} />;
|
||||
}
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
export interface ComboBoxProps extends Ariakit.ComboboxProps {
|
||||
shortcut?: ShortcutDefinition;
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
export function ComboBox({
|
||||
autoSelect = true,
|
||||
placeholder = "Filter options",
|
||||
shortcut,
|
||||
...props
|
||||
}: ComboBoxProps) {
|
||||
return (
|
||||
<div className="flex h-9 w-full flex-none items-center border-b border-grid-dimmed bg-transparent px-3 text-xs text-text-dimmed outline-none">
|
||||
<Ariakit.Combobox
|
||||
autoSelect={autoSelect}
|
||||
render={<input placeholder={placeholder} />}
|
||||
className="flex-1 bg-transparent text-xs text-text-dimmed outline-none"
|
||||
{...props}
|
||||
/>
|
||||
{shortcut && (
|
||||
<ShortcutKey className={cn("size-4 flex-none")} shortcut={shortcut} variant={"small"} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const sizes = {
|
||||
"secondary/small":
|
||||
"text-xs h-6 bg-tertiary border border-tertiary group-hover:text-text-bright hover:border-charcoal-600 pr-2 pl-1.5",
|
||||
medium: "text-sm h-8 bg-tertiary border border-tertiary hover:border-charcoal-600 px-2.5",
|
||||
minimal: "text-xs h-6 bg-transparent hover:bg-tertiary pl-1.5 pr-2",
|
||||
};
|
||||
|
||||
export type SelectProps = {
|
||||
size?: keyof typeof sizes;
|
||||
width?: "content" | "full";
|
||||
};
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & SelectProps
|
||||
>(({ className, children, width = "content", size = "secondary/small", ...props }, ref) => {
|
||||
const sizeClassName = sizes[size];
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"ring-offset-background focus-visible:ring-ring group flex items-center justify-between gap-x-1 rounded text-text-dimmed transition placeholder:text-text-dimmed hover:text-text-bright focus-visible:bg-tertiary focus-visible:text-text-bright focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
width === "full" ? "w-full" : "w-min",
|
||||
sizeClassName,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"size-4 text-text-dimmed transition group-hover:text-text-bright group-focus:text-text-bright"
|
||||
)}
|
||||
/>
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
});
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-max overflow-hidden rounded-md border border-charcoal-700 bg-background-dimmed text-text-bright shadow-md animate-in fade-in-40",
|
||||
position === "popper" && "translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"space-y-0.5 px-1 py-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"-ml-1 -mr-1 mb-1 bg-charcoal-900 py-1.5 pl-2 pr-2 font-sans text-xxs font-normal uppercase leading-normal tracking-wider text-text-dimmed first-of-type:-mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
type SelectItemProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
|
||||
({ className, children, contentClassName, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-12 text-sm outline-none transition data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-charcoal-750 focus:bg-charcoal-750/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
);
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../primitives/Select";
|
||||
} from "../primitives/SimpleSelect";
|
||||
import { Spinner } from "../primitives/Spinner";
|
||||
import {
|
||||
FilterableEnvironment,
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import type { TaskRunStatus as TaskRunStatusType } from "@trigger.dev/database";
|
||||
import { RuntimeEnvironment, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
|
||||
import { useCallback } from "react";
|
||||
import * as Ariakit from "@ariakit/react";
|
||||
import { CalendarIcon, CpuChipIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { RuntimeEnvironment, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
|
||||
import { ListFilterIcon } from "lucide-react";
|
||||
import { ReactNode, startTransition, useCallback, useMemo, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
ComboBox,
|
||||
ComboboxProvider,
|
||||
SelectButtonItem,
|
||||
SelectItem,
|
||||
SelectList,
|
||||
SelectPopover,
|
||||
SelectProvider,
|
||||
SelectTrigger,
|
||||
shortcutFromIndex,
|
||||
} from "~/components/primitives/Select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -11,243 +27,656 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "~/components/primitives/Tooltip";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
import { Button } from "../../primitives/Buttons";
|
||||
import { Paragraph } from "../../primitives/Paragraph";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../primitives/Select";
|
||||
import { TimeFrameFilter } from "../TimeFrameFilter";
|
||||
import { TaskRunStatusCombo, descriptionForTaskRunStatus } from "./TaskRunStatus";
|
||||
|
||||
export const allTaskRunStatuses = [
|
||||
"PENDING",
|
||||
"WAITING_FOR_DEPLOY",
|
||||
"EXECUTING",
|
||||
"RETRYING_AFTER_FAILURE",
|
||||
"WAITING_TO_RESUME",
|
||||
"COMPLETED_SUCCESSFULLY",
|
||||
"CANCELED",
|
||||
"COMPLETED_WITH_ERRORS",
|
||||
"INTERRUPTED",
|
||||
"SYSTEM_FAILURE",
|
||||
"CRASHED",
|
||||
] as TaskRunStatusType[];
|
||||
TaskRunStatusCombo,
|
||||
allTaskRunStatuses,
|
||||
descriptionForTaskRunStatus,
|
||||
runStatusTitle,
|
||||
} from "./TaskRunStatus";
|
||||
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
|
||||
|
||||
export const TaskAttemptStatus = z.nativeEnum(TaskRunStatus);
|
||||
|
||||
export const TaskRunListSearchFilters = z.object({
|
||||
cursor: z.string().optional(),
|
||||
direction: z.enum(["forward", "backward"]).optional(),
|
||||
environments: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => (value ? value.split(",") : undefined)),
|
||||
tasks: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => (value ? value.split(",") : undefined)),
|
||||
versions: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => (value ? value.split(",") : undefined)),
|
||||
environments: z.preprocess(
|
||||
(value) => (typeof value === "string" ? [value] : value),
|
||||
z.string().array().optional()
|
||||
),
|
||||
tasks: z.preprocess(
|
||||
(value) => (typeof value === "string" ? [value] : value),
|
||||
z.string().array().optional()
|
||||
),
|
||||
versions: z.preprocess(
|
||||
(value) => (typeof value === "string" ? [value] : value),
|
||||
z.string().array().optional()
|
||||
),
|
||||
statuses: z.preprocess(
|
||||
(value) => (typeof value === "string" ? value.split(",") : undefined),
|
||||
(value) => (typeof value === "string" ? [value] : value),
|
||||
TaskAttemptStatus.array().optional()
|
||||
),
|
||||
period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()),
|
||||
from: z.coerce.number().optional(),
|
||||
to: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
|
||||
|
||||
const All = "ALL";
|
||||
|
||||
type DisplayableEnvironment = Pick<RuntimeEnvironment, "type" | "id"> & {
|
||||
userName?: string;
|
||||
};
|
||||
|
||||
type RunFiltersProps = {
|
||||
possibleEnvironments: DisplayableEnvironment[];
|
||||
possibleTasks: string[];
|
||||
possibleTasks: { slug: string; triggerSource: TaskTriggerSource }[];
|
||||
hasFilters: boolean;
|
||||
};
|
||||
|
||||
export function RunsFilters({ possibleEnvironments, possibleTasks }: RunFiltersProps) {
|
||||
const navigate = useNavigate();
|
||||
export function RunsFilters(props: RunFiltersProps) {
|
||||
const location = useOptimisticLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const { environments, tasks, versions, statuses, from, to } = TaskRunListSearchFilters.parse(
|
||||
Object.fromEntries(searchParams.entries())
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback((filterType: string, value: string | undefined) => {
|
||||
if (value) {
|
||||
searchParams.set(filterType, value);
|
||||
} else {
|
||||
searchParams.delete(filterType);
|
||||
}
|
||||
searchParams.delete("cursor");
|
||||
searchParams.delete("direction");
|
||||
navigate(`${location.pathname}?${searchParams.toString()}`);
|
||||
}, []);
|
||||
|
||||
const handleStatusChange = useCallback((value: TaskRunAttemptStatus | typeof All) => {
|
||||
handleFilterChange("statuses", value === "ALL" ? undefined : value);
|
||||
}, []);
|
||||
|
||||
const handleTaskChange = useCallback((value: string | typeof All) => {
|
||||
handleFilterChange("tasks", value === "ALL" ? undefined : value);
|
||||
}, []);
|
||||
|
||||
const handleEnvironmentChange = useCallback((value: string | typeof All) => {
|
||||
handleFilterChange("environments", value === "ALL" ? undefined : value);
|
||||
}, []);
|
||||
|
||||
const handleTimeFrameChange = useCallback((range: { from?: number; to?: number }) => {
|
||||
if (range.from) {
|
||||
searchParams.set("from", range.from.toString());
|
||||
} else {
|
||||
searchParams.delete("from");
|
||||
}
|
||||
|
||||
if (range.to) {
|
||||
searchParams.set("to", range.to.toString());
|
||||
} else {
|
||||
searchParams.delete("to");
|
||||
}
|
||||
|
||||
searchParams.delete("cursor");
|
||||
searchParams.delete("direction");
|
||||
navigate(`${location.pathname}?${searchParams.toString()}`);
|
||||
}, []);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
searchParams.delete("statuses");
|
||||
searchParams.delete("environments");
|
||||
searchParams.delete("tasks");
|
||||
searchParams.delete("from");
|
||||
searchParams.delete("to");
|
||||
navigate(`${location.pathname}?${searchParams.toString()}`);
|
||||
}, []);
|
||||
const hasFilters =
|
||||
searchParams.has("statuses") ||
|
||||
searchParams.has("environments") ||
|
||||
searchParams.has("tasks") ||
|
||||
searchParams.has("period");
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between">
|
||||
<SelectGroup>
|
||||
<Select
|
||||
name="environment"
|
||||
value={environments?.at(0) ?? "ALL"}
|
||||
onValueChange={handleEnvironmentChange}
|
||||
>
|
||||
<SelectTrigger size="minimal" width="full">
|
||||
<SelectValue placeholder={"Select environment"} className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"ALL"}>
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="pl-0.5 transition group-hover:text-text-bright"
|
||||
>
|
||||
All environments
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
{possibleEnvironments.map((env) => (
|
||||
<SelectItem key={env.id} value={env.id}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<EnvironmentLabel environment={env} userName={env.userName} />
|
||||
<Paragraph variant="extra-small">environment</Paragraph>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<SelectGroup>
|
||||
<Select name="status" value={statuses?.at(0) ?? "ALL"} onValueChange={handleStatusChange}>
|
||||
<SelectTrigger size="minimal" width="full">
|
||||
<SelectValue placeholder="Select status" className="ml-2 p-0">
|
||||
{statuses?.at(0) ? (
|
||||
<TaskRunStatusCombo
|
||||
status={statuses[0]}
|
||||
className="text-xs"
|
||||
iconClassName="animate-none"
|
||||
/>
|
||||
) : (
|
||||
"All statuses"
|
||||
)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="overflow-visible">
|
||||
<SelectItem value={"ALL"} className="">
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="pl-0.5 transition group-hover:text-text-bright"
|
||||
>
|
||||
All statuses
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
{allTaskRunStatuses.map((status) => (
|
||||
<TooltipProvider key={status}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="group flex w-full flex-col py-0">
|
||||
<SelectItem value={status} className="">
|
||||
<TaskRunStatusCombo
|
||||
status={status}
|
||||
className="text-xs"
|
||||
iconClassName="animate-none"
|
||||
/>
|
||||
<TooltipContent side="right" sideOffset={9}>
|
||||
<Paragraph variant="extra-small">
|
||||
{descriptionForTaskRunStatus(status)}
|
||||
</Paragraph>
|
||||
</TooltipContent>
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<SelectGroup>
|
||||
<Select name="tasks" value={tasks?.at(0) ?? "ALL"} onValueChange={handleTaskChange}>
|
||||
<SelectTrigger size="minimal" width="full">
|
||||
<SelectValue placeholder="Select task" className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={"ALL"}>
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="pl-0.5 transition group-hover:text-text-bright"
|
||||
>
|
||||
All tasks
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
{possibleTasks
|
||||
.sort((a, b) => a.localeCompare(b)) // 🔤
|
||||
.map((task) => (
|
||||
<SelectItem key={task} value={task}>
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="pl-0.5 transition group-hover:text-text-bright"
|
||||
>
|
||||
{task}
|
||||
</Paragraph>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<TimeFrameFilter from={from} to={to} onRangeChanged={handleTimeFrameChange} />
|
||||
|
||||
<Button variant="minimal/small" onClick={() => clearFilters()} LeadingIcon={XMarkIcon} />
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<FilterMenu {...props} />
|
||||
<AppliedFilters {...props} />
|
||||
{hasFilters && (
|
||||
<Form>
|
||||
<Button variant="minimal/small" LeadingIcon={XMarkIcon}>
|
||||
Clear all
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filterTypes = [
|
||||
{
|
||||
name: "statuses",
|
||||
title: "Status",
|
||||
icon: (
|
||||
<div className="flex size-4 items-center justify-center">
|
||||
<div className="size-3 rounded-full border-2 border-text-dimmed" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ name: "environments", title: "Environment", icon: <CpuChipIcon className="size-4" /> },
|
||||
{ name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> },
|
||||
{ name: "created", title: "Created", icon: <CalendarIcon className="size-4" /> },
|
||||
];
|
||||
|
||||
type FilterType = (typeof filterTypes)[number]["name"];
|
||||
|
||||
const shortcut = { key: "f" };
|
||||
|
||||
function FilterMenu(props: RunFiltersProps) {
|
||||
const [filterType, setFilterType] = useState<FilterType | undefined>();
|
||||
|
||||
const filterTrigger = (
|
||||
<SelectTrigger
|
||||
icon={
|
||||
<div className="flex size-4 items-center justify-center">
|
||||
<ListFilterIcon className="size-3.5" />
|
||||
</div>
|
||||
}
|
||||
variant={"minimal/small"}
|
||||
shortcut={shortcut}
|
||||
tooltipTitle={"Filter runs"}
|
||||
>
|
||||
Filter
|
||||
</SelectTrigger>
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterMenuProvider onClose={() => setFilterType(undefined)}>
|
||||
{(search, setSearch) => (
|
||||
<Menu
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
trigger={filterTrigger}
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterMenuProvider({
|
||||
children,
|
||||
onClose,
|
||||
}: {
|
||||
children: (search: string, setSearch: (value: string) => void) => React.ReactNode;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
|
||||
return (
|
||||
<ComboboxProvider
|
||||
resetValueOnHide
|
||||
setValue={(value) => {
|
||||
startTransition(() => {
|
||||
setSearchValue(value);
|
||||
});
|
||||
}}
|
||||
setOpen={(open) => {
|
||||
if (!open && onClose) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children(searchValue, setSearchValue)}
|
||||
</ComboboxProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedFilters({ possibleEnvironments, possibleTasks }: RunFiltersProps) {
|
||||
return (
|
||||
<>
|
||||
<AppliedStatusFilter />
|
||||
<AppliedEnvironmentFilter possibleEnvironments={possibleEnvironments} />
|
||||
<AppliedTaskFilter possibleTasks={possibleTasks} />
|
||||
<AppliedPeriodFilter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type MenuProps = {
|
||||
searchValue: string;
|
||||
clearSearchValue: () => void;
|
||||
trigger: React.ReactNode;
|
||||
filterType: FilterType | undefined;
|
||||
setFilterType: (filterType: FilterType | undefined) => void;
|
||||
} & RunFiltersProps;
|
||||
|
||||
function Menu(props: MenuProps) {
|
||||
switch (props.filterType) {
|
||||
case undefined:
|
||||
return <MainMenu {...props} />;
|
||||
case "statuses":
|
||||
return <StatusDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "environments":
|
||||
return <EnvironmentsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "tasks":
|
||||
return <TasksDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "created":
|
||||
return <CreatedDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: MenuProps) {
|
||||
const filtered = useMemo(() => {
|
||||
return filterTypes.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<SelectProvider virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover>
|
||||
<ComboBox placeholder={"Filter by..."} shortcut={shortcut} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((type, index) => (
|
||||
<SelectButtonItem
|
||||
key={type.name}
|
||||
onClick={() => {
|
||||
clearSearchValue();
|
||||
setFilterType(type.name);
|
||||
}}
|
||||
icon={type.icon}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
|
||||
>
|
||||
{type.title}
|
||||
</SelectButtonItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const statuses = allTaskRunStatuses.map((status) => ({
|
||||
title: runStatusTitle(status),
|
||||
value: status,
|
||||
}));
|
||||
|
||||
function StatusDropdown({
|
||||
trigger,
|
||||
clearSearchValue,
|
||||
searchValue,
|
||||
onClose,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
clearSearchValue: () => void;
|
||||
searchValue: string;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { values, replace } = useSearchParams();
|
||||
|
||||
const handleChange = (values: string[]) => {
|
||||
clearSearchValue();
|
||||
replace({ statuses: values, cursor: undefined, direction: undefined });
|
||||
};
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return statuses.filter((item) => item.title.toLowerCase().includes(searchValue.toLowerCase()));
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<SelectProvider value={values("statuses")} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox placeholder={"Filter by status..."} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((item, index) => (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="group flex w-full flex-col py-0">
|
||||
<TaskRunStatusCombo status={item.value} iconClassName="animate-none" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={9}>
|
||||
<Paragraph variant="extra-small">
|
||||
{descriptionForTaskRunStatus(item.value)}
|
||||
</Paragraph>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedStatusFilter() {
|
||||
const { values, del } = useSearchParams();
|
||||
const statuses = values("statuses");
|
||||
|
||||
if (statuses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<StatusDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer" />}>
|
||||
<AppliedFilter
|
||||
label="Status"
|
||||
value={appliedSummary(statuses.map((v) => runStatusTitle(v as TaskRunStatus)))}
|
||||
onRemove={() => del(["statuses", "cursor", "direction"])}
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function EnvironmentsDropdown({
|
||||
trigger,
|
||||
clearSearchValue,
|
||||
searchValue,
|
||||
onClose,
|
||||
possibleEnvironments,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
clearSearchValue: () => void;
|
||||
searchValue: string;
|
||||
onClose?: () => void;
|
||||
possibleEnvironments: DisplayableEnvironment[];
|
||||
}) {
|
||||
const { values, replace } = useSearchParams();
|
||||
|
||||
const handleChange = (values: string[]) => {
|
||||
clearSearchValue();
|
||||
replace({ environments: values, cursor: undefined, direction: undefined });
|
||||
};
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return possibleEnvironments.filter((item) => {
|
||||
const title = environmentTitle(item, item.userName);
|
||||
return title.toLowerCase().includes(searchValue.toLowerCase());
|
||||
});
|
||||
}, [searchValue, possibleEnvironments]);
|
||||
|
||||
return (
|
||||
<SelectProvider value={values("environments")} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox placeholder={"Filter by environment..."} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((item, index) => (
|
||||
<SelectItem
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
|
||||
>
|
||||
<EnvironmentLabel environment={item} userName={item.userName} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedEnvironmentFilter({
|
||||
possibleEnvironments,
|
||||
}: Pick<RunFiltersProps, "possibleEnvironments">) {
|
||||
const { values, del } = useSearchParams();
|
||||
|
||||
if (values("environments").length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<EnvironmentsDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer" />}>
|
||||
<AppliedFilter
|
||||
label="Environment"
|
||||
value={appliedSummary(
|
||||
values("environments").map((v) => {
|
||||
const environment = possibleEnvironments.find((env) => env.id === v);
|
||||
return environment ? environmentTitle(environment, environment.userName) : v;
|
||||
})
|
||||
)}
|
||||
onRemove={() => del(["environments", "cursor", "direction"])}
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
possibleEnvironments={possibleEnvironments}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TasksDropdown({
|
||||
trigger,
|
||||
clearSearchValue,
|
||||
searchValue,
|
||||
onClose,
|
||||
possibleTasks,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
clearSearchValue: () => void;
|
||||
searchValue: string;
|
||||
onClose?: () => void;
|
||||
possibleTasks: { slug: string; triggerSource: TaskTriggerSource }[];
|
||||
}) {
|
||||
const { values, replace } = useSearchParams();
|
||||
|
||||
const handleChange = (values: string[]) => {
|
||||
clearSearchValue();
|
||||
replace({ tasks: values, cursor: undefined, direction: undefined });
|
||||
};
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return possibleTasks.filter((item) => {
|
||||
return item.slug.toLowerCase().includes(searchValue.toLowerCase());
|
||||
});
|
||||
}, [searchValue, possibleTasks]);
|
||||
|
||||
return (
|
||||
<SelectProvider value={values("tasks")} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox placeholder={"Filter by task..."} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((item, index) => (
|
||||
<SelectItem
|
||||
key={item.slug}
|
||||
value={item.slug}
|
||||
icon={<TaskTriggerSourceIcon source={item.triggerSource} className="size-4" />}
|
||||
>
|
||||
{item.slug}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedTaskFilter({ possibleTasks }: Pick<RunFiltersProps, "possibleTasks">) {
|
||||
const { values, del } = useSearchParams();
|
||||
|
||||
if (values("tasks").length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<TasksDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer" />}>
|
||||
<AppliedFilter
|
||||
label="Task"
|
||||
value={appliedSummary(
|
||||
values("tasks").map((v) => {
|
||||
const task = possibleTasks.find((task) => task.slug === v);
|
||||
return task ? task.slug : v;
|
||||
})
|
||||
)}
|
||||
onRemove={() => del(["tasks", "cursor", "direction"])}
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
possibleTasks={possibleTasks}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const timePeriods = [
|
||||
{
|
||||
label: "All periods",
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
label: "5 mins ago",
|
||||
value: "5m",
|
||||
},
|
||||
{
|
||||
label: "15 mins ago",
|
||||
value: "15m",
|
||||
},
|
||||
{
|
||||
label: "30 mins ago",
|
||||
value: "30m",
|
||||
},
|
||||
{
|
||||
label: "1 hour ago",
|
||||
value: "1h",
|
||||
},
|
||||
{
|
||||
label: "3 hours ago",
|
||||
value: "3h",
|
||||
},
|
||||
{
|
||||
label: "6 hours ago",
|
||||
value: "6h",
|
||||
},
|
||||
{
|
||||
label: "1 day ago",
|
||||
value: "1d",
|
||||
},
|
||||
{
|
||||
label: "3 days ago",
|
||||
value: "3d",
|
||||
},
|
||||
{
|
||||
label: "7 days ago",
|
||||
value: "7d",
|
||||
},
|
||||
{
|
||||
label: "10 days ago",
|
||||
value: "10d",
|
||||
},
|
||||
{
|
||||
label: "14 days ago",
|
||||
value: "14d",
|
||||
},
|
||||
{
|
||||
label: "30 days ago",
|
||||
value: "30d",
|
||||
},
|
||||
];
|
||||
|
||||
function CreatedDropdown({
|
||||
trigger,
|
||||
clearSearchValue,
|
||||
searchValue,
|
||||
onClose,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
clearSearchValue: () => void;
|
||||
searchValue: string;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { value, replace } = useSearchParams();
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
clearSearchValue();
|
||||
if (newValue === "all") {
|
||||
if (!value) return;
|
||||
}
|
||||
|
||||
replace({ period: newValue, cursor: undefined, direction: undefined });
|
||||
};
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return timePeriods.filter((item) =>
|
||||
item.label.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<SelectProvider value={value("period")} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
hideOnEnter={false}
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox placeholder={"Filter by period..."} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value} hideOnClick={false}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedPeriodFilter() {
|
||||
const { value, del } = useSearchParams();
|
||||
|
||||
if (value("period") === undefined || value("period") === "all") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<CreatedDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer" />}>
|
||||
<AppliedFilter
|
||||
label="Created"
|
||||
value={
|
||||
timePeriods.find((t) => t.value === value("period"))?.label ?? value("period")
|
||||
}
|
||||
onRemove={() => del(["period", "cursor", "direction"])}
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function appliedSummary(values: string[], maxValues = 3) {
|
||||
if (values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (values.length > maxValues) {
|
||||
return `${values.slice(0, maxValues).join(", ")} + ${values.length - maxValues} more`;
|
||||
}
|
||||
|
||||
return values.join(", ");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { TrashIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import { useCallback } from "react";
|
||||
import { z } from "zod";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { useThrottle } from "~/hooks/useThrottle";
|
||||
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
||||
import { Button } from "../../primitives/Buttons";
|
||||
import { Paragraph } from "../../primitives/Paragraph";
|
||||
@@ -14,10 +16,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../primitives/Select";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { useDebounce } from "~/hooks/useDebounce";
|
||||
import { useThrottle } from "~/hooks/useThrottle";
|
||||
} from "../../primitives/SimpleSelect";
|
||||
|
||||
export const ScheduleListFilters = z.object({
|
||||
page: z.coerce.number().default(1),
|
||||
@@ -53,6 +52,9 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
Object.fromEntries(searchParams.entries())
|
||||
);
|
||||
|
||||
const hasFilters =
|
||||
searchParams.has("tasks") || searchParams.has("environments") || searchParams.has("search");
|
||||
|
||||
const handleFilterChange = useCallback((filterType: string, value: string | undefined) => {
|
||||
if (value) {
|
||||
searchParams.set(filterType, value);
|
||||
@@ -146,7 +148,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
<SelectItem key={task} value={task}>
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="pl-0.5 transition group-hover:text-text-bright"
|
||||
className="whitespace-nowrap pl-0.5 transition group-hover:text-text-bright"
|
||||
>
|
||||
{task}
|
||||
</Paragraph>
|
||||
@@ -156,7 +158,11 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
|
||||
<Button variant="minimal/small" onClick={() => clearFilters()} LeadingIcon={XMarkIcon} />
|
||||
{hasFilters && (
|
||||
<Button variant="minimal/small" onClick={() => clearFilters()} LeadingIcon={XMarkIcon}>
|
||||
Clear all
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,20 @@ import { SnowflakeIcon } from "lucide-react";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export const allTaskRunStatuses = [
|
||||
"WAITING_FOR_DEPLOY",
|
||||
"PENDING",
|
||||
"EXECUTING",
|
||||
"RETRYING_AFTER_FAILURE",
|
||||
"WAITING_TO_RESUME",
|
||||
"COMPLETED_SUCCESSFULLY",
|
||||
"CANCELED",
|
||||
"COMPLETED_WITH_ERRORS",
|
||||
"CRASHED",
|
||||
"INTERRUPTED",
|
||||
"SYSTEM_FAILURE",
|
||||
] as TaskRunStatus[];
|
||||
|
||||
const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
|
||||
PENDING: "Task is waiting to be executed",
|
||||
WAITING_FOR_DEPLOY: "Task needs to be deployed first to start executing",
|
||||
|
||||
@@ -104,6 +104,7 @@ const EnvironmentSchema = z.object({
|
||||
COORDINATOR_SECRET: z.string().default("coordinator-secret"),
|
||||
DEPOT_TOKEN: z.string().optional(),
|
||||
DEPOT_PROJECT_ID: z.string().optional(),
|
||||
DEPOT_ORG_ID: z.string().optional(),
|
||||
CONTAINER_REGISTRY_ORIGIN: z.string().optional(),
|
||||
CONTAINER_REGISTRY_USERNAME: z.string().optional(),
|
||||
CONTAINER_REGISTRY_PASSWORD: z.string().optional(),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTextFilter } from "./useTextFilter";
|
||||
|
||||
type Task = {
|
||||
id: string;
|
||||
friendlyId: string;
|
||||
taskIdentifier: string;
|
||||
exportName: string;
|
||||
filePath: string;
|
||||
triggerSource: string;
|
||||
};
|
||||
|
||||
export function useFilterTasks<T extends Task>({ tasks }: { tasks: T[] }) {
|
||||
return useTextFilter<T>({
|
||||
items: tasks,
|
||||
filter: (task, text) => {
|
||||
if (task.taskIdentifier.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.exportName.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.filePath.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.id.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.friendlyId.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -53,3 +53,11 @@ export function useIsNewOrganizationPage(matches?: UIMatch[]): boolean {
|
||||
export const useOrganizationChanged = (action: (org: MatchedOrganization | undefined) => void) => {
|
||||
useChanged(useOptionalOrganization, action);
|
||||
};
|
||||
|
||||
export function useIsImpersonating(matches?: UIMatch[]) {
|
||||
const data = useTypedMatchesData<typeof orgLoader>({
|
||||
id: "routes/_app.orgs.$organizationSlug",
|
||||
matches,
|
||||
});
|
||||
return data?.isImpersonating === true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { useOptimisticLocation } from "./useOptimisticLocation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type Values = Record<string, string | string[] | undefined>;
|
||||
|
||||
export function useSearchParams() {
|
||||
const navigate = useNavigate();
|
||||
const location = useOptimisticLocation();
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
const set = useCallback(
|
||||
(values: Values) => {
|
||||
for (const [param, value] of Object.entries(values)) {
|
||||
if (value === undefined) {
|
||||
search.delete(param);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
search.set(param, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
search.delete(param);
|
||||
for (const v of value) {
|
||||
search.append(param, v);
|
||||
}
|
||||
}
|
||||
},
|
||||
[location, search]
|
||||
);
|
||||
|
||||
const replace = useCallback(
|
||||
(values: Values) => {
|
||||
set(values);
|
||||
navigate(`${location.pathname}?${search.toString()}`, { replace: true });
|
||||
},
|
||||
[location, search]
|
||||
);
|
||||
|
||||
const del = useCallback(
|
||||
(keys: string | string[]) => {
|
||||
if (!Array.isArray(keys)) {
|
||||
keys = [keys];
|
||||
}
|
||||
for (const key of keys) {
|
||||
search.delete(key);
|
||||
}
|
||||
navigate(`${location.pathname}?${search.toString()}`, { replace: true });
|
||||
},
|
||||
[location, search]
|
||||
);
|
||||
|
||||
const value = useCallback(
|
||||
(param: string) => {
|
||||
return search.get(param) ?? undefined;
|
||||
},
|
||||
[location, search]
|
||||
);
|
||||
|
||||
const values = useCallback(
|
||||
(param: string) => {
|
||||
return search.getAll(param);
|
||||
},
|
||||
[location, search]
|
||||
);
|
||||
|
||||
return {
|
||||
value,
|
||||
values,
|
||||
set,
|
||||
replace,
|
||||
del,
|
||||
};
|
||||
}
|
||||
@@ -17,16 +17,22 @@ export type ShortcutDefinition =
|
||||
| Shortcut;
|
||||
|
||||
type useShortcutKeysProps = {
|
||||
shortcut: ShortcutDefinition;
|
||||
shortcut: ShortcutDefinition | undefined;
|
||||
action: (event: KeyboardEvent) => void;
|
||||
disabled?: boolean;
|
||||
enabledOnInputElements?: boolean;
|
||||
};
|
||||
|
||||
export function useShortcutKeys({ shortcut, action, disabled = false }: useShortcutKeysProps) {
|
||||
export function useShortcutKeys({
|
||||
shortcut,
|
||||
action,
|
||||
disabled = false,
|
||||
enabledOnInputElements,
|
||||
}: useShortcutKeysProps) {
|
||||
const { platform } = useOperatingSystem();
|
||||
const isMac = platform === "mac";
|
||||
const relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const relevantShortcut =
|
||||
shortcut && "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
|
||||
const keys = createKeysFromShortcut(relevantShortcut);
|
||||
useHotkeys(
|
||||
@@ -36,13 +42,17 @@ export function useShortcutKeys({ shortcut, action, disabled = false }: useShort
|
||||
},
|
||||
{
|
||||
enabled: !disabled,
|
||||
enableOnFormTags: relevantShortcut.enabledOnInputElements,
|
||||
enableOnContentEditable: relevantShortcut.enabledOnInputElements,
|
||||
enableOnFormTags: enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements,
|
||||
enableOnContentEditable: enabledOnInputElements ?? relevantShortcut?.enabledOnInputElements,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createKeysFromShortcut(shortcut: Shortcut) {
|
||||
function createKeysFromShortcut(shortcut: Shortcut | undefined) {
|
||||
if (!shortcut) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const modifiers = shortcut.modifiers;
|
||||
const character = shortcut.key;
|
||||
|
||||
|
||||
@@ -26,3 +26,9 @@ export function useUser(matches?: UIMatch[]): User {
|
||||
export function useUserChanged(callback: (user: User | undefined) => void) {
|
||||
useChanged(useOptionalUser, callback);
|
||||
}
|
||||
|
||||
export function useHasAdminAccess(matches?: UIMatch[]): boolean {
|
||||
const user = useOptionalUser(matches);
|
||||
|
||||
return Boolean(user?.admin);
|
||||
}
|
||||
|
||||
@@ -168,6 +168,12 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: {
|
||||
contains: search,
|
||||
mode: "insensitive",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { json, Session } from "@remix-run/node";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { createCookieSessionStorage } from "@remix-run/node";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
export type ToastMessage = {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { AttemptStatus, RetrieveRunResponse, RunStatus, logger } from "@trigger.dev/core/v3";
|
||||
import { TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
|
||||
import assertNever from "assert-never";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
|
||||
export class ApiRetrieveRunPresenter extends BasePresenter {
|
||||
public async call(
|
||||
friendlyId: string,
|
||||
env: AuthenticatedEnvironment,
|
||||
showSecretDetails: boolean
|
||||
): Promise<RetrieveRunResponse | undefined> {
|
||||
return this.traceWithEnv("call", env, async (span) => {
|
||||
const taskRun = await this._prisma.taskRun.findUnique({
|
||||
where: {
|
||||
friendlyId,
|
||||
runtimeEnvironmentId: env.id,
|
||||
},
|
||||
include: {
|
||||
attempts: {
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
},
|
||||
lockedToVersion: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!taskRun) {
|
||||
logger.debug("Task run not found", { friendlyId, envId: env.id });
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: taskRun.friendlyId,
|
||||
status: ApiRetrieveRunPresenter.apiStatusFromRunStatus(taskRun.status),
|
||||
taskIdentifier: taskRun.taskIdentifier,
|
||||
idempotencyKey: taskRun.idempotencyKey ?? undefined,
|
||||
version: taskRun.lockedToVersion ? taskRun.lockedToVersion.version : undefined,
|
||||
createdAt: taskRun.createdAt ?? undefined,
|
||||
updatedAt: taskRun.updatedAt ?? undefined,
|
||||
attempts: !showSecretDetails
|
||||
? []
|
||||
: taskRun.attempts.map((a) => ({
|
||||
id: a.friendlyId,
|
||||
status: ApiRetrieveRunPresenter.apiStatusFromAttemptStatus(a.status),
|
||||
createdAt: a.createdAt ?? undefined,
|
||||
updatedAt: a.updatedAt ?? undefined,
|
||||
startedAt: a.startedAt ?? undefined,
|
||||
completedAt: a.completedAt ?? undefined,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static apiStatusFromRunStatus(status: TaskRunStatus): RunStatus {
|
||||
switch (status) {
|
||||
case "WAITING_FOR_DEPLOY":
|
||||
case "PENDING": {
|
||||
return "PENDING";
|
||||
}
|
||||
case "RETRYING_AFTER_FAILURE":
|
||||
case "EXECUTING": {
|
||||
return "EXECUTING";
|
||||
}
|
||||
case "WAITING_TO_RESUME":
|
||||
case "PAUSED": {
|
||||
return "PAUSED";
|
||||
}
|
||||
case "CANCELED": {
|
||||
return "CANCELED";
|
||||
}
|
||||
case "COMPLETED_SUCCESSFULLY": {
|
||||
return "COMPLETED";
|
||||
}
|
||||
case "SYSTEM_FAILURE":
|
||||
case "INTERRUPTED":
|
||||
case "CRASHED":
|
||||
case "COMPLETED_WITH_ERRORS": {
|
||||
return "FAILED";
|
||||
}
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static apiStatusFromAttemptStatus(status: TaskRunAttemptStatus): AttemptStatus {
|
||||
switch (status) {
|
||||
case "PENDING": {
|
||||
return "PENDING";
|
||||
}
|
||||
case "PAUSED": {
|
||||
return "PAUSED";
|
||||
}
|
||||
case "EXECUTING": {
|
||||
return "EXECUTING";
|
||||
}
|
||||
case "COMPLETED": {
|
||||
return "COMPLETED";
|
||||
}
|
||||
case "FAILED": {
|
||||
return "FAILED";
|
||||
}
|
||||
case "CANCELED": {
|
||||
return "CANCELED";
|
||||
}
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
DeploymentErrorData,
|
||||
ExternalBuildData,
|
||||
TaskMetadataFailedToParseData,
|
||||
groupTaskMetadataIssuesByTask,
|
||||
} from "@trigger.dev/core/v3";
|
||||
@@ -39,6 +40,7 @@ export class DeploymentPresenter {
|
||||
const project = await this.#prismaClient.project.findFirstOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
},
|
||||
where: {
|
||||
slug: projectSlug,
|
||||
@@ -65,6 +67,9 @@ export class DeploymentPresenter {
|
||||
shortCode: true,
|
||||
version: true,
|
||||
errorData: true,
|
||||
imageReference: true,
|
||||
externalBuildData: true,
|
||||
projectId: true,
|
||||
environment: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -117,6 +122,10 @@ export class DeploymentPresenter {
|
||||
},
|
||||
});
|
||||
|
||||
const externalBuildData = deployment.externalBuildData
|
||||
? ExternalBuildData.safeParse(deployment.externalBuildData)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
deployment: {
|
||||
id: deployment.id,
|
||||
@@ -137,6 +146,11 @@ export class DeploymentPresenter {
|
||||
deployedBy: deployment.triggeredBy,
|
||||
errorData: this.#prepareErrorData(deployment.errorData),
|
||||
sdkVersion: deployment.worker?.sdkVersion,
|
||||
imageReference: deployment.imageReference,
|
||||
externalBuildData:
|
||||
externalBuildData && externalBuildData.success ? externalBuildData.data : undefined,
|
||||
projectId: deployment.projectId,
|
||||
organizationId: project.organizationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
|
||||
import parse from "parse-duration";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
@@ -15,6 +16,7 @@ type RunListOptions = {
|
||||
statuses?: TaskRunStatus[];
|
||||
environments?: string[];
|
||||
scheduleId?: string;
|
||||
period?: string;
|
||||
from?: number;
|
||||
to?: number;
|
||||
//pagination
|
||||
@@ -38,6 +40,7 @@ export class RunListPresenter extends BasePresenter {
|
||||
statuses,
|
||||
environments,
|
||||
scheduleId,
|
||||
period,
|
||||
from,
|
||||
to,
|
||||
direction = "forward",
|
||||
@@ -47,10 +50,11 @@ export class RunListPresenter extends BasePresenter {
|
||||
const hasStatusFilters = statuses && statuses.length > 0;
|
||||
|
||||
const hasFilters =
|
||||
tasks !== undefined ||
|
||||
versions !== undefined ||
|
||||
(tasks !== undefined && tasks.length > 0) ||
|
||||
(versions !== undefined && versions.length > 0) ||
|
||||
hasStatusFilters ||
|
||||
environments !== undefined ||
|
||||
(environments !== undefined && environments.length > 0) ||
|
||||
(period !== undefined && period !== "all") ||
|
||||
from !== undefined ||
|
||||
to !== undefined;
|
||||
|
||||
@@ -90,6 +94,8 @@ export class RunListPresenter extends BasePresenter {
|
||||
},
|
||||
});
|
||||
|
||||
const periodMs = period ? parse(period) : undefined;
|
||||
|
||||
//get the runs
|
||||
let runs = await this._replica.$queryRaw<
|
||||
{
|
||||
@@ -152,6 +158,11 @@ export class RunListPresenter extends BasePresenter {
|
||||
: Prisma.empty
|
||||
}
|
||||
${scheduleId ? Prisma.sql`AND tr."scheduleId" = ${scheduleId}` : Prisma.empty}
|
||||
${
|
||||
periodMs
|
||||
? Prisma.sql`AND tr."createdAt" >= NOW() - INTERVAL '1 millisecond' * ${periodMs}`
|
||||
: Prisma.empty
|
||||
}
|
||||
${
|
||||
from
|
||||
? Prisma.sql`AND tr."createdAt" >= ${new Date(from).toISOString()}::timestamp`
|
||||
@@ -224,7 +235,11 @@ export class RunListPresenter extends BasePresenter {
|
||||
next,
|
||||
previous,
|
||||
},
|
||||
possibleTasks: possibleTasks.map((task) => task.slug),
|
||||
possibleTasks: possibleTasks
|
||||
.map((task) => ({ slug: task.slug, triggerSource: task.triggerSource }))
|
||||
.sort((a, b) => {
|
||||
return a.slug.localeCompare(b.slug);
|
||||
}),
|
||||
filters: {
|
||||
tasks: tasks || [],
|
||||
versions: versions || [],
|
||||
|
||||
@@ -38,6 +38,7 @@ export class RunPresenter {
|
||||
id: true,
|
||||
type: true,
|
||||
slug: true,
|
||||
organizationId: true,
|
||||
orgMember: {
|
||||
select: {
|
||||
user: {
|
||||
@@ -66,9 +67,13 @@ export class RunPresenter {
|
||||
if (!traceSummary) {
|
||||
return {
|
||||
run: {
|
||||
id: run.id,
|
||||
number: run.number,
|
||||
friendlyId: run.friendlyId,
|
||||
traceId: run.traceId,
|
||||
environment: {
|
||||
id: run.runtimeEnvironment.id,
|
||||
organizationId: run.runtimeEnvironment.organizationId,
|
||||
type: run.runtimeEnvironment.type,
|
||||
slug: run.runtimeEnvironment.slug,
|
||||
userId: run.runtimeEnvironment.orgMember?.user.id,
|
||||
@@ -118,9 +123,13 @@ export class RunPresenter {
|
||||
|
||||
return {
|
||||
run: {
|
||||
id: run.id,
|
||||
number: run.number,
|
||||
friendlyId: run.friendlyId,
|
||||
traceId: run.traceId,
|
||||
environment: {
|
||||
id: run.runtimeEnvironment.id,
|
||||
organizationId: run.runtimeEnvironment.organizationId,
|
||||
type: run.runtimeEnvironment.type,
|
||||
slug: run.runtimeEnvironment.slug,
|
||||
userId: run.runtimeEnvironment.orgMember?.user.id,
|
||||
|
||||
@@ -42,7 +42,7 @@ export class SpanPresenter {
|
||||
const output =
|
||||
span.outputType === "application/store"
|
||||
? `/resources/packets/${span.environmentId}/${span.output}`
|
||||
: typeof span.output !== "undefined" && span.output !== null
|
||||
: typeof span.output !== "undefined"
|
||||
? await prettyPrintPacket(span.output, span.outputType ?? undefined)
|
||||
: undefined;
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
} from "~/components/primitives/SimpleSelect";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
|
||||
type FirstEndpointSheetProps = {
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
} from "~/components/primitives/SimpleSelect";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { runStatusClassNameColor, runStatusTitle } from "~/components/runs/RunStatuses";
|
||||
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
|
||||
+22
-1
@@ -9,6 +9,7 @@ import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommandV3, TriggerDevStepV3, TriggerLoginStepV3 } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
@@ -17,8 +18,9 @@ import { Callout } from "~/components/primitives/Callout";
|
||||
import { formatDateTime } from "~/components/primitives/DateTime";
|
||||
import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import {
|
||||
@@ -125,6 +127,25 @@ export default function Page() {
|
||||
<PageContainer>
|
||||
<NavBar>
|
||||
<PageTitle title="Tasks" />
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
{tasks.map((task) => (
|
||||
<Property label={task.exportName} key={task.slug}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{task.environments
|
||||
.map((e) =>
|
||||
e.userName ? `${e.userName}/${e.id}` : `${e.type.slice(0, 3)}/${e.id}`
|
||||
)
|
||||
.join(", ")}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody>
|
||||
<div className={cn("grid h-full grid-rows-1")}>
|
||||
|
||||
+16
-3
@@ -1,7 +1,8 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { BookOpenIcon, LightBulbIcon, ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { RegenerateApiKeyModal } from "~/components/environments/RegenerateApiKeyModal";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
@@ -10,8 +11,9 @@ import { Callout } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
import { PageAccessories, NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -24,7 +26,6 @@ import {
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { createEnvironment } from "~/models/organization.server";
|
||||
import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server";
|
||||
@@ -132,6 +133,18 @@ export default function Page() {
|
||||
<NavBar>
|
||||
<PageTitle title="API keys" />
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
{environments.map((environment) => (
|
||||
<Property label={environment.slug} key={environment.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{environment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<LinkButton
|
||||
variant={"minimal/small"}
|
||||
LeadingIcon={BookOpenIcon}
|
||||
|
||||
+44
-1
@@ -1,8 +1,9 @@
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { Link, useLocation } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { UserAvatar } from "~/components/UserProfilePhoto";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
@@ -68,6 +69,48 @@ export default function Page() {
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright">
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
<Header2 className={cn("whitespace-nowrap")}>Deploy: {deployment.shortCode}</Header2>
|
||||
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{deployment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Project ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{deployment.projectId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{deployment.organizationId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
{deployment.imageReference && (
|
||||
<Property label="Image">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{deployment.imageReference}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
)}
|
||||
{deployment.externalBuildData && (
|
||||
<Property label="Build Server">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
to={`/resources/${deployment.projectId}/deployments/${deployment.id}/logs`}
|
||||
className="extra-small/bright/mono underline"
|
||||
>
|
||||
{deployment.externalBuildData.buildId}
|
||||
</Link>
|
||||
</div>
|
||||
</Property>
|
||||
)}
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<LinkButton
|
||||
to={`${v3DeploymentsPath(organization, project)}${page ? `?page=${page}` : ""}`}
|
||||
variant="minimal/medium"
|
||||
|
||||
+56
@@ -67,6 +67,8 @@ import {
|
||||
v3RunsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -116,6 +118,33 @@ export default function Page() {
|
||||
title={`Run #${run.number}`}
|
||||
/>
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Trace ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.traceId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Env ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.environment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{run.environment.organizationId}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<EnvironmentLabel
|
||||
size="large"
|
||||
environment={run.environment}
|
||||
@@ -165,6 +194,33 @@ export default function Page() {
|
||||
title={`Run #${run.number}`}
|
||||
/>
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Trace ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.traceId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Env ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{run.environment.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{run.environment.organizationId}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<EnvironmentLabel size="large" environment={run.environment} userName={usernameForEnv} />
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
|
||||
+12
-3
@@ -30,8 +30,15 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { projectParam, organizationSlug } = ProjectParamSchema.parse(params);
|
||||
|
||||
const url = new URL(request.url);
|
||||
const s = Object.fromEntries(url.searchParams.entries());
|
||||
const { tasks, versions, statuses, environments, from, to, cursor, direction } =
|
||||
const s = {
|
||||
cursor: url.searchParams.get("cursor") ?? undefined,
|
||||
direction: url.searchParams.get("direction") ?? undefined,
|
||||
statuses: url.searchParams.getAll("statuses"),
|
||||
environments: url.searchParams.getAll("environments"),
|
||||
tasks: url.searchParams.getAll("tasks"),
|
||||
period: url.searchParams.get("period") ?? undefined,
|
||||
};
|
||||
const { tasks, versions, statuses, environments, period, from, to, cursor, direction } =
|
||||
TaskRunListSearchFilters.parse(s);
|
||||
|
||||
const presenter = new RunListPresenter();
|
||||
@@ -42,6 +49,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
versions,
|
||||
statuses,
|
||||
environments,
|
||||
period,
|
||||
from,
|
||||
to,
|
||||
direction: direction,
|
||||
@@ -88,10 +96,11 @@ export default function Page() {
|
||||
) : (
|
||||
<div className={cn("grid h-fit grid-cols-1 gap-4")}>
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-x-2">
|
||||
<div className="mb-2 flex items-start justify-between gap-x-2">
|
||||
<RunsFilters
|
||||
possibleEnvironments={project.environments}
|
||||
possibleTasks={list.possibleTasks}
|
||||
hasFilters={list.hasFilters}
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<ListPagination list={list} />
|
||||
|
||||
+14
@@ -4,6 +4,7 @@ import { Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
@@ -12,6 +13,7 @@ import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
@@ -93,6 +95,18 @@ export default function Page() {
|
||||
<NavBar>
|
||||
<PageTitle title="Schedules" />
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
{schedules.map((schedule) => (
|
||||
<Property label={schedule.friendlyId} key={schedule.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{schedule.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<LinkButton
|
||||
LeadingIcon={PlusIcon}
|
||||
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
|
||||
|
||||
+21
-1
@@ -3,6 +3,7 @@ import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunction, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
@@ -14,7 +15,9 @@ import { Hint } from "~/components/primitives/Hint";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
@@ -110,6 +113,23 @@ export default function Page() {
|
||||
<PageContainer>
|
||||
<NavBar>
|
||||
<PageTitle title={`${project.name} project settings`} />
|
||||
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{project.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{project.organizationId}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
|
||||
<PageBody>
|
||||
|
||||
+2
-31
@@ -30,11 +30,11 @@ import {
|
||||
} from "~/components/primitives/Table";
|
||||
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
|
||||
import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource";
|
||||
import { useFilterTasks } from "~/hooks/useFilterTasks";
|
||||
import { useLinkStatus } from "~/hooks/useLinkStatus";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useTextFilter } from "~/hooks/useTextFilter";
|
||||
import {
|
||||
SelectedEnvironment,
|
||||
TaskListItem,
|
||||
@@ -153,36 +153,7 @@ function TaskSelector({
|
||||
tasks: TaskListItem[];
|
||||
environmentSlug: string;
|
||||
}) {
|
||||
const { filterText, setFilterText, filteredItems } = useTextFilter<TaskListItem>({
|
||||
items: tasks,
|
||||
filter: (task, text) => {
|
||||
if (task.taskIdentifier.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.exportName.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.filePath.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.id.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.friendlyId.toLowerCase().includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
const { filterText, setFilterText, filteredItems } = useFilterTasks<TaskListItem>({ tasks });
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-charcoal-800 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-type
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { UserAvatar } from "~/components/UserProfilePhoto";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import {
|
||||
Alert,
|
||||
@@ -23,8 +24,9 @@ import { Button, ButtonContent, LinkButton } from "~/components/primitives/Butto
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
@@ -107,6 +109,28 @@ export default function Page() {
|
||||
<PageContainer>
|
||||
<NavBar>
|
||||
<PageTitle title="Team" />
|
||||
|
||||
<PageAccessories>
|
||||
<AdminDebugTooltip>
|
||||
<PropertyTable>
|
||||
<Property label="Org ID">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">{organization.id}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
|
||||
{members.map((member) => (
|
||||
<Property label={member.user.name} key={member.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Paragraph variant="extra-small/bright/mono">
|
||||
{member.user.email} - {member.user.id}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
))}
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody>
|
||||
<Header2>Members</Header2>
|
||||
|
||||
@@ -16,14 +16,7 @@ import { FormTitle } from "~/components/primitives/FormTitle";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
@@ -148,20 +141,24 @@ export default function NewOrganizationPage() {
|
||||
{canCreateV3Projects ? (
|
||||
<InputGroup>
|
||||
<Label htmlFor={projectVersion.id}>Project version</Label>
|
||||
<SelectGroup>
|
||||
<Select
|
||||
{...conform.input(projectVersion, { type: "select" })}
|
||||
defaultValue={defaultVersion}
|
||||
>
|
||||
<SelectTrigger width="full" size="medium">
|
||||
<SelectValue placeholder="Project version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="v2">Version 2</SelectItem>
|
||||
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Select
|
||||
{...conform.select(projectVersion)}
|
||||
defaultValue={undefined}
|
||||
variant="tertiary/medium"
|
||||
placeholder="Select version"
|
||||
dropdownIcon
|
||||
text={(value) => {
|
||||
switch (value) {
|
||||
case "v2":
|
||||
return "Version 2";
|
||||
case "v3":
|
||||
return "Version 3";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem value="v2">Version 2</SelectItem>
|
||||
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
|
||||
</Select>
|
||||
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
|
||||
</InputGroup>
|
||||
) : (
|
||||
|
||||
@@ -17,14 +17,7 @@ import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { RadioGroupItem } from "~/components/primitives/RadioButton";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { createOrganization } from "~/models/organization.server";
|
||||
@@ -143,17 +136,24 @@ export default function NewOrganizationPage() {
|
||||
{v3Enabled ? (
|
||||
<InputGroup>
|
||||
<Label htmlFor={projectVersion.id}>Project version</Label>
|
||||
<SelectGroup>
|
||||
<Select {...conform.input(projectVersion, { type: "select" })} defaultValue={"v2"}>
|
||||
<SelectTrigger width="full" size="medium">
|
||||
<SelectValue placeholder="Project version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="v2">Version 2</SelectItem>
|
||||
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Select
|
||||
{...conform.select(projectVersion)}
|
||||
defaultValue={undefined}
|
||||
variant="tertiary/medium"
|
||||
placeholder="Select version"
|
||||
dropdownIcon
|
||||
text={(value) => {
|
||||
switch (value) {
|
||||
case "v2":
|
||||
return "Version 2";
|
||||
case "v3":
|
||||
return "Version 3";
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem value="v2">Version 2</SelectItem>
|
||||
<SelectItem value="v3">Version 3 (Developer Preview)</SelectItem>
|
||||
</Select>
|
||||
<FormError id={projectVersion.errorId}>{projectVersion.error}</FormError>
|
||||
</InputGroup>
|
||||
) : (
|
||||
|
||||
@@ -15,9 +15,12 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
return redirect(confirmBasicDetailsPath());
|
||||
}
|
||||
|
||||
return typedjson({
|
||||
headers: [["Set-Cookie", await commitSession(await clearRedirectTo(request))]],
|
||||
});
|
||||
return typedjson(
|
||||
{},
|
||||
{
|
||||
headers: { "Set-Cookie": await commitSession(await clearRedirectTo(request)) },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||
import { Form } from "@remix-run/react";
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
@@ -18,10 +16,8 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { adminGetOrganizations, adminGetUsers, setV3Enabled } from "~/models/admin.server";
|
||||
import { adminGetOrganizations, setV3Enabled } from "~/models/admin.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { commitImpersonationSession, setImpersonationId } from "~/services/impersonation.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { createSearchParams } from "~/utils/searchParams";
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
runId: z.string(),
|
||||
});
|
||||
|
||||
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
if (request.method.toUpperCase() === "OPTIONS") {
|
||||
return apiCors(request, json({}));
|
||||
}
|
||||
|
||||
const authenticationResult = await authenticateApiRequest(request, {
|
||||
allowPublicKey: true,
|
||||
});
|
||||
if (!authenticationResult) {
|
||||
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
|
||||
}
|
||||
|
||||
const authenticatedEnv = authenticationResult.environment;
|
||||
|
||||
const parsed = ParamsSchema.safeParse(params);
|
||||
|
||||
if (!parsed.success) {
|
||||
return apiCors(request, json({ error: "Invalid or missing runId" }, { status: 400 }));
|
||||
}
|
||||
|
||||
const { runId } = parsed.data;
|
||||
|
||||
const showSecretDetails = authenticationResult.type === "PRIVATE";
|
||||
|
||||
const presenter = new ApiRetrieveRunPresenter();
|
||||
const result = await presenter.call(runId, authenticatedEnv, showSecretDetails);
|
||||
|
||||
if (!result) {
|
||||
return apiCors(request, json({ error: "Run not found" }, { status: 404 }));
|
||||
}
|
||||
|
||||
return apiCors(request, json(result));
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { ExternalBuildData } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { redirectBackWithErrorMessage } from "~/models/message.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectId: z.string(),
|
||||
deploymentId: z.string(),
|
||||
});
|
||||
|
||||
// Redirects to Depot.dev
|
||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
const userId = await requireUserId(request);
|
||||
const { projectId, deploymentId } = ParamsSchema.parse(params);
|
||||
|
||||
await prisma.project.findFirstOrThrow({
|
||||
where: {
|
||||
id: projectId,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const deployment = await prisma.workerDeployment.findUniqueOrThrow({
|
||||
where: {
|
||||
id: deploymentId,
|
||||
projectId: projectId,
|
||||
},
|
||||
});
|
||||
|
||||
const externalBuildData = deployment.externalBuildData
|
||||
? ExternalBuildData.safeParse(deployment.externalBuildData)
|
||||
: undefined;
|
||||
|
||||
if (!externalBuildData || externalBuildData.success === false) {
|
||||
return redirectBackWithErrorMessage(request, "No build data found for this deployment.");
|
||||
}
|
||||
|
||||
if (!env.DEPOT_ORG_ID) {
|
||||
return redirectBackWithErrorMessage(request, "No Depot organization ID found.");
|
||||
}
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: `https://depot.dev/orgs/${env.DEPOT_ORG_ID}/projects/${externalBuildData.data.projectId}/builds/${externalBuildData.data.buildId}/logs`,
|
||||
},
|
||||
});
|
||||
}
|
||||
+17
-27
@@ -21,14 +21,7 @@ import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/primitives/Select";
|
||||
import { Select, SelectItem } from "~/components/primitives/Select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -189,28 +182,25 @@ export function UpsertScheduleForm({
|
||||
<Fieldset>
|
||||
<InputGroup>
|
||||
<Label htmlFor={taskIdentifier.id}>Task</Label>
|
||||
<SelectGroup>
|
||||
<Select
|
||||
{...conform.input(taskIdentifier, { type: "select" })}
|
||||
defaultValue={schedule?.taskIdentifier}
|
||||
>
|
||||
<SelectTrigger size="medium" width="full">
|
||||
<SelectValue placeholder="Select task" className="ml-2 p-0" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{possibleTasks.map((task) => (
|
||||
<Select
|
||||
{...conform.select(taskIdentifier)}
|
||||
placeholder="Select a task"
|
||||
defaultValue={schedule?.taskIdentifier}
|
||||
heading={"Filter..."}
|
||||
items={possibleTasks}
|
||||
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
|
||||
dropdownIcon
|
||||
>
|
||||
{(matches) => (
|
||||
<>
|
||||
{matches?.map((task) => (
|
||||
<SelectItem key={task} value={task}>
|
||||
<Paragraph
|
||||
variant="extra-small"
|
||||
className="pl-0.5 transition group-hover:text-text-bright"
|
||||
>
|
||||
{task}
|
||||
</Paragraph>
|
||||
{task}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
</>
|
||||
)}
|
||||
</Select>
|
||||
<FormError id={taskIdentifier.errorId}>{taskIdentifier.error}</FormError>
|
||||
</InputGroup>
|
||||
{showGenerateField && <AIGeneratedCronField onSuccess={setCronPattern} />}
|
||||
|
||||
@@ -63,6 +63,14 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
{ spanId: newRun.spanId }
|
||||
);
|
||||
|
||||
logger.debug("Replayed run", {
|
||||
taskRunId: taskRun.id,
|
||||
taskRunFriendlyId: taskRun.friendlyId,
|
||||
newRunId: newRun.id,
|
||||
newRunFriendlyId: newRun.friendlyId,
|
||||
runPath,
|
||||
});
|
||||
|
||||
return redirectWithSuccessMessage(runPath, request, `Replaying run`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import { CpuChipIcon } from "@heroicons/react/20/solid";
|
||||
import { CircleStackIcon } from "@heroicons/react/24/outline";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { startTransition, useCallback, useMemo, useState } from "react";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import {
|
||||
ComboBox,
|
||||
ComboboxProvider,
|
||||
SelectButtonItem,
|
||||
SelectItem,
|
||||
SelectList,
|
||||
SelectPopover,
|
||||
SelectProvider,
|
||||
SelectTrigger,
|
||||
shortcutFromIndex,
|
||||
} from "~/components/primitives/Select";
|
||||
import {
|
||||
TaskRunStatusCombo,
|
||||
allTaskRunStatuses,
|
||||
runStatusTitle,
|
||||
} from "~/components/runs/v3/TaskRunStatus";
|
||||
import { useSearchParams } from "~/hooks/useSearchParam";
|
||||
import { ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
|
||||
export default function Story() {
|
||||
return (
|
||||
<div className="flex h-full max-w-full flex-wrap items-start justify-start gap-2 px-8 py-16">
|
||||
<Form className="space-y-4">
|
||||
<div className="flex gap-16">
|
||||
<Filter />
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filterTypes = [
|
||||
{
|
||||
name: "status",
|
||||
title: "Status",
|
||||
icon: <CircleStackIcon className="h-4 w-4" />,
|
||||
},
|
||||
{ name: "environment", title: "Environment", icon: <CpuChipIcon className="h-4 w-4" /> },
|
||||
];
|
||||
|
||||
type FilterType = (typeof filterTypes)[number]["name"];
|
||||
|
||||
//todo what if we had shortcut keys that would deeplink you to the appropriate filter?
|
||||
function Filter() {
|
||||
const [filterType, setFilterType] = useState<FilterType | undefined>();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const shortcut = { key: "f" };
|
||||
|
||||
const clearSearchValue = useCallback(() => {
|
||||
setSearchValue("");
|
||||
}, [setSearchValue]);
|
||||
|
||||
const filterTrigger = (
|
||||
<SelectTrigger variant={"tertiary/small"} shortcut={shortcut} tooltipTitle={"Filter runs"}>
|
||||
Filter
|
||||
</SelectTrigger>
|
||||
);
|
||||
|
||||
return (
|
||||
<ComboboxProvider
|
||||
resetValueOnHide
|
||||
setValue={(value) => {
|
||||
startTransition(() => {
|
||||
setSearchValue(value);
|
||||
});
|
||||
}}
|
||||
setOpen={(open) => {
|
||||
if (!open) {
|
||||
setFilterType(undefined);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
searchValue={searchValue}
|
||||
clearSearchValue={clearSearchValue}
|
||||
shortcut={shortcut}
|
||||
trigger={filterTrigger}
|
||||
filterType={filterType}
|
||||
setFilterType={setFilterType}
|
||||
/>
|
||||
</ComboboxProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type MenuProps = {
|
||||
searchValue: string;
|
||||
clearSearchValue: () => void;
|
||||
shortcut: ShortcutDefinition;
|
||||
trigger: React.ReactNode;
|
||||
filterType: FilterType | undefined;
|
||||
setFilterType: (filterType: FilterType | undefined) => void;
|
||||
};
|
||||
|
||||
function Menu(props: MenuProps) {
|
||||
switch (props.filterType) {
|
||||
case undefined:
|
||||
return <MainMenu {...props} />;
|
||||
case "status":
|
||||
return <Statuses {...props} />;
|
||||
case "environment":
|
||||
return <Environments {...props} />;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function MainMenu({ searchValue, clearSearchValue, setFilterType, trigger, shortcut }: MenuProps) {
|
||||
const filtered = useMemo(() => {
|
||||
return filterTypes.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<SelectProvider virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover>
|
||||
<ComboBox placeholder={"Filter..."} shortcut={shortcut} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((type, index) => (
|
||||
<SelectButtonItem
|
||||
key={type.name}
|
||||
onClick={() => {
|
||||
clearSearchValue();
|
||||
setFilterType(type.name);
|
||||
}}
|
||||
icon={type.icon}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
|
||||
>
|
||||
{type.title}
|
||||
</SelectButtonItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const statuses = allTaskRunStatuses.map((status) => ({
|
||||
title: runStatusTitle(status),
|
||||
value: status,
|
||||
}));
|
||||
|
||||
function Statuses({ trigger, clearSearchValue, shortcut, searchValue, setFilterType }: MenuProps) {
|
||||
const { values, replace } = useSearchParams();
|
||||
|
||||
const handleChange = useCallback((values: string[]) => {
|
||||
clearSearchValue();
|
||||
replace({ status: values });
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return statuses.filter((item) => item.title.toLowerCase().includes(searchValue.toLowerCase()));
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<SelectProvider value={values("status")} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
hideOnEscape={() => {
|
||||
setFilterType(undefined);
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ComboBox placeholder={"Filter by status..."} shortcut={shortcut} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((item, index) => (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
|
||||
>
|
||||
<TaskRunStatusCombo status={item.value} iconClassName="animate-none" />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const environments = [
|
||||
{
|
||||
type: "DEVELOPMENT" as const,
|
||||
},
|
||||
{
|
||||
type: "STAGING" as const,
|
||||
},
|
||||
{
|
||||
type: "PRODUCTION" as const,
|
||||
},
|
||||
];
|
||||
|
||||
function Environments({
|
||||
trigger,
|
||||
clearSearchValue,
|
||||
shortcut,
|
||||
searchValue,
|
||||
setFilterType,
|
||||
}: MenuProps) {
|
||||
const { values, replace } = useSearchParams();
|
||||
|
||||
const handleChange = useCallback((values: string[]) => {
|
||||
clearSearchValue();
|
||||
replace({ environment: values });
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return environments.filter((item) =>
|
||||
item.type.toLowerCase().includes(searchValue.toLowerCase())
|
||||
);
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<SelectProvider value={values("environment")} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
hideOnEscape={() => {
|
||||
setFilterType(undefined);
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ComboBox
|
||||
placeholder={"Filter by environment..."}
|
||||
shortcut={shortcut}
|
||||
value={searchValue}
|
||||
/>
|
||||
<SelectList>
|
||||
{filtered.map((item, index) => (
|
||||
<SelectItem
|
||||
key={item.type}
|
||||
value={item.type}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
|
||||
>
|
||||
<EnvironmentLabel environment={item} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,91 +1,272 @@
|
||||
import { Header1, Header2 } from "~/components/primitives/Headers";
|
||||
import { CircleStackIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useNavigate } from "@remix-run/react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { LogoIcon } from "~/components/LogoIcon";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectGroupLabel,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectLinkItem,
|
||||
shortcutFromIndex,
|
||||
} from "~/components/primitives/Select";
|
||||
import {
|
||||
TaskRunStatusCombo,
|
||||
allTaskRunStatuses,
|
||||
runStatusTitle,
|
||||
} from "~/components/runs/v3/TaskRunStatus";
|
||||
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
|
||||
|
||||
const branches = [
|
||||
"main",
|
||||
"0.10-stable",
|
||||
"0.11-stable",
|
||||
"0.12-stable",
|
||||
"0.13-stable",
|
||||
"0.14-stable",
|
||||
"15-stable",
|
||||
"15.6-dev",
|
||||
"16.3-dev",
|
||||
"16.4.2-dev",
|
||||
"16.8.3",
|
||||
"16.8.4",
|
||||
"16.8.5",
|
||||
"16.8.6",
|
||||
"17.0.0-dev",
|
||||
"builds/facebook-www",
|
||||
"devtools-v4-merge",
|
||||
"fabric-cleanup",
|
||||
"fabric-focus-blur",
|
||||
"gh-pages",
|
||||
"leg",
|
||||
"nativefb-enable-cache",
|
||||
"nov-main-trigger",
|
||||
"rsckeys",
|
||||
];
|
||||
|
||||
export default function Story() {
|
||||
return (
|
||||
<div className="p-20">
|
||||
<div className="flex flex-col">
|
||||
<Header1 className="mb-4">Variants</Header1>
|
||||
<Header2 className="my-4">size=small width=content</Header2>
|
||||
<SelectGroup>
|
||||
<Select name="colorScheme" defaultValue="dark">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectLabel>Color Scheme</SelectLabel>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
<SelectLabel>Other themes</SelectLabel>
|
||||
<SelectItem value="sunset">Sunset</SelectItem>
|
||||
<SelectItem value="midnight">Midnight</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="lunar">Lunar</SelectItem>
|
||||
</SelectContent>
|
||||
<div className="flex h-full max-w-full flex-wrap items-start justify-start gap-2 px-4 py-16">
|
||||
<Form className="space-y-4">
|
||||
<div className="flex gap-16">
|
||||
<ProjectSelector />
|
||||
<Statuses />
|
||||
|
||||
<Select name="static" text="Static" defaultValue={[]} shortcut={{ key: "e" }}>
|
||||
<SelectItem value={"value"} shortcut={{ key: "1" }}>
|
||||
Item 1
|
||||
</SelectItem>
|
||||
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
|
||||
Item 2
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Header2 className="my-4">size=small width=full</Header2>
|
||||
<SelectGroup>
|
||||
<Select name="colorScheme" defaultValue="dark">
|
||||
<SelectTrigger width="full">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
<SelectItem value="sunset">Sunset</SelectItem>
|
||||
<SelectItem value="midnight">Midnight</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="lunar">Lunar</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
<Select
|
||||
variant="tertiary/medium"
|
||||
text="Tertiary medium"
|
||||
defaultValue={[]}
|
||||
shortcut={{ key: "e" }}
|
||||
>
|
||||
<SelectItem value={"value"} shortcut={{ key: "1" }}>
|
||||
Item 1
|
||||
</SelectItem>
|
||||
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
|
||||
Item 2
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Header2 className="my-4">size=medium width=content</Header2>
|
||||
<SelectGroup>
|
||||
<Select name="colorScheme" defaultValue="dark">
|
||||
<SelectTrigger size="medium">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
<SelectItem value="sunset">Sunset</SelectItem>
|
||||
<SelectItem value="midnight">Midnight</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="lunar">Lunar</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
<Select variant="minimal/small" text="Minimal" defaultValue={[]} shortcut={{ key: "e" }}>
|
||||
<SelectItem value={"value"} shortcut={{ key: "1" }}>
|
||||
Item 1
|
||||
</SelectItem>
|
||||
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
|
||||
Item 2
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
<Header2 className="my-4">size=medium width=full</Header2>
|
||||
<SelectGroup>
|
||||
<Select name="colorScheme" defaultValue="dark">
|
||||
<SelectTrigger size="medium" width="full">
|
||||
<SelectValue placeholder="Theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
<SelectItem value="sunset">Sunset</SelectItem>
|
||||
<SelectItem value="midnight">Midnight</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectItem value="lunar">Lunar</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
<Select
|
||||
variant="minimal/medium"
|
||||
text="Tertiary medium"
|
||||
defaultValue={[]}
|
||||
shortcut={{ key: "e" }}
|
||||
>
|
||||
<SelectItem value={"value"} shortcut={{ key: "1" }}>
|
||||
Item 1
|
||||
</SelectItem>
|
||||
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
|
||||
Item 2
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</SelectGroup>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
name="static"
|
||||
text="Heading"
|
||||
defaultValue={[]}
|
||||
showHeading={true}
|
||||
heading="A heading"
|
||||
shortcut={{ key: "h" }}
|
||||
>
|
||||
<SelectItem value={"value"} shortcut={{ key: "1" }}>
|
||||
Item 1
|
||||
</SelectItem>
|
||||
<SelectItem value={"value2"} shortcut={{ key: "2" }}>
|
||||
Item 2
|
||||
</SelectItem>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
name="branch2"
|
||||
heading={"Filter by status..."}
|
||||
defaultValue={"main"}
|
||||
items={branches}
|
||||
>
|
||||
{(matches) => matches?.map((value) => <SelectItem key={value} value={value} />)}
|
||||
</Select>
|
||||
|
||||
<Button variant="tertiary/small">Submit</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const statuses = allTaskRunStatuses.map((status) => ({
|
||||
title: runStatusTitle(status),
|
||||
value: status,
|
||||
}));
|
||||
|
||||
function Statuses() {
|
||||
const navigate = useNavigate();
|
||||
const location = useOptimisticLocation();
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
const handleChange = useCallback((values: string[]) => {
|
||||
search.delete("status");
|
||||
for (const value of values) {
|
||||
search.append("status", value);
|
||||
}
|
||||
navigate(`${location.pathname}?${search.toString()}`, { replace: true });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Select
|
||||
name="status"
|
||||
text="Status"
|
||||
value={search.getAll("status")}
|
||||
setValue={handleChange}
|
||||
heading={"Filter by status..."}
|
||||
items={statuses}
|
||||
filter={(item, search) => item.title.toLowerCase().includes(search.toLowerCase())}
|
||||
shortcut={{ key: "s" }}
|
||||
>
|
||||
{(matches, { shortcutsEnabled }) => (
|
||||
<>
|
||||
{matches?.map((item, index) => (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled })}
|
||||
>
|
||||
<TaskRunStatusCombo status={item.value} iconClassName="animate-none" />
|
||||
</SelectItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export const projects = [
|
||||
{
|
||||
type: "section" as const,
|
||||
title: "Apple",
|
||||
items: [
|
||||
{
|
||||
title: "iTunes",
|
||||
value: "itunes",
|
||||
},
|
||||
{
|
||||
title: "App Store",
|
||||
value: "appstore",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "section" as const,
|
||||
title: "Google",
|
||||
items: [
|
||||
{
|
||||
title: "Maps",
|
||||
value: "maps",
|
||||
},
|
||||
{
|
||||
title: "Gmail",
|
||||
value: "gmail",
|
||||
},
|
||||
{
|
||||
title: "Waymo",
|
||||
value: "waymo",
|
||||
},
|
||||
{
|
||||
title: "Android",
|
||||
value: "android",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "section" as const,
|
||||
title: "Uber",
|
||||
items: [
|
||||
{
|
||||
title: "Planner",
|
||||
value: "planner",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function ProjectSelector() {
|
||||
const location = useOptimisticLocation();
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
const selected = projects
|
||||
.find((p) => p.items.some((i) => i.value === search.get("project")))
|
||||
?.items.find((i) => i.value === search.get("project"));
|
||||
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
searchParams.delete("project");
|
||||
|
||||
return (
|
||||
<Select
|
||||
name="project"
|
||||
defaultValue={selected?.value}
|
||||
text={selected?.title}
|
||||
heading="Find project..."
|
||||
icon={<LogoIcon className="h-3 w-3" />}
|
||||
items={projects}
|
||||
shortcut={{ key: "p", modifiers: ["alt"] }}
|
||||
filter={(item, search, sectionTitle) =>
|
||||
sectionTitle?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
item.title.toLowerCase().includes(search.toLowerCase())
|
||||
}
|
||||
>
|
||||
{(matches, { shortcutsEnabled, section }) => (
|
||||
<SelectGroup>
|
||||
{section && <SelectGroupLabel>{section.title}</SelectGroupLabel>}
|
||||
{matches?.map((match, index) => (
|
||||
<SelectLinkItem
|
||||
icon={<CircleStackIcon className="size-3" />}
|
||||
key={match.value}
|
||||
value={match.value}
|
||||
to={`?${searchParams.toString()}&project=${match.value}`}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled, section })}
|
||||
>
|
||||
{match.title}
|
||||
</SelectLinkItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,6 +134,10 @@ const stories: Story[] = [
|
||||
name: "Select",
|
||||
slug: "select",
|
||||
},
|
||||
{
|
||||
name: "Filter",
|
||||
slug: "filter",
|
||||
},
|
||||
{
|
||||
name: "Popover",
|
||||
slug: "popover",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
correctErrorStackTrace,
|
||||
createPacketAttributesAsJson,
|
||||
flattenAttributes,
|
||||
NULL_SENTINEL,
|
||||
isExceptionSpanEvent,
|
||||
omit,
|
||||
unflattenAttributes,
|
||||
@@ -438,21 +439,10 @@ export class EventRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = isEmptyJson(fullEvent.output)
|
||||
? null
|
||||
: unflattenAttributes(fullEvent.output as Attributes);
|
||||
const output = rehydrateJson(fullEvent.output);
|
||||
const payload = rehydrateJson(fullEvent.payload);
|
||||
|
||||
const payload = isEmptyJson(fullEvent.payload)
|
||||
? null
|
||||
: unflattenAttributes(fullEvent.payload as Attributes);
|
||||
|
||||
const show = unflattenAttributes(
|
||||
filteredAttributes(fullEvent.properties as Attributes, SemanticInternalAttributes.SHOW)
|
||||
)[SemanticInternalAttributes.SHOW] as
|
||||
| {
|
||||
actions?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
const show = rehydrateShow(fullEvent.properties);
|
||||
|
||||
const properties = sanitizedAttributes(fullEvent.properties);
|
||||
|
||||
@@ -1046,7 +1036,7 @@ function isEmptyJson(json: Prisma.JsonValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function sanitizedAttributes(json: Prisma.JsonValue): Record<string, unknown> | undefined {
|
||||
function sanitizedAttributes(json: Prisma.JsonValue) {
|
||||
if (json === null || json === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -1143,3 +1133,57 @@ function getNowInNanoseconds(): bigint {
|
||||
function getDateFromNanoseconds(nanoseconds: bigint) {
|
||||
return new Date(Number(nanoseconds) / 1_000_000);
|
||||
}
|
||||
|
||||
function rehydrateJson(json: Prisma.JsonValue): any {
|
||||
if (json === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (json === NULL_SENTINEL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
return json;
|
||||
}
|
||||
|
||||
if (typeof json === "number") {
|
||||
return json;
|
||||
}
|
||||
|
||||
if (typeof json === "boolean") {
|
||||
return json;
|
||||
}
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
return json.map((item) => rehydrateJson(item));
|
||||
}
|
||||
|
||||
if (typeof json === "object") {
|
||||
return unflattenAttributes(json as Attributes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function rehydrateShow(properties: Prisma.JsonValue): { actions?: boolean } | undefined {
|
||||
if (properties === null || properties === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof properties !== "object") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(properties)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = properties[SemanticInternalAttributes.SHOW_ACTIONS];
|
||||
|
||||
if (typeof actions === "boolean") {
|
||||
return { actions };
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
MarQSQueuePriorityStrategy,
|
||||
MessagePayload,
|
||||
QueueCapacities,
|
||||
QueueRange,
|
||||
} from "./types";
|
||||
|
||||
const tracer = trace.getTracer("marqs");
|
||||
@@ -563,7 +564,7 @@ export class MarQS {
|
||||
parentQueue
|
||||
);
|
||||
|
||||
const queues = await this.#zrangeWithScores(parentQueue, range[0], range[1]);
|
||||
const queues = await this.#getChildQueuesWithScores(parentQueue, range);
|
||||
|
||||
const queuesWithScores = await this.#calculateQueueScores(queues, calculateCapacities);
|
||||
|
||||
@@ -574,21 +575,25 @@ export class MarQS {
|
||||
selectionId
|
||||
);
|
||||
|
||||
if (typeof choice !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
span.setAttributes({
|
||||
...flattenAttributes(queues, "marqs.queues"),
|
||||
});
|
||||
span.setAttributes({
|
||||
...flattenAttributes(queuesWithScores, "marqs.queuesWithScores"),
|
||||
});
|
||||
span.setAttribute("marqs.nextRange", range);
|
||||
span.setAttribute("marqs.queueCount", queues.length);
|
||||
span.setAttribute("marqs.queueChoice", choice);
|
||||
span.setAttribute("nextRange.offset", range.offset);
|
||||
span.setAttribute("nextRange.count", range.count);
|
||||
span.setAttribute("queueCount", queues.length);
|
||||
|
||||
return choice;
|
||||
if (typeof choice !== "string") {
|
||||
span.setAttribute("noQueueChoice", true);
|
||||
|
||||
return;
|
||||
} else {
|
||||
span.setAttribute("queueChoice", choice);
|
||||
|
||||
return choice;
|
||||
}
|
||||
},
|
||||
{
|
||||
kind: SpanKind.CONSUMER,
|
||||
@@ -632,12 +637,19 @@ export class MarQS {
|
||||
});
|
||||
}
|
||||
|
||||
async #zrangeWithScores(
|
||||
async #getChildQueuesWithScores(
|
||||
key: string,
|
||||
min: number,
|
||||
max: number
|
||||
range: QueueRange
|
||||
): Promise<Array<{ value: string; score: number }>> {
|
||||
const valuesWithScores = await this.redis.zrange(key, min, max, "WITHSCORES");
|
||||
const valuesWithScores = await this.redis.zrangebyscore(
|
||||
key,
|
||||
"-inf",
|
||||
Date.now(),
|
||||
"WITHSCORES",
|
||||
"LIMIT",
|
||||
range.offset,
|
||||
range.count
|
||||
);
|
||||
const result: Array<{ value: string; score: number }> = [];
|
||||
|
||||
for (let i = 0; i < valuesWithScores.length; i += 2) {
|
||||
@@ -1537,7 +1549,7 @@ function getMarQSClient() {
|
||||
|
||||
return new MarQS({
|
||||
keysProducer: new MarQSShortKeyProducer(KEY_PREFIX),
|
||||
queuePriorityStrategy: new SimpleWeightedChoiceStrategy({ queueSelectionCount: 12 }),
|
||||
queuePriorityStrategy: new SimpleWeightedChoiceStrategy({ queueSelectionCount: 36 }),
|
||||
envQueuePriorityStrategy: new SimpleWeightedChoiceStrategy({ queueSelectionCount: 12 }),
|
||||
workers: 1,
|
||||
redis: redisOptions,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { RedisOptions } from "ioredis";
|
||||
import { MarQSQueuePriorityStrategy, PriorityStrategyChoice, QueueWithScores } from "./types";
|
||||
import {
|
||||
MarQSQueuePriorityStrategy,
|
||||
PriorityStrategyChoice,
|
||||
QueueRange,
|
||||
QueueWithScores,
|
||||
} from "./types";
|
||||
import { nanoid } from "nanoid";
|
||||
import seedrandom from "seedrandom";
|
||||
|
||||
@@ -26,9 +31,7 @@ export class DynamicWeightedChoiceStrategy implements MarQSQueuePriorityStrategy
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
nextCandidateSelection(
|
||||
parentQueue: string
|
||||
): Promise<{ range: [number, number]; selectionId: string }> {
|
||||
nextCandidateSelection(parentQueue: string): Promise<{ range: QueueRange; selectionId: string }> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -39,13 +42,18 @@ export type SimpleWeightedChoiceStrategyOptions = {
|
||||
};
|
||||
|
||||
export class SimpleWeightedChoiceStrategy implements MarQSQueuePriorityStrategy {
|
||||
private _nextRangesByParentQueue: Map<string, [number, number]> = new Map();
|
||||
private _nextRangesByParentQueue: Map<string, QueueRange> = new Map();
|
||||
private _randomGenerator = seedrandom(this.options.randomSeed);
|
||||
|
||||
constructor(private options: SimpleWeightedChoiceStrategyOptions) {}
|
||||
|
||||
private nextRangeForParentQueue(parentQueue: string) {
|
||||
return this._nextRangesByParentQueue.get(parentQueue) ?? [0, this.options.queueSelectionCount];
|
||||
private nextRangeForParentQueue(parentQueue: string): QueueRange {
|
||||
return (
|
||||
this._nextRangesByParentQueue.get(parentQueue) ?? {
|
||||
offset: 0,
|
||||
count: this.options.queueSelectionCount,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
chooseQueue(
|
||||
@@ -55,25 +63,23 @@ export class SimpleWeightedChoiceStrategy implements MarQSQueuePriorityStrategy
|
||||
): PriorityStrategyChoice {
|
||||
const filteredQueues = filterQueuesAtCapacity(queues);
|
||||
|
||||
if (filteredQueues.length === 0) {
|
||||
if (queues.length === this.options.queueSelectionCount) {
|
||||
const nextRangeForParentQueue = this.nextRangeForParentQueue(parentQueue);
|
||||
const nextRange: [number, number] = nextRangeForParentQueue
|
||||
? [
|
||||
nextRangeForParentQueue[1],
|
||||
nextRangeForParentQueue[1] + this.options.queueSelectionCount,
|
||||
]
|
||||
: [this.options.queueSelectionCount, this.options.queueSelectionCount * 2];
|
||||
// If all queues are at capacity, and we were passed the max number of queues, then we will slide the window "to the right"
|
||||
this._nextRangesByParentQueue.set(parentQueue, nextRange);
|
||||
} else {
|
||||
this._nextRangesByParentQueue.delete(parentQueue);
|
||||
}
|
||||
|
||||
return { abort: true };
|
||||
if (queues.length === this.options.queueSelectionCount) {
|
||||
const nextRangeForParentQueue = this.nextRangeForParentQueue(parentQueue);
|
||||
const nextRange: QueueRange = nextRangeForParentQueue
|
||||
? {
|
||||
offset: nextRangeForParentQueue.offset + this.options.queueSelectionCount,
|
||||
count: this.options.queueSelectionCount,
|
||||
}
|
||||
: { offset: this.options.queueSelectionCount, count: this.options.queueSelectionCount };
|
||||
// If all queues are at capacity, and we were passed the max number of queues, then we will slide the window "to the right"
|
||||
this._nextRangesByParentQueue.set(parentQueue, nextRange);
|
||||
} else {
|
||||
this._nextRangesByParentQueue.delete(parentQueue);
|
||||
}
|
||||
|
||||
this._nextRangesByParentQueue.delete(parentQueue);
|
||||
if (filteredQueues.length === 0) {
|
||||
return { abort: true };
|
||||
}
|
||||
|
||||
const queueWeights = this.#calculateQueueWeights(filteredQueues);
|
||||
|
||||
@@ -82,7 +88,7 @@ export class SimpleWeightedChoiceStrategy implements MarQSQueuePriorityStrategy
|
||||
|
||||
async nextCandidateSelection(
|
||||
parentQueue: string
|
||||
): Promise<{ range: [number, number]; selectionId: string }> {
|
||||
): Promise<{ range: QueueRange; selectionId: string }> {
|
||||
return { range: this.nextRangeForParentQueue(parentQueue), selectionId: nanoid(24) };
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { socketIo } from "../handleSocketIo.server";
|
||||
import { findCurrentWorkerDeployment } from "../models/workerDeployment.server";
|
||||
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
|
||||
import { tracer } from "../tracer.server";
|
||||
import { SEMINTATTRS_FORCE_RECORDING, tracer } from "../tracer.server";
|
||||
import { CrashTaskRunService } from "../services/crashTaskRun.server";
|
||||
|
||||
const WithTraceContext = z.object({
|
||||
@@ -67,7 +67,6 @@ export type SharedQueueConsumerOptions = {
|
||||
traceTimeoutSeconds?: number;
|
||||
nextTickInterval?: number;
|
||||
interval?: number;
|
||||
parentContext?: Context;
|
||||
};
|
||||
|
||||
export class SharedQueueConsumer {
|
||||
@@ -93,7 +92,6 @@ export class SharedQueueConsumer {
|
||||
traceTimeoutSeconds: options.traceTimeoutSeconds ?? 60, // 60 seconds
|
||||
nextTickInterval: options.nextTickInterval ?? 1000, // 1 second
|
||||
interval: options.interval ?? 100, // 100ms
|
||||
parentContext: options.parentContext ?? ROOT_CONTEXT,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,19 +191,17 @@ export class SharedQueueConsumer {
|
||||
) {
|
||||
this.#endCurrentSpan();
|
||||
|
||||
const parentContext = this._options.parentContext ?? ROOT_CONTEXT;
|
||||
|
||||
// Create a new trace
|
||||
this._currentSpan = tracer.startSpan(
|
||||
"SharedQueueConsumer.doWork()",
|
||||
{
|
||||
kind: SpanKind.CONSUMER,
|
||||
},
|
||||
parentContext
|
||||
ROOT_CONTEXT
|
||||
);
|
||||
|
||||
// Get the span trace context
|
||||
this._currentSpanContext = trace.setSpan(parentContext, this._currentSpan);
|
||||
this._currentSpanContext = trace.setSpan(ROOT_CONTEXT, this._currentSpan);
|
||||
|
||||
this._perTraceCountdown = this._options.maximumItemsPerTrace;
|
||||
this._lastNewTrace = new Date();
|
||||
|
||||
@@ -18,6 +18,8 @@ export type QueueWithScores = {
|
||||
age: number;
|
||||
};
|
||||
|
||||
export type QueueRange = { offset: number; count: number };
|
||||
|
||||
export interface MarQSKeyProducer {
|
||||
queueConcurrencyLimitKey(env: AuthenticatedEnvironment, queue: string): string;
|
||||
envConcurrencyLimitKey(env: AuthenticatedEnvironment): string;
|
||||
@@ -69,9 +71,7 @@ export interface MarQSQueuePriorityStrategy {
|
||||
*
|
||||
* @returns The scores and the selectionId for the next candidate selection
|
||||
*/
|
||||
nextCandidateSelection(
|
||||
parentQueue: string
|
||||
): Promise<{ range: [number, number]; selectionId: string }>;
|
||||
nextCandidateSelection(parentQueue: string): Promise<{ range: QueueRange; selectionId: string }>;
|
||||
}
|
||||
|
||||
export const MessagePayload = z.object({
|
||||
|
||||
@@ -172,7 +172,7 @@ export async function createBackgroundTasks(
|
||||
},
|
||||
});
|
||||
|
||||
if (taskQueue.concurrencyLimit) {
|
||||
if (typeof taskQueue.concurrencyLimit === "number") {
|
||||
await marqs?.updateQueueConcurrencyLimits(
|
||||
environment,
|
||||
taskQueue.name,
|
||||
|
||||
@@ -51,11 +51,15 @@ export class UpsertTaskScheduleService extends BaseService {
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task with identifier ${schedule.taskIdentifier} not found in project.`);
|
||||
throw new ServiceValidationError(
|
||||
`Task with identifier ${schedule.taskIdentifier} not found in project.`
|
||||
);
|
||||
}
|
||||
|
||||
if (task.triggerSource !== "SCHEDULED") {
|
||||
throw new Error(`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`);
|
||||
throw new ServiceValidationError(
|
||||
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
|
||||
);
|
||||
}
|
||||
|
||||
const result = await $transaction(this._prisma, async (tx) => {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { clientWebsocketMessages, serverWebsocketMessages } from "@trigger.dev/core/v3";
|
||||
import type { StructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
import {
|
||||
MessageCatalogToSocketIoEvents,
|
||||
ZodMessageHandler,
|
||||
ZodMessageSender,
|
||||
MessageCatalogToSocketIoEvents,
|
||||
} from "@trigger.dev/core/v3/zodMessageHandler";
|
||||
import type { StructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
import { Evt } from "evt";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { DisconnectReason, Namespace, Socket } from "socket.io";
|
||||
import { env } from "~/env.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { SharedQueueConsumer } from "./marqs/sharedQueueConsumer.server";
|
||||
import type { DisconnectReason, Namespace, Socket } from "socket.io";
|
||||
import { ROOT_CONTEXT, Span, SpanKind, trace } from "@opentelemetry/api";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
const tracer = trace.getTracer("sharedQueueConsumerPool");
|
||||
|
||||
interface SharedQueueConsumerPoolOptions {
|
||||
sender: ZodMessageSender<typeof serverWebsocketMessages>;
|
||||
@@ -22,22 +20,8 @@ interface SharedQueueConsumerPoolOptions {
|
||||
|
||||
class SharedQueueConsumerPool {
|
||||
#consumers: SharedQueueConsumer[];
|
||||
#span: Span;
|
||||
|
||||
constructor(opts: SharedQueueConsumerPoolOptions) {
|
||||
this.#span = tracer.startSpan(
|
||||
"SharedQueueConsumerPool()",
|
||||
{
|
||||
kind: SpanKind.CONSUMER,
|
||||
attributes: {
|
||||
"pool.size": opts.poolSize,
|
||||
},
|
||||
},
|
||||
ROOT_CONTEXT
|
||||
);
|
||||
|
||||
const spanContext = trace.setSpan(ROOT_CONTEXT, this.#span);
|
||||
|
||||
this.#consumers = Array(opts.poolSize)
|
||||
.fill(null)
|
||||
.map(
|
||||
@@ -45,7 +29,6 @@ class SharedQueueConsumerPool {
|
||||
new SharedQueueConsumer(opts.sender, {
|
||||
interval: env.SHARED_QUEUE_CONSUMER_INTERVAL_MS,
|
||||
nextTickInterval: env.SHARED_QUEUE_CONSUMER_NEXT_TICK_INTERVAL_MS,
|
||||
parentContext: spanContext,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -56,7 +39,6 @@ class SharedQueueConsumerPool {
|
||||
|
||||
async stop() {
|
||||
await Promise.allSettled(this.#consumers.map((consumer) => consumer.stop()));
|
||||
this.#span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +82,7 @@ export class SharedSocketConnection {
|
||||
},
|
||||
});
|
||||
|
||||
logger.log("Starting SharedQueueConsumer pool", {
|
||||
logger.debug("Starting SharedQueueConsumer pool", {
|
||||
poolSize: opts.poolSize ?? this._defaultPoolSize,
|
||||
});
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ function getTracer() {
|
||||
);
|
||||
} else {
|
||||
if (env.INTERNAL_OTEL_TRACE_LOGGING_ENABLED === "1") {
|
||||
console.log(`🔦 Tracer: Logger exporter enabled`);
|
||||
console.log(`🔦 Tracer: Logger exporter enabled (sampling = ${samplingRate})`);
|
||||
|
||||
const loggerExporter = new LoggerSpanExporter();
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
"/public/build"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.6",
|
||||
"@ariakit/react-core": "^0.4.6",
|
||||
"@aws-sdk/client-sqs": "^3.445.0",
|
||||
"@codemirror/autocomplete": "^6.3.1",
|
||||
"@codemirror/commands": "^6.1.2",
|
||||
@@ -124,11 +126,13 @@
|
||||
"lodash.omit": "^4.5.0",
|
||||
"lucide-react": "^0.229.0",
|
||||
"marked": "^4.0.18",
|
||||
"match-sorter": "^6.3.4",
|
||||
"morgan": "^1.10.0",
|
||||
"nanoid": "^3.3.4",
|
||||
"non.geist": "^1.0.2",
|
||||
"ohash": "^1.1.3",
|
||||
"openai": "^4.33.1",
|
||||
"parse-duration": "^1.1.0",
|
||||
"posthog-js": "^1.93.3",
|
||||
"posthog-node": "^3.1.3",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
|
||||
@@ -88,7 +88,10 @@ describe("SimpleWeightedChoiceStrategy", () => {
|
||||
|
||||
const nextSelection = await stategy.nextCandidateSelection("parentQueue");
|
||||
|
||||
expect(nextSelection).toEqual({ range: [3, 6], selectionId: expect.any(String) });
|
||||
expect(nextSelection).toEqual({
|
||||
range: { offset: 3, count: 3 },
|
||||
selectionId: expect.any(String),
|
||||
});
|
||||
|
||||
// Now pass some queues that have some capacity
|
||||
const chosenQueue2 = stategy.chooseQueue(
|
||||
@@ -129,7 +132,10 @@ describe("SimpleWeightedChoiceStrategy", () => {
|
||||
|
||||
const nextSelection2 = await stategy.nextCandidateSelection("parentQueue");
|
||||
|
||||
expect(nextSelection2).toEqual({ range: [0, 3], selectionId: expect.any(String) });
|
||||
expect(nextSelection2).toEqual({
|
||||
range: { offset: 6, count: 3 },
|
||||
selectionId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("should adjust the next filter range only if passed the maximum number of queues", async () => {
|
||||
@@ -167,7 +173,10 @@ describe("SimpleWeightedChoiceStrategy", () => {
|
||||
|
||||
const nextSelection = await stategy.nextCandidateSelection("parentQueue");
|
||||
|
||||
expect(nextSelection).toEqual({ range: [0, 3], selectionId: expect.any(String) });
|
||||
expect(nextSelection).toEqual({
|
||||
range: { offset: 0, count: 3 },
|
||||
selectionId: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("should adjust the next candidate range ONLY for the matching parent queue", async () => {
|
||||
@@ -182,7 +191,7 @@ describe("SimpleWeightedChoiceStrategy", () => {
|
||||
queue: "queue1",
|
||||
age: 4497,
|
||||
capacities: {
|
||||
queue: { current: 10, limit: 10 },
|
||||
queue: { current: 0, limit: 10 },
|
||||
env: { current: 0, limit: 10 },
|
||||
org: { current: 0, limit: 10 },
|
||||
},
|
||||
@@ -210,15 +219,21 @@ describe("SimpleWeightedChoiceStrategy", () => {
|
||||
"selectionId"
|
||||
);
|
||||
|
||||
expect(chosenQueue).toEqual({ abort: true });
|
||||
expect(chosenQueue).toEqual("queue1");
|
||||
|
||||
const nextSelection = await stategy.nextCandidateSelection("parentQueue2");
|
||||
|
||||
expect(nextSelection).toEqual({ range: [0, 3], selectionId: expect.any(String) });
|
||||
expect(nextSelection).toEqual({
|
||||
range: { offset: 0, count: 3 },
|
||||
selectionId: expect.any(String),
|
||||
});
|
||||
|
||||
const nextSelection2 = await stategy.nextCandidateSelection("parentQueue");
|
||||
|
||||
expect(nextSelection2).toEqual({ range: [3, 6], selectionId: expect.any(String) });
|
||||
expect(nextSelection2).toEqual({
|
||||
range: { offset: 3, count: 3 },
|
||||
selectionId: expect.any(String),
|
||||
});
|
||||
|
||||
const chosenQueue2 = stategy.chooseQueue(
|
||||
[
|
||||
@@ -250,14 +265,52 @@ describe("SimpleWeightedChoiceStrategy", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
"parentQueue2",
|
||||
"parentQueue",
|
||||
"selectionId"
|
||||
);
|
||||
|
||||
expect(chosenQueue2).toEqual("queue3");
|
||||
expect(chosenQueue2).toEqual("queue2");
|
||||
|
||||
const nextSelection3 = await stategy.nextCandidateSelection("parentQueue2");
|
||||
const nextSelection3 = await stategy.nextCandidateSelection("parentQueue");
|
||||
|
||||
expect(nextSelection3).toEqual({ range: [0, 3], selectionId: expect.any(String) });
|
||||
expect(nextSelection3).toEqual({
|
||||
range: { offset: 6, count: 3 },
|
||||
selectionId: expect.any(String),
|
||||
});
|
||||
|
||||
// Not passed 3 queues, so the range should be reset (we've reached the end)
|
||||
const chosenQueue3 = stategy.chooseQueue(
|
||||
[
|
||||
{
|
||||
queue: "queue1",
|
||||
age: 4497,
|
||||
capacities: {
|
||||
queue: { current: 0, limit: 10 },
|
||||
env: { current: 0, limit: 10 },
|
||||
org: { current: 0, limit: 10 },
|
||||
},
|
||||
},
|
||||
{
|
||||
queue: "queue2",
|
||||
age: 19670,
|
||||
capacities: {
|
||||
queue: { current: 0, limit: 10 },
|
||||
env: { current: 0, limit: 10 },
|
||||
org: { current: 0, limit: 10 },
|
||||
},
|
||||
},
|
||||
],
|
||||
"parentQueue",
|
||||
"selectionId"
|
||||
);
|
||||
|
||||
expect(chosenQueue3).toEqual("queue2");
|
||||
|
||||
const nextSelection4 = await stategy.nextCandidateSelection("parentQueue");
|
||||
|
||||
expect(nextSelection4).toEqual({
|
||||
range: { offset: 0, count: 3 },
|
||||
selectionId: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,21 @@ export const client = new TriggerClient({
|
||||
});
|
||||
```
|
||||
|
||||
## 2. Set your environment variables on your server
|
||||
## 2. Include your Jobs in `sideEffects` if needed
|
||||
|
||||
If your `package.json` does not declare `sideEffects`, you can safely skip this step. Otherwise, include a glob pattern that matches your jobs so that they are not tree shaken away in your production bundle.
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"name": "my-app",
|
||||
"sideEffects": [
|
||||
"./src/jobs/**/*.ts"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Set your environment variables on your server
|
||||
|
||||
Ensure that your `TRIGGER_API_KEY` (or whatever you've set it to) environment variable is set on your server, to the correct value from the "Environments & API Keys" page in your Trigger.dev dashboard.
|
||||
|
||||
@@ -31,11 +45,11 @@ The exact instructions will vary depending on where you deploy to. Here are the
|
||||
- [Cloudflare Workers](https://developers.cloudflare.com/workers/platform/environment-variables/)
|
||||
- [Netlify](https://docs.netlify.com/environment-variables/overview/)
|
||||
|
||||
## 3. Deploy your code as usual
|
||||
## 4. Deploy your code as usual
|
||||
|
||||
Your Job code lives in your codebase, so you can deploy it as you normally would.
|
||||
|
||||
## 4. Connect the Endpoint for the first time
|
||||
## 5. Connect the Endpoint for the first time
|
||||
|
||||
1. Go to the "Environments & API Keys" page in your Trigger.dev dashboard
|
||||

|
||||
|
||||
+71
-22
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Trigger.dev",
|
||||
"openapi": ["/openapi.yml", "/v3-openapi.json"],
|
||||
"versions": ["v3 (Developer Preview)", "v2"],
|
||||
"openapi": [
|
||||
"/openapi.yml",
|
||||
"/v3-openapi.json"
|
||||
],
|
||||
"versions": [
|
||||
"v3 (Developer Preview)",
|
||||
"v2"
|
||||
],
|
||||
"logo": {
|
||||
"dark": "/logo/dark.png",
|
||||
"light": "/logo/light.png",
|
||||
@@ -90,7 +96,9 @@
|
||||
{
|
||||
"group": "",
|
||||
"version": "v3 (Developer Preview)",
|
||||
"pages": ["v3/introduction"]
|
||||
"pages": [
|
||||
"v3/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Getting Started",
|
||||
@@ -113,7 +121,12 @@
|
||||
"v3/apikeys",
|
||||
{
|
||||
"group": "Task types",
|
||||
"pages": ["v3/tasks-regular", "v3/tasks-scheduled", "v3/tasks-zod", "v3/tasks-webhooks"]
|
||||
"pages": [
|
||||
"v3/tasks-regular",
|
||||
"v3/tasks-scheduled",
|
||||
"v3/tasks-zod",
|
||||
"v3/tasks-webhooks"
|
||||
]
|
||||
},
|
||||
"v3/trigger-config"
|
||||
]
|
||||
@@ -121,7 +134,10 @@
|
||||
{
|
||||
"group": "Development",
|
||||
"version": "v3 (Developer Preview)",
|
||||
"pages": ["v3/cli-dev", "v3/run-tests"]
|
||||
"pages": [
|
||||
"v3/cli-dev",
|
||||
"v3/run-tests"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Deployment",
|
||||
@@ -132,7 +148,9 @@
|
||||
"v3/github-actions",
|
||||
{
|
||||
"group": "Deployment integrations",
|
||||
"pages": ["v3/vercel-integration"]
|
||||
"pages": [
|
||||
"v3/vercel-integration"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -208,7 +226,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Objects",
|
||||
"pages": ["v3/reference-context"]
|
||||
"pages": [
|
||||
"v3/reference-context"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "CLI",
|
||||
@@ -226,9 +246,7 @@
|
||||
{
|
||||
"group": "Runs API",
|
||||
"pages": [
|
||||
"v3/management-start-run",
|
||||
"v3/management-get-run",
|
||||
"v3/management-get-runs",
|
||||
"v3/management-retrieve-run",
|
||||
"v3/management-replay-run",
|
||||
"v3/management-cancel-run"
|
||||
]
|
||||
@@ -259,7 +277,11 @@
|
||||
{
|
||||
"group": "Open source",
|
||||
"version": "v3 (Developer Preview)",
|
||||
"pages": ["v3/github-repo", "v3/open-source-self-hosting", "v3/open-source-contributing"]
|
||||
"pages": [
|
||||
"v3/github-repo",
|
||||
"v3/open-source-self-hosting",
|
||||
"v3/open-source-contributing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Help",
|
||||
@@ -460,7 +482,10 @@
|
||||
"pages": [
|
||||
{
|
||||
"group": "Airtable",
|
||||
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
|
||||
"pages": [
|
||||
"integrations/apis/airtable",
|
||||
"integrations/apis/airtable-tasks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "GitHub",
|
||||
@@ -486,16 +511,25 @@
|
||||
},
|
||||
{
|
||||
"group": "Plain",
|
||||
"pages": ["integrations/apis/plain", "integrations/apis/plain-tasks"]
|
||||
"pages": [
|
||||
"integrations/apis/plain",
|
||||
"integrations/apis/plain-tasks"
|
||||
]
|
||||
},
|
||||
"integrations/apis/replicate",
|
||||
{
|
||||
"group": "SendGrid",
|
||||
"pages": ["integrations/apis/sendgrid", "integrations/apis/sendgrid-tasks"]
|
||||
"pages": [
|
||||
"integrations/apis/sendgrid",
|
||||
"integrations/apis/sendgrid-tasks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Resend",
|
||||
"pages": ["integrations/apis/resend", "integrations/apis/resend-tasks"]
|
||||
"pages": [
|
||||
"integrations/apis/resend",
|
||||
"integrations/apis/resend-tasks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Shopify",
|
||||
@@ -507,7 +541,10 @@
|
||||
},
|
||||
{
|
||||
"group": "Slack",
|
||||
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
|
||||
"pages": [
|
||||
"integrations/apis/slack",
|
||||
"integrations/apis/slack-tasks"
|
||||
]
|
||||
},
|
||||
"integrations/apis/stripe",
|
||||
{
|
||||
@@ -532,7 +569,9 @@
|
||||
"sdk/triggerclient/constructor",
|
||||
{
|
||||
"group": "Instance properties",
|
||||
"pages": ["sdk/triggerclient/store"]
|
||||
"pages": [
|
||||
"sdk/triggerclient/store"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Instance methods",
|
||||
@@ -595,7 +634,10 @@
|
||||
"sdk/dynamictrigger/constructor",
|
||||
{
|
||||
"group": "Instance methods",
|
||||
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
|
||||
"pages": [
|
||||
"sdk/dynamictrigger/register",
|
||||
"sdk/dynamictrigger/unregister"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -606,7 +648,10 @@
|
||||
"sdk/dynamicschedule/constructor",
|
||||
{
|
||||
"group": "Instance methods",
|
||||
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
|
||||
"pages": [
|
||||
"sdk/dynamicschedule/register",
|
||||
"sdk/dynamicschedule/unregister"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -618,7 +663,9 @@
|
||||
},
|
||||
{
|
||||
"group": "HTTP Reference",
|
||||
"pages": ["sdk/api-reference/events/create-an-event"]
|
||||
"pages": [
|
||||
"sdk/api-reference/events/create-an-event"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "React SDK",
|
||||
@@ -633,7 +680,9 @@
|
||||
{
|
||||
"group": "Overview",
|
||||
"version": "v2",
|
||||
"pages": ["examples/introduction"]
|
||||
"pages": [
|
||||
"examples/introduction"
|
||||
]
|
||||
}
|
||||
],
|
||||
"footerSocials": {
|
||||
@@ -641,4 +690,4 @@
|
||||
"github": "https://github.com/triggerdotdev",
|
||||
"linkedin": "https://www.linkedin.com/company/triggerdotdev"
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
-2
@@ -493,7 +493,7 @@
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "typescript",
|
||||
"source": "const handle = await runs.replay(existingRun.id);"
|
||||
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nconst handle = await runs.replay(\"run_1234\");"
|
||||
},
|
||||
{
|
||||
"lang": "sh",
|
||||
@@ -601,7 +601,7 @@
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "typescript",
|
||||
"source": "await runs.cancel(existingRun.id);"
|
||||
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nawait runs.cancel(\"run_1234\");"
|
||||
},
|
||||
{
|
||||
"lang": "sh",
|
||||
@@ -609,6 +609,107 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v3/runs/{run_id}": {
|
||||
"get": {
|
||||
"description": "Retrieve a run",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "run_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The ID of an existing run. When you trigger a run you will get an id in the response."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RetrieveRunResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Invalid or missing run ID"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Invalid or Missing API key"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Resource not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Run not found"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"run"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"operationId": "retrieve_run_v1",
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "typescript",
|
||||
"source": "import { runs } from \"@trigger.dev/sdk/v3\";\n\nawait runs.retrieve(\"run_1234\");"
|
||||
},
|
||||
{
|
||||
"lang": "sh",
|
||||
"source": "curl --request GET \\\n\t--url https://api.trigger.dev/api/v3/runs/{run_id} \\\n\t--header 'Authorization: Bearer <token>'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -620,6 +721,94 @@
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"RetrieveRunResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"status",
|
||||
"taskIdentifier",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"attempts"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"PENDING",
|
||||
"EXECUTING",
|
||||
"PAUSED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"CANCELED"
|
||||
]
|
||||
},
|
||||
"taskIdentifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"idempotencyKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"attempts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"status",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"PENDING",
|
||||
"EXECUTING",
|
||||
"PAUSED",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"CANCELED"
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"startedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"completedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateScheduleOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -10,3 +10,37 @@ When you [trigger a task](/v3/triggering) from your backend code, you need to se
|
||||
Each environment has its own secret key. You can find the value on the API keys page in the Trigger.dev dashboard:
|
||||
|
||||

|
||||
|
||||
### Automatically Configuring the SDK
|
||||
|
||||
To automatically configure the SDK with your secret key, you can set the `TRIGGER_SECRET_KEY` environment variable. The SDK will automatically use this value when calling API methods (like `trigger`).
|
||||
|
||||
```bash
|
||||
export TRIGGER_SECRET_KEY=tr_dev_…
|
||||
```
|
||||
|
||||
You can do the same if you are self-hosting and need to change the default URL by using `TRIGGER_API_URL`.
|
||||
|
||||
```bash
|
||||
export TRIGGER_API_URL=https://trigger.example.com
|
||||
```
|
||||
|
||||
The default URL is `https://api.trigger.dev`.
|
||||
|
||||
### Manually Configuring the SDK
|
||||
|
||||
If you prefer to manually configure the SDK, you can call the `configure` method:
|
||||
|
||||
```ts
|
||||
import { configure } from "@trigger.dev/sdk/v3";
|
||||
import { myTask } from "./trigger/myTasks";
|
||||
|
||||
configure({
|
||||
secretKey: "tr_dev_1234", // WARNING: Never actually hardcode your secret key like this
|
||||
baseURL: "https://mytrigger.example.com", // Optional
|
||||
});
|
||||
|
||||
async function triggerTask() {
|
||||
await myTask.trigger({ userId: "1234" }); // This will use the secret key and base URL you configured
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,6 +29,8 @@ This task will retry 10 times with exponential backoff.
|
||||
One way to gain reliability is to break your work into smaller tasks and [trigger](/v3/triggering) them from each other. Each task can have its own retrying behavior:
|
||||
|
||||
```ts /trigger/multiple-tasks.ts
|
||||
import { task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
retry: {
|
||||
@@ -66,7 +68,7 @@ We provide some useful functions that you can use to retry smaller parts of a ta
|
||||
You can retry a block of code that can throw an error, with the same retry settings as a task.
|
||||
|
||||
```ts /trigger/retry-on-throw.ts
|
||||
import { task, logger, retry } from "@trigger.dev/sdk/v3"
|
||||
import { task, logger, retry } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const retryOnThrow = task({
|
||||
id: "retry-on-throw",
|
||||
@@ -102,6 +104,8 @@ You can use `fetch`, `axios`, or any other library in your code.
|
||||
But we do provide a convenient function to perform HTTP requests with conditional retrying based on the response:
|
||||
|
||||
```ts /trigger/retry-fetch.ts
|
||||
import { task, logger, retry } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const taskWithFetchRetries = task({
|
||||
id: "task-with-fetch-retries",
|
||||
run: async ({ payload, ctx }) => {
|
||||
@@ -195,6 +199,8 @@ In this complicated example:
|
||||
- If we've run out of requests or tokens we retry at the time specified in the headers.
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const openaiTask = task({
|
||||
id: "openai-task",
|
||||
retry: {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Retrieve run"
|
||||
openapi: "v3-openapi GET /api/v3/runs/{run_id}"
|
||||
---
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/airtable
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/airtable",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "Trigger.dev integration for airtable",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -25,8 +25,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"airtable": "^0.12.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/github
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/github",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "The official GitHub integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -30,8 +30,8 @@
|
||||
"@octokit/request-error": "^5.0.1",
|
||||
"@octokit/webhooks": "^12.0.10",
|
||||
"octokit": "^3.1.2",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/linear
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/linear",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "Trigger.dev integration for @linear/sdk",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@linear/sdk": "^8.0.0",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/openai",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "The official OpenAI integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"openai": "^4.16.1",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24"
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/plain
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/plain",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "The official Plain.com integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -24,8 +24,8 @@
|
||||
"build:tsup": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"@team-plain/typescript-sdk": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/replicate
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/replicate",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "Trigger.dev integration for replicate",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -25,8 +25,8 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"replicate": "^0.18.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/resend
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/resend",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "The official Resend.com integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -24,8 +24,8 @@
|
||||
"build:tsup": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"resend": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/sendgrid
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/sendgrid",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "Trigger.dev integration for @sendgrid/mail",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24"
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# @trigger.dev/shopify
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1fd26ff98: improved error messages when a shopify webhook fails to register
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/shopify",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "Trigger.dev integration for @shopify/shopify-api",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@shopify/shopify-api": "^8.0.2",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -63,8 +63,30 @@ export class Shopify implements TriggerIntegration {
|
||||
throw `Can't create Shopify integration (${options.id}) as apiKey was undefined`;
|
||||
}
|
||||
|
||||
if (Object.keys(options).includes("apiSecretKey") && !options.apiSecretKey) {
|
||||
throw `Can't create Shopify integration (${options.id}) as apiSecretKey was undefined`;
|
||||
}
|
||||
|
||||
if (Object.keys(options).includes("adminAccessToken") && !options.adminAccessToken) {
|
||||
throw `Can't create Shopify integration (${options.id}) as adminAccessToken was undefined`;
|
||||
}
|
||||
|
||||
if (Object.keys(options).includes("hostName") && !options.hostName) {
|
||||
throw `Can't create Shopify integration (${options.id}) as hostName was undefined`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
this._shopDomain = this._options.hostName.replace("http://", "").replace("https://", "");
|
||||
// Extract the shop domain if user has entered the full URL
|
||||
this._shopDomain = this._options.hostName
|
||||
.replace(/^https?:\/\//, "") // Remove http:// or https://
|
||||
.replace(/\/$/, ""); // Remove trailing slash if it exists (e.g. `example.myshopify.com/`)
|
||||
|
||||
// Regular expression to ensure the shopDomain is a valid `.myshopify.com` domain
|
||||
const shopifyDomainPattern = /^[a-zA-Z0-9-]+\.myshopify\.com$/;
|
||||
|
||||
if (!shopifyDomainPattern.test(this._shopDomain)) {
|
||||
throw `Can't create Shopify integration (${options.id}) because hostName should be a valid ".myshopify.com" domain, not a custom primary domain. For example: my-domain.myshopify.com`;
|
||||
}
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
|
||||
@@ -86,21 +86,36 @@ export function createWebhookEventSource(integration: Shopify) {
|
||||
key: (params) => params.topic,
|
||||
crud: {
|
||||
create: async ({ io, ctx }) => {
|
||||
const webhook = await io.integration.rest.Webhook.save("create-webhook", {
|
||||
fromData: {
|
||||
address: ctx.url,
|
||||
topic: ctx.params.topic,
|
||||
// fields: ctx.params.fields,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const webhook = await io.integration.rest.Webhook.save("create-webhook", {
|
||||
fromData: {
|
||||
address: ctx.url,
|
||||
topic: ctx.params.topic,
|
||||
// fields: ctx.params.fields,
|
||||
},
|
||||
});
|
||||
|
||||
const clientSecret = await io.integration.runTask(
|
||||
"get-client-secret",
|
||||
async (client) => client.config.apiSecretKey
|
||||
);
|
||||
if (!webhook.id) {
|
||||
throw new Error(
|
||||
"Failed to create webhook. Ensure your Shopfiy client configuration is correct. Have you set the correct access scopes? Are you using the primary myshopify.com domain?"
|
||||
);
|
||||
}
|
||||
|
||||
await io.store.job.set("set-id", "webhook-id", webhook.id);
|
||||
await io.store.job.set("set-secret", "webhook-secret", clientSecret);
|
||||
const clientSecret = await io.integration.runTask(
|
||||
"get-client-secret",
|
||||
async (client) => client.config.apiSecretKey
|
||||
);
|
||||
|
||||
await io.store.job.set("set-id", "webhook-id", webhook.id);
|
||||
await io.store.job.set("set-secret", "webhook-secret", clientSecret);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await io.logger.error(`Failed to create webhook: ${error.message}`);
|
||||
} else {
|
||||
await io.logger.error("Failed to create webhook", { rawError: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
delete: async ({ io, ctx }) => {
|
||||
const webhookId = await io.store.job.get<number>("get-webhook-id", "webhook-id");
|
||||
@@ -109,23 +124,41 @@ export function createWebhookEventSource(integration: Shopify) {
|
||||
throw new Error("Missing webhook ID for delete operation.");
|
||||
}
|
||||
|
||||
await io.integration.rest.Webhook.delete("delete-webhook", {
|
||||
id: webhookId,
|
||||
});
|
||||
try {
|
||||
await io.integration.rest.Webhook.delete("delete-webhook", {
|
||||
id: webhookId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await io.logger.error(`Failed to delete webhook: ${error.message}`);
|
||||
} else {
|
||||
await io.logger.error("Failed to delete webhook", { rawError: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
await io.store.job.delete("delete-webhook-id", "webhook-id");
|
||||
},
|
||||
update: async ({ io, ctx }) => {
|
||||
const webhookId = await io.store.job.get<number>("get-webhook-id", "webhook-id");
|
||||
|
||||
await io.integration.rest.Webhook.save("update-webhook", {
|
||||
fromData: {
|
||||
id: webhookId,
|
||||
address: ctx.url,
|
||||
topic: ctx.params.topic,
|
||||
// fields: ctx.params.fields,
|
||||
},
|
||||
});
|
||||
try {
|
||||
await io.integration.rest.Webhook.save("update-webhook", {
|
||||
fromData: {
|
||||
id: webhookId,
|
||||
address: ctx.url,
|
||||
topic: ctx.params.topic,
|
||||
// fields: ctx.params.fields,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
await io.logger.error(`Failed to update webhook: ${error.message}`);
|
||||
} else {
|
||||
await io.logger.error("Failed to update webhook", { rawError: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
verify: async ({ request, client, ctx }) => {
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
# @trigger.dev/slack
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/slack",
|
||||
"version": "3.0.0-beta.24",
|
||||
"version": "3.0.0-beta.29",
|
||||
"description": "The official Slack integration for Trigger.dev",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@slack/web-api": "^6.8.1",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.24",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
# @trigger.dev/stripe
|
||||
|
||||
## 3.0.0-beta.29
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.29
|
||||
- @trigger.dev/sdk@3.0.0-beta.29
|
||||
|
||||
## 3.0.0-beta.28
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d9dfbc75]
|
||||
- @trigger.dev/sdk@3.0.0-beta.28
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.28
|
||||
|
||||
## 3.0.0-beta.27
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [203e00208]
|
||||
- @trigger.dev/sdk@3.0.0-beta.27
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.27
|
||||
|
||||
## 3.0.0-beta.26
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.26
|
||||
- @trigger.dev/sdk@3.0.0-beta.26
|
||||
|
||||
## 3.0.0-beta.25
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.25
|
||||
- @trigger.dev/sdk@3.0.0-beta.25
|
||||
|
||||
## 3.0.0-beta.24
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user