Compare commits

...

20 Commits

Author SHA1 Message Date
nicktrn cfe4b5409a Fix lockfile 2024-05-08 17:27:34 +01:00
github-actions[bot] 223768a025 chore: Update version for release (beta) (#1091)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-08 17:25:22 +01:00
Ramadan Omar 1fd26ff98b Improvement: Added more context to Shopify webhook registering errors (#1090)
* Improvement: Added more context to Shopify webhook registering errors

Co-authored-by: Ahmed Ramadan <ahmedramadan1337@gmail.com>

* revert: shopify reference jobs

* fix: added error handling to some shopify client edge cases

* small changes to crud error handling

---------

Co-authored-by: Ahmed Ramadan <ahmedramadan1337@gmail.com>
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
2024-05-08 17:22:59 +01:00
Eric Allam 8d34c63c2d Fix issue when concurrency limit is set to 0 on a queue 2024-05-08 10:19:06 +01:00
Zafer Cesur ff04c986a1 Add notice on sideEffects in deployment guide (#680)
Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-05-07 22:58:27 +01:00
Matt Aitken 7d6430dc46 New filtered dropdown and run filters (#1089)
* Extract out useFilterTasks

* WIP on Combobox

* Revert "WIP on Combobox"

This reverts commit f0a9b2e57c67f24d5e0876630e17a54252ceb86b.

* Added ariakit

* Started adding a Listbox component

* Listbox WIP

* More WIP on the list box

* More work on the Listbox

* WIP on filterable Listbox using headless… but it’s going to be a nightmare to make it good. Wrong approach

* WIP using Ariakit

* Multiple selection working

* WIP in reworking it so search is built in and it’s easier to use

* Nicer API for listbox

* Allow sections to be rendered with filtering

* More progress on the styling of the menu

* The empty state

* Removed focus outline

* Allow filtering based on the section title

* Render the shortcut correctly

* Allow passing static children to Select and render a separator if a section has no title

* Static example and added subtle focus

* Allow static data with no items array passed in

* Allow shortcuts on select items

* Only how heading if true

* Tooltip for keyboard shortcut

* useShortcutKeys fixes

- Can be passed an undefined definition (so we render the same number of hooks always).
- Fix for enabledOnInputElements not being used…

* Shortcut keys working with lists

* Don’t render shortcut numbers higher than 0

* More improvements

* WIP on a Link row

* Nicer API for the children function

* Much nicer shortcut key support

* Separated SelectTrigger out

* Heading separated out

* More styled elements created

* Removed unused element

* Reordered statuses

* Added search context

* Fixes for default values

* Removed tab elements

* Combo box

* Simplified it more

* Started work on Filter menu

* Experiments with filter menu

* Filter menu proof of concept working

* Nice hook for using a search param

* The escape key goes back to the previous menu

* Added filter page to storybook

* Added other variants

* Renamed Select file to OldSelect, implemented Feedback form

* Added placeholder support

* Added an optional dropdown icon (defaults to a chevron down)

* Feedback new Select

* Feedback just use static items

* New project page

* Renamed storybook pages

* Lots of progress on the new run filters

* Added new tasks filters

* Show the applied filters

* Added period filtering

* Fix for escape resetting the period value to the previous value when escape is pressed

* Show the Clear button sooner

* Removed unused code

* Reworked useSearchParams so multiple values can be set at once…

* Renamed file: OldSelect -> SImpleSelect

* Fix for new Select not working in forms because props weren’t being passed through

* New Select on the schedules page

* Better layout when there are lots of run filters

* Many style improvements to run filters

* Status filter allows you to select as well

* Rolled out the menus for the other files

* Minor improvements to schedule page and page padding

* Latest lockfile
2024-05-07 22:55:31 +01:00
Dan b289aa31da Readme updates - removed old roadmap and added dev preview EA link (#1078)
* Removed outdated roadmap

* Added link to the dev preview article
2024-05-07 22:55:00 +01:00
Eric Allam 0508de6258 Release 3.0.0-beta.28 2024-05-06 12:35:27 +01:00
github-actions[bot] 56a063e586 chore: Update version for release (beta) (#1087)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-06 12:34:43 +01:00
Eric Allam d490bc5cbf Add the "log" level back in as an alias to "info" 2024-05-06 12:28:17 +01:00
Eric Allam 6d9dfbc75d Add configure function to be able to configure the SDK manually
And move the ApiClient configuration away from AsyncLocalStorage and use our globals system instead
2024-05-06 12:12:30 +01:00
Eric Allam 6406924b02 Ensure @trigger.dev/sdk and @trigger.dev/core are always in the list of deployed dependencies 2024-05-06 11:01:49 +01:00
Eric Allam 2156e15266 Adding some additional telemetry during deploy to help debug issues 2024-05-06 10:34:54 +01:00
Eric Allam 49b4d47726 Release 3.0.0-beta.27 packages 2024-05-05 08:20:07 +01:00
github-actions[bot] 2e4f5d893c chore: Update version for release (beta) (#1086)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-05 08:19:13 +01:00
Eric Allam 203e002087 Add runs.retrieve management API method to get info about a run by run ID 2024-05-04 22:24:42 +01:00
Eric Allam 1670c4c41d Remove "log" Log Level, unify log and info messages under the "info" log level 2024-05-03 14:55:24 +01:00
Eric Allam 9d6c424236 Add deploy admin debug tooltips 2024-05-03 11:22:51 +01:00
nicktrn cffc88aeac Increase indexing specs 2024-05-03 09:45:18 +01:00
nicktrn bce398b892 increase indexing specs 2024-05-03 09:36:16 +01:00
136 changed files with 4190 additions and 939 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/shopify": patch
---
improved error messages when a shopify webhook fails to register
+6
View File
@@ -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
+6
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Adding some additional telemetry during deploy to help debug issues
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Add the "log" level back in as an alias to "info"
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Add configure function to be able to configure the SDK manually
+7
View File
@@ -47,11 +47,13 @@
"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",
@@ -60,6 +62,7 @@
"eleven-paws-join",
"few-students-share",
"funny-swans-destroy",
"gorgeous-gorillas-compete",
"green-bags-wink",
"khaki-apricots-design",
"khaki-poems-lay",
@@ -69,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",
@@ -77,6 +82,7 @@
"new-rivers-tell",
"ninety-pets-travel",
"odd-poets-own",
"pink-pumas-rhyme",
"polite-ducks-switch",
"polite-rockets-matter",
"poor-flowers-cross",
@@ -107,6 +113,7 @@
"tame-guests-know",
"tender-moose-tell",
"tender-oranges-rhyme",
"thin-parents-heal",
"tidy-balloons-suffer",
"tidy-dryers-sleep",
"tiny-doors-type",
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Ensure @trigger.dev/sdk and @trigger.dev/core are always in the list of deployed dependencies
+4 -26
View File
@@ -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
Were 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).
+2 -2
View File
@@ -80,8 +80,8 @@ class KubernetesTaskOperations implements TaskOperations {
],
resources: {
limits: {
cpu: "250m",
memory: "0.5G",
cpu: "1",
memory: "1G",
"ephemeral-storage": "2Gi",
},
},
+19 -26
View File
@@ -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">
@@ -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>
);
}
+601 -120
View File
@@ -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,
+642 -213
View File
@@ -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",
+1
View File
@@ -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(),
+43
View File
@@ -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;
},
});
}
+76
View File
@@ -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,
};
}
+16 -6
View File
@@ -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;
@@ -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 || [],
@@ -27,7 +27,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/primitives/Select";
} from "~/components/primitives/SimpleSelect";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
type FirstEndpointSheetProps = {
@@ -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";
@@ -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"
@@ -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} />
@@ -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">
@@ -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>
) : (
+19 -19
View File
@@ -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>
) : (
@@ -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`,
},
});
}
@@ -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} />}
@@ -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>
);
}
+257 -76
View File
@@ -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",
@@ -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) => {
+4
View File
@@ -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",
+17 -3
View File
@@ -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
![Go to the Environments & API Keys page ](/images/environments-link.png)
+71 -22
View File
@@ -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
View File
@@ -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": {
+34
View File
@@ -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:
![How to find your secret key](/images/v3/api-keys.png)
### 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
}
```
+4
View File
@@ -0,0 +1,4 @@
---
title: "Retrieve run"
openapi: "v3-openapi GET /api/v3/runs/{run_id}"
---
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@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"
},
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.26"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29"
},
"engines": {
"node": ">=18.0.0"
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@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": {
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@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"
},
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"resend": "^2.1.0"
},
"engines": {
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.26"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29"
},
"engines": {
"node": ">=16.8.0"
+24
View File
@@ -1,5 +1,29 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/shopify",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+23 -1
View File
@@ -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() {
+57 -24
View File
@@ -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 }) => {
+20
View File
@@ -1,5 +1,25 @@
# @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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "3.0.0-beta.26",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"zod": "3.22.3"
},
"engines": {
+23
View File
@@ -1,5 +1,28 @@
# @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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"description": "Trigger.dev integration for stripe",
"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.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"stripe": "^12.14.0",
"zod": "3.22.3"
},
+23
View File
@@ -1,5 +1,28 @@
# @trigger.dev/supabase
## 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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"supabase-management-js": "^1.0.0",
"zod": "3.22.3"
},
+23
View File
@@ -1,5 +1,28 @@
# @trigger.dev/typeform
## 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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"description": "The official Typeform integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.26",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.29",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29",
"@typeform/api-client": "^1.8.0",
"zod": "3.22.3"
},
+20
View File
@@ -1,5 +1,25 @@
# @trigger.dev/astro
## 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
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@trigger.dev/astro",
"description": "An Astro-native integration for Trigger.dev background jobs platform",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
@@ -20,7 +20,7 @@
"build:tsup": "tsup"
},
"peerDependencies": {
"@trigger.dev/sdk": "workspace:^3.0.0-beta.26"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.29"
},
"devDependencies": {
"astro": "^3.0.12",
+25
View File
@@ -1,5 +1,30 @@
# trigger.dev
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/core@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- 2156e1526: Adding some additional telemetry during deploy to help debug issues
- 6406924b0: Ensure @trigger.dev/sdk and @trigger.dev/core are always in the list of deployed dependencies
- Updated dependencies [d490bc5cb]
- Updated dependencies [6d9dfbc75]
- @trigger.dev/core@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- 1670c4c41: Remove "log" Log Level, unify log and info messages under the "info" log level
- Updated dependencies [1670c4c41]
- Updated dependencies [203e00208]
- @trigger.dev/core@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "trigger.dev",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"description": "A Command-Line Interface for Trigger.dev (v3) projects",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -86,7 +86,7 @@
"@opentelemetry/sdk-trace-base": "^1.22.0",
"@opentelemetry/sdk-trace-node": "^1.22.0",
"@opentelemetry/semantic-conventions": "^1.22.0",
"@trigger.dev/core": "workspace:3.0.0-beta.26",
"@trigger.dev/core": "workspace:3.0.0-beta.29",
"@types/degit": "^2.8.3",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
+105 -59
View File
@@ -1286,7 +1286,7 @@ async function compileProject(
const javascriptProject = new JavascriptProject(config.projectDir);
const dependencies = await gatherRequiredDependencies(allImports, config, javascriptProject);
const dependencies = await resolveRequiredDependencies(allImports, config, javascriptProject);
logger.debug("gatherRequiredDependencies()", { dependencies });
@@ -1300,6 +1300,10 @@ async function compileProject(
},
};
span.setAttributes({
...flattenAttributes(packageJsonContents, "packageJson.contents"),
});
await writeJSONFile(join(tempDir, "package.json"), packageJsonContents);
const copyResult = await copyAdditionalFiles(config, tempDir);
@@ -1548,81 +1552,123 @@ async function typecheckProject(config: ResolvedConfig, options: DeployCommandOp
// Returns the dependencies that are required by the output that are found in output and the CLI package dependencies
// Returns the dependency names and the version to use (taken from the CLI deps package.json)
async function gatherRequiredDependencies(
async function resolveRequiredDependencies(
imports: Metafile["outputs"][string]["imports"],
config: ResolvedConfig,
project: JavascriptProject
) {
const dependencies: Record<string, string> = {};
const resolvablePackageNames = new Set<string>();
return await tracer.startActiveSpan("resolveRequiredDependencies", async (span) => {
const resolvablePackageNames = new Set<string>();
for (const file of imports) {
if ((file.kind !== "require-call" && file.kind !== "dynamic-import") || !file.external) {
continue;
}
const packageName = detectPackageNameFromImportPath(file.path);
if (!packageName) {
continue;
}
resolvablePackageNames.add(packageName);
}
const resolvedPackageVersions = await project.resolveAll(Array.from(resolvablePackageNames));
const missingPackages = Array.from(resolvablePackageNames).filter(
(packageName) => !resolvedPackageVersions[packageName]
);
for (const missingPackage of missingPackages) {
const internalDependencyVersion =
(packageJson.dependencies as Record<string, string>)[missingPackage] ??
detectDependencyVersion(missingPackage);
if (internalDependencyVersion) {
dependencies[missingPackage] = stripWorkspaceFromVersion(internalDependencyVersion);
}
}
for (const [packageName, version] of Object.entries(resolvedPackageVersions)) {
dependencies[packageName] = version;
}
if (config.additionalPackages) {
for (const packageName of config.additionalPackages) {
if (dependencies[packageName]) {
for (const file of imports) {
if ((file.kind !== "require-call" && file.kind !== "dynamic-import") || !file.external) {
continue;
}
const packageParts = parsePackageName(packageName);
const packageName = detectPackageNameFromImportPath(file.path);
if (packageParts.version) {
dependencies[packageParts.name] = packageParts.version;
if (!packageName) {
continue;
} else {
const externalDependencyVersion = await project.resolve(packageParts.name, {
allowDev: true,
});
}
if (externalDependencyVersion) {
dependencies[packageParts.name] = externalDependencyVersion;
resolvablePackageNames.add(packageName);
}
span.setAttribute("resolvablePackageNames", Array.from(resolvablePackageNames));
const resolvedPackageVersions = await project.resolveAll(Array.from(resolvablePackageNames));
const missingPackages = Array.from(resolvablePackageNames).filter(
(packageName) => !resolvedPackageVersions[packageName]
);
span.setAttributes({
...flattenAttributes(resolvedPackageVersions, "resolvedPackageVersions"),
});
span.setAttribute("missingPackages", missingPackages);
const dependencies: Record<string, string> = {};
for (const missingPackage of missingPackages) {
const internalDependencyVersion =
(packageJson.dependencies as Record<string, string>)[missingPackage] ??
detectDependencyVersion(missingPackage);
if (internalDependencyVersion) {
dependencies[missingPackage] = stripWorkspaceFromVersion(internalDependencyVersion);
}
}
for (const [packageName, version] of Object.entries(resolvedPackageVersions)) {
dependencies[packageName] = version;
}
if (config.additionalPackages) {
span.setAttribute("additionalPackages", config.additionalPackages);
for (const packageName of config.additionalPackages) {
if (dependencies[packageName]) {
continue;
}
const packageParts = parsePackageName(packageName);
if (packageParts.version) {
dependencies[packageParts.name] = packageParts.version;
continue;
} else {
logger.log(
`${chalkWarning("X Warning:")} Could not find version for package ${chalkPurple(
packageName
)}, add a version specifier to the package name (e.g. ${
packageParts.name
}@latest) or add it to your project's package.json`
);
const externalDependencyVersion = await project.resolve(packageParts.name, {
allowDev: true,
});
if (externalDependencyVersion) {
dependencies[packageParts.name] = externalDependencyVersion;
continue;
} else {
logger.log(
`${chalkWarning("X Warning:")} Could not find version for package ${chalkPurple(
packageName
)}, add a version specifier to the package name (e.g. ${
packageParts.name
}@latest) or add it to your project's package.json`
);
}
}
}
}
}
// Make sure we sort the dependencies by key to ensure consistent hashing
return Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
if (!dependencies["@trigger.dev/sdk"]) {
logger.debug("Adding missing @trigger.dev/sdk dependency", {
version: packageJson.version,
});
span.setAttribute("addingMissingSDK", packageJson.version);
dependencies["@trigger.dev/sdk"] = packageJson.version;
}
if (!dependencies["@trigger.dev/core"]) {
logger.debug("Adding missing @trigger.dev/core dependency", {
version: packageJson.version,
});
span.setAttribute("addingMissingCore", packageJson.version);
dependencies["@trigger.dev/core"] = packageJson.version;
}
// Make sure we sort the dependencies by key to ensure consistent hashing
const result = Object.fromEntries(
Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
);
span.setAttributes({
...flattenAttributes(result, "dependencies"),
});
span.end();
return result;
});
}
type AdditionalFilesReturn =
+1 -1
View File
@@ -149,7 +149,7 @@ export function bundleDependenciesPlugin(
}
}
logger.ignore(`[${buildIdentifier}] Externalizing ${args.path}`, {
logger.debug(`[${buildIdentifier}] Externalizing ${args.path}`, {
...args,
});
@@ -6,6 +6,9 @@ import { PackageManager, getUserPackageManager } from "./getUserPackageManager";
import { PackageJson } from "type-fest";
import { assertExhaustive } from "./assertExhaustive";
import { builtinModules } from "node:module";
import { tracer } from "../cli/common";
import { recordSpanException } from "@trigger.dev/core/v3/otel";
import { flattenAttributes } from "@trigger.dev/core/v3";
export type ResolveOptions = { allowDev: boolean };
@@ -47,60 +50,81 @@ export class JavascriptProject {
packageNames: string[],
options?: ResolveOptions
): Promise<Record<string, string>> {
const externalPackages = packageNames.filter((packageName) => !isBuiltInModule(packageName));
return tracer.startActiveSpan("JavascriptProject.resolveAll", async (span) => {
const externalPackages = packageNames.filter((packageName) => !isBuiltInModule(packageName));
const opts = { allowDev: false, ...options };
const opts = { allowDev: false, ...options };
const command = await this.#getCommand();
const command = await this.#getCommand();
try {
const versions = await command.resolveDependencyVersions(externalPackages, {
cwd: this.projectPath,
span.setAttributes({
externalPackages,
packageManager: command.name,
});
if (versions) {
logger.debug(`Resolved [${externalPackages.join(", ")}] version using ${command.name}`, {
versions,
try {
const versions = await command.resolveDependencyVersions(externalPackages, {
cwd: this.projectPath,
});
}
// Merge the resolved versions with the package.json dependencies
const missingPackages = externalPackages.filter((packageName) => !versions[packageName]);
const missingPackageVersions: Record<string, string> = {};
for (const packageName of missingPackages) {
const packageJsonVersion = this.packageJson.dependencies?.[packageName];
if (typeof packageJsonVersion === "string") {
logger.debug(`Resolved ${packageName} version using package.json`, {
packageJsonVersion,
if (versions) {
logger.debug(`Resolved [${externalPackages.join(", ")}] version using ${command.name}`, {
versions,
});
missingPackageVersions[packageName] = packageJsonVersion;
span.setAttributes({
...flattenAttributes(versions, "versions"),
});
}
if (opts.allowDev) {
const devPackageJsonVersion = this.packageJson.devDependencies?.[packageName];
// Merge the resolved versions with the package.json dependencies
const missingPackages = externalPackages.filter((packageName) => !versions[packageName]);
const missingPackageVersions: Record<string, string> = {};
if (typeof devPackageJsonVersion === "string") {
logger.debug(`Resolved ${packageName} version using devDependencies`, {
devPackageJsonVersion,
for (const packageName of missingPackages) {
const packageJsonVersion = this.packageJson.dependencies?.[packageName];
if (typeof packageJsonVersion === "string") {
logger.debug(`Resolved ${packageName} version using package.json`, {
packageJsonVersion,
});
missingPackageVersions[packageName] = devPackageJsonVersion;
missingPackageVersions[packageName] = packageJsonVersion;
}
if (opts.allowDev) {
const devPackageJsonVersion = this.packageJson.devDependencies?.[packageName];
if (typeof devPackageJsonVersion === "string") {
logger.debug(`Resolved ${packageName} version using devDependencies`, {
devPackageJsonVersion,
});
missingPackageVersions[packageName] = devPackageJsonVersion;
}
}
}
span.setAttributes({
...flattenAttributes(missingPackageVersions, "missingPackageVersions"),
missingPackages,
});
span.end();
return { ...versions, ...missingPackageVersions };
} catch (error) {
recordSpanException(span, error);
span.end();
logger.debug(`Failed to resolve dependency versions using ${command.name}`, {
packageNames,
error,
});
return {};
}
return { ...versions, ...missingPackageVersions };
} catch (error) {
logger.debug(`Failed to resolve dependency versions using ${command.name}`, {
packageNames,
error,
});
return {};
}
});
}
async resolve(packageName: string, options?: ResolveOptions): Promise<string | undefined> {
@@ -55,7 +55,9 @@ clock.setGlobalClock(durableClock);
const tracer = new TriggerTracer({ tracer: otelTracer, logger: otelLogger });
const consoleInterceptor = new ConsoleInterceptor(
otelLogger,
__PROJECT_CONFIG__.enableConsoleLogging ?? false
typeof __PROJECT_CONFIG__.enableConsoleLogging === "boolean"
? __PROJECT_CONFIG__.enableConsoleLogging
: true
);
const devRuntimeManager = new DevRuntimeManager();
@@ -73,7 +75,7 @@ const configLogLevel = triggerLogLevel
const otelTaskLogger = new OtelTaskLogger({
logger: otelLogger,
tracer: tracer,
level: logLevels.includes(configLogLevel as any) ? (configLogLevel as LogLevel) : "log",
level: logLevels.includes(configLogLevel as any) ? (configLogLevel as LogLevel) : "info",
});
logger.setGlobalTaskLogger(otelTaskLogger);
@@ -62,7 +62,7 @@ const configLogLevel = triggerLogLevel
const otelTaskLogger = new OtelTaskLogger({
logger: otelLogger,
tracer: tracer,
level: logLevels.includes(configLogLevel as any) ? (configLogLevel as LogLevel) : "log",
level: logLevels.includes(configLogLevel as any) ? (configLogLevel as LogLevel) : "info",
});
logger.setGlobalTaskLogger(otelTaskLogger);
+25
View File
@@ -1,5 +1,30 @@
# create-trigger
## 3.0.0-beta.29
### Patch Changes
- @trigger.dev/core@3.0.0-beta.29
- @trigger.dev/yalt@3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- Updated dependencies [d490bc5cb]
- Updated dependencies [6d9dfbc75]
- @trigger.dev/core@3.0.0-beta.28
- @trigger.dev/yalt@3.0.0-beta.28
## 3.0.0-beta.27
### Patch Changes
- Updated dependencies [1670c4c41]
- Updated dependencies [203e00208]
- @trigger.dev/core@3.0.0-beta.27
- @trigger.dev/yalt@3.0.0-beta.27
## 3.0.0-beta.26
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/cli",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"description": "The Trigger.dev CLI",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
+6
View File
@@ -1,5 +1,11 @@
# @trigger.dev/core-apps
## 3.0.0-beta.29
## 3.0.0-beta.28
## 3.0.0-beta.27
## 3.0.0-beta.26
## 3.0.0-beta.25
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@trigger.dev/core-apps",
"description": "Backend core code used across apps",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"private": true,
"license": "MIT",
"main": "./dist/index.js",
+6
View File
@@ -1,5 +1,11 @@
# @trigger.dev/core-backend
## 3.0.0-beta.29
## 3.0.0-beta.28
## 3.0.0-beta.27
## 3.0.0-beta.26
## 3.0.0-beta.25
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/core-backend",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"description": "Core code used across `@trigger.dev/sdk` and Trigger.dev server",
"license": "MIT",
"main": "./dist/index.js",
+16
View File
@@ -1,5 +1,21 @@
# internal-platform
## 3.0.0-beta.29
## 3.0.0-beta.28
### Patch Changes
- d490bc5cb: Add the "log" level back in as an alias to "info"
- 6d9dfbc75: Add configure function to be able to configure the SDK manually
## 3.0.0-beta.27
### Patch Changes
- 1670c4c41: Remove "log" Log Level, unify log and info messages under the "info" log level
- 203e00208: Add runs.retrieve management API method to get info about a run by run ID
## 3.0.0-beta.26
## 3.0.0-beta.25
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/core",
"version": "3.0.0-beta.26",
"version": "3.0.0-beta.29",
"description": "Core code used across the Trigger.dev SDK and platform",
"license": "MIT",
"main": "./dist/index.js",
+13 -41
View File
@@ -11,6 +11,7 @@ import {
ListScheduleOptions,
ListSchedulesResult,
ReplayRunResponse,
RetrieveRunResponse,
ScheduleObject,
TaskRunExecutionResult,
TriggerTaskRequestBody,
@@ -133,6 +134,18 @@ export class ApiClient {
);
}
retrieveRun(runId: string) {
return zodfetch(
RetrieveRunResponse,
`${this.baseUrl}/api/v3/runs/${runId}`,
{
method: "GET",
headers: this.#getHeaders(false),
},
zodFetchOptions
);
}
replayRun(runId: string) {
return zodfetch(
ReplayRunResponse,
@@ -240,44 +253,3 @@ export class ApiClient {
return headers;
}
}
type ApiClientContext = {
baseURL: string;
accessToken: string;
};
export class ApiClientManager {
private _storage: SafeAsyncLocalStorage<ApiClientContext> =
new SafeAsyncLocalStorage<ApiClientContext>();
get baseURL(): string | undefined {
const store = this.#getStore();
return store?.baseURL ?? getEnvVar("TRIGGER_API_URL") ?? "https://api.trigger.dev";
}
get accessToken(): string | undefined {
const store = this.#getStore();
return store?.accessToken ?? getEnvVar("TRIGGER_SECRET_KEY");
}
get client(): ApiClient | undefined {
if (!this.baseURL || !this.accessToken) {
return undefined;
}
return new ApiClient(this.baseURL, this.accessToken);
}
runWith<R extends (...args: any[]) => Promise<any>>(
context: ApiClientContext,
fn: R
): Promise<ReturnType<R>> {
return this._storage.runWith(context, fn);
}
#getStore(): ApiClientContext | undefined {
return this._storage.getStore();
}
}
export const apiClientManager = new ApiClientManager();
@@ -0,0 +1,7 @@
// Split module-level variable definition into separate files to allow
// tree-shaking on each api instance.
import { APIClientManagerAPI } from "./apiClientManager";
/** Entrypoint for logger API */
export const apiClientManager = APIClientManagerAPI.getInstance();
export type { ApiClientConfiguration } from "./apiClientManager/types";
@@ -0,0 +1,50 @@
import { ApiClient } from "../apiClient";
import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals";
import { getEnvVar } from "../utils/getEnv";
import { ApiClientConfiguration } from "./types";
const API_NAME = "api-client";
export class APIClientManagerAPI {
private static _instance?: APIClientManagerAPI;
private constructor() {}
public static getInstance(): APIClientManagerAPI {
if (!this._instance) {
this._instance = new APIClientManagerAPI();
}
return this._instance;
}
public disable() {
unregisterGlobal(API_NAME);
}
public setGlobalAPIClientConfiguration(config: ApiClientConfiguration): boolean {
return registerGlobal(API_NAME, config);
}
get baseURL(): string | undefined {
const store = this.#getConfig();
return store?.baseURL ?? getEnvVar("TRIGGER_API_URL") ?? "https://api.trigger.dev";
}
get accessToken(): string | undefined {
const store = this.#getConfig();
return store?.secretKey ?? getEnvVar("TRIGGER_SECRET_KEY");
}
get client(): ApiClient | undefined {
if (!this.baseURL || !this.accessToken) {
return undefined;
}
return new ApiClient(this.baseURL, this.accessToken);
}
#getConfig(): ApiClientConfiguration | undefined {
return getGlobal(API_NAME);
}
}
@@ -0,0 +1,4 @@
export type ApiClientConfiguration = {
baseURL?: string;
secretKey?: string;
};
+1
View File
@@ -6,6 +6,7 @@ export * from "./limits";
export * from "./logger-api";
export * from "./runtime-api";
export * from "./task-context-api";
export * from "./apiClientManager-api";
export * from "./schemas";
export { SemanticInternalAttributes } from "./semanticInternalAttributes";
export * from "./task-catalog-api";
+7 -7
View File
@@ -7,9 +7,9 @@ import { flattenAttributes } from "../utils/flattenAttributes";
import { ClockTime } from "../clock/clock";
import { clock } from "../clock-api";
export type LogLevel = "none" | "log" | "error" | "warn" | "info" | "debug";
export type LogLevel = "none" | "error" | "warn" | "info" | "debug" | "log";
export const logLevels: Array<LogLevel> = ["none", "error", "warn", "log", "info", "debug"];
export const logLevels: Array<LogLevel> = ["none", "error", "warn", "info", "debug"];
export type TaskLoggerConfig = {
logger: Logger;
@@ -34,31 +34,31 @@ export class OtelTaskLogger implements TaskLogger {
}
debug(message: string, properties?: Record<string, unknown>) {
if (this._level < 5) return;
if (this._level < 4) return; // ["none", "error", "warn", "info", "debug"];
this.#emitLog(message, this.#getTimestampInHrTime(), "debug", SeverityNumber.DEBUG, properties);
}
log(message: string, properties?: Record<string, unknown>) {
if (this._level < 3) return;
if (this._level < 3) return; // ["none", "error", "warn", "info", "debug"];
this.#emitLog(message, this.#getTimestampInHrTime(), "log", SeverityNumber.INFO, properties);
}
info(message: string, properties?: Record<string, unknown>) {
if (this._level < 4) return;
if (this._level < 3) return; // ["none", "error", "warn", "info", "debug"];
this.#emitLog(message, this.#getTimestampInHrTime(), "info", SeverityNumber.INFO, properties);
}
warn(message: string, properties?: Record<string, unknown>) {
if (this._level < 2) return;
if (this._level < 2) return; // ["none", "error", "warn", "info", "debug"];
this.#emitLog(message, this.#getTimestampInHrTime(), "warn", SeverityNumber.WARN, properties);
}
error(message: string, properties?: Record<string, unknown>) {
if (this._level < 1) return;
if (this._level < 1) return; // ["none", "error", "warn", "info", "debug"];
this.#emitLog(message, this.#getTimestampInHrTime(), "error", SeverityNumber.ERROR, properties);
}
+46
View File
@@ -316,3 +316,49 @@ export const ListScheduleOptions = z.object({
});
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
export const RunStatus = z.enum([
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED",
]);
export type RunStatus = z.infer<typeof RunStatus>;
export const AttemptStatus = z.enum([
"PENDING",
"EXECUTING",
"PAUSED",
"COMPLETED",
"FAILED",
"CANCELED",
]);
export type AttemptStatus = z.infer<typeof AttemptStatus>;
export const RetrieveRunResponse = z.object({
id: z.string(),
status: RunStatus,
taskIdentifier: z.string(),
idempotencyKey: z.string().optional(),
version: z.string().optional(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
attempts: z.array(
z
.object({
id: z.string(),
status: AttemptStatus,
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
startedAt: z.coerce.date().optional(),
completedAt: z.coerce.date().optional(),
})
.optional()
),
});
export type RetrieveRunResponse = z.infer<typeof RetrieveRunResponse>;

Some files were not shown because too many files have changed in this diff Show More