Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8eb68dd852 | |||
| a42037da03 | |||
| 43bc7ed94e | |||
| 4fdb7f8288 | |||
| 37b9b056c4 | |||
| 801c86bf73 | |||
| a1de11a001 | |||
| 29e9e372ee | |||
| affc128161 | |||
| 40ba8ad0ee | |||
| 96168eb383 | |||
| 2c68473cc8 | |||
| 469c8a2532 | |||
| ebeb790522 | |||
| e765240370 | |||
| 38770a91c7 | |||
| de188a22c5 | |||
| fae93ac747 | |||
| c3f6557eb6 | |||
| 9491a1649c | |||
| 0a5aa2dc15 | |||
| 4a1a5b2e4a | |||
| 503882762c | |||
| e3db257397 | |||
| de1cc868e3 | |||
| ff7fa9e19a | |||
| ba4f04db37 | |||
| d7911892ee | |||
| f888f85a09 | |||
| e9a63a4868 | |||
| 93ecbc5e05 | |||
| 60a8a5777b | |||
| ed1cd16753 | |||
| 6bb4dd7b6f | |||
| 0b5a0be807 | |||
| 74d1e61e42 | |||
| 12c83a56af | |||
| 75a54540a4 | |||
| b68012f81c | |||
| fb83d58703 | |||
| 52b2a8289c | |||
| 1e93ec4216 | |||
| cb27b7278a | |||
| e85fc501a6 | |||
| 0bfeb0816f | |||
| b207601732 | |||
| 3913e57ef4 | |||
| 26f310397a | |||
| 0a845767a0 | |||
| ed2a26c865 |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
- Fix additionalFiles that aren't decendants
|
||||
- Stop swallowing uncaught exceptions in prod
|
||||
- Improve warnings and errors, fail early on critical warnings
|
||||
- New arg to --save-logs even for successful builds
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
v3 CLI update command and package manager detection fix
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Hoist uncaughtException handler to the top of workers to better report error messages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Fix error stack traces
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Implement task.onSuccess/onFailure and config.onSuccess/onFailure
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Remove the env var check during deploy (too many false negatives)
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Make msw a normal dependency (for now) to fix Module Not Found error in Next.js.
|
||||
|
||||
It turns out that webpack will "hoist" dynamically imported modules and attempt to resolve them at build time, even though it's an optional peer dep:
|
||||
|
||||
https://x.com/maverickdotdev/status/1782465214308319404
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Adds support for `emitDecoratorMetadata: true` and `experimentalDecorators: true` in your tsconfig using the [`@anatine/esbuild-decorators`](https://github.com/anatine/esbuildnx/tree/main/packages/esbuild-decorators) package. This allows you to use libraries like TypeORM:
|
||||
|
||||
```ts orm/index.ts
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class Photo {
|
||||
@PrimaryColumn()
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
name!: string;
|
||||
|
||||
@Column()
|
||||
description!: string;
|
||||
|
||||
@Column()
|
||||
filename!: string;
|
||||
|
||||
@Column()
|
||||
views!: number;
|
||||
|
||||
@Column()
|
||||
isPublished!: boolean;
|
||||
}
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: "postgres",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "v3-catalog",
|
||||
entities: [Photo],
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
});
|
||||
```
|
||||
|
||||
And then in your trigger.config.ts file you can initialize the datasource using the new `init` option:
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
import { AppDataSource } from "@/trigger/orm";
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
// ... other options here
|
||||
init: async (payload, { ctx }) => {
|
||||
await AppDataSource.initialize();
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Now you are ready to use this in your tasks:
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk/v3";
|
||||
import { AppDataSource, Photo } from "./orm";
|
||||
|
||||
export const taskThatUsesDecorators = task({
|
||||
id: "taskThatUsesDecorators",
|
||||
run: async (payload: { message: string }) => {
|
||||
console.log("Creating a photo...");
|
||||
|
||||
const photo = new Photo();
|
||||
photo.id = 2;
|
||||
photo.name = "Me and Bears";
|
||||
photo.description = "I am near polar bears";
|
||||
photo.filename = "photo-with-bears.jpg";
|
||||
photo.views = 1;
|
||||
photo.isPublished = true;
|
||||
|
||||
await AppDataSource.manager.save(photo);
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fixes an issue that was treating v2 trigger directories as v3
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fix dev CLI output when not printing update messages
|
||||
+18
-1
@@ -44,14 +44,21 @@
|
||||
"@trigger.dev/yalt": "2.3.18"
|
||||
},
|
||||
"changesets": [
|
||||
"angry-eagles-trade",
|
||||
"beige-pens-dance",
|
||||
"big-tomatoes-deliver",
|
||||
"breezy-gorillas-mate",
|
||||
"chilled-hornets-move",
|
||||
"clean-pianos-listen",
|
||||
"clever-apes-collect",
|
||||
"clever-carrots-travel",
|
||||
"cool-glasses-bake",
|
||||
"cuddly-feet-approve",
|
||||
"dry-walls-check",
|
||||
"eight-pumas-float",
|
||||
"eleven-paws-join",
|
||||
"few-students-share",
|
||||
"funny-swans-destroy",
|
||||
"green-bags-wink",
|
||||
"khaki-apricots-design",
|
||||
"khaki-poems-lay",
|
||||
@@ -63,19 +70,27 @@
|
||||
"loud-actors-remember",
|
||||
"many-ligers-pump",
|
||||
"mighty-camels-joke",
|
||||
"mighty-flowers-train",
|
||||
"nasty-jars-pump",
|
||||
"new-rivers-tell",
|
||||
"ninety-pets-travel",
|
||||
"odd-poets-own",
|
||||
"polite-ducks-switch",
|
||||
"polite-rockets-matter",
|
||||
"poor-flowers-cross",
|
||||
"purple-garlics-shop",
|
||||
"rare-roses-float",
|
||||
"real-planets-stare",
|
||||
"rich-kangaroos-unite",
|
||||
"rotten-beers-refuse",
|
||||
"rotten-dryers-exercise",
|
||||
"selfish-ducks-sort",
|
||||
"shaggy-spoons-taste",
|
||||
"sharp-emus-compare",
|
||||
"sharp-zebras-serve",
|
||||
"shiny-coats-cry",
|
||||
"silly-suits-switch",
|
||||
"slow-buses-own",
|
||||
"smart-needles-move",
|
||||
"smart-olives-eat",
|
||||
"spicy-lamps-smoke",
|
||||
@@ -91,6 +106,8 @@
|
||||
"tidy-dryers-sleep",
|
||||
"tiny-doors-type",
|
||||
"tiny-elephants-scream",
|
||||
"tricky-bulldogs-heal"
|
||||
"tricky-bulldogs-heal",
|
||||
"tricky-ladybugs-unite",
|
||||
"two-pumas-wait"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Add typescript as a dependency so the esbuild-decorator will work even when running in npx
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fix a bug where revoking the CLI token would prevent you from ever logging in again with the CLI.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Add git to prod worker image which fixes private package installs
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Lock SDK and CLI deps on exact core version
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Move to our global system from AsyncLocalStorage for the current task context storage
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core-apps": patch
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Extracting out all the non-SDK related features from the main @trigger.dev/core/v3 export
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Add support for tasks located in subdirectories inside trigger dirs
|
||||
@@ -27,6 +27,6 @@ jobs:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
# e2e:
|
||||
# uses: ./.github/workflows/e2e.yml
|
||||
# secrets: inherit
|
||||
|
||||
@@ -4,13 +4,15 @@ on:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING: 1
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
short_sha: ${{ steps.get_commit.outputs.sha_short }}
|
||||
steps:
|
||||
- name: Setup Depot CLI
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
|
||||
@@ -49,11 +49,11 @@ jobs:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
# e2e:
|
||||
# uses: ./.github/workflows/e2e.yml
|
||||
# secrets: inherit
|
||||
|
||||
publish:
|
||||
needs: [typecheck, units, e2e]
|
||||
needs: [typecheck, units]
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"execa": "^8.0.1",
|
||||
"nanoid": "^5.0.6",
|
||||
"prom-client": "^15.1.0",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io-client": "^4.7.4"
|
||||
"socket.io": "4.7.4",
|
||||
"socket.io-client": "4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18",
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
PlatformToCoordinatorMessages,
|
||||
ProdWorkerSocketData,
|
||||
ProdWorkerToCoordinatorMessages,
|
||||
ZodNamespace,
|
||||
ZodSocketConnection,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { ZodNamespace } from "@trigger.dev/core/v3/zodNamespace";
|
||||
import { ZodSocketConnection } from "@trigger.dev/core/v3/zodSocket";
|
||||
import { HttpReply, getTextBody, SimpleLogger } from "@trigger.dev/core-apps";
|
||||
|
||||
import { collectDefaultMetrics, register, Gauge } from "prom-client";
|
||||
|
||||
@@ -211,7 +211,7 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
},
|
||||
{
|
||||
name: "populate-taskinfo",
|
||||
image: "busybox",
|
||||
image: "docker.io/library/busybox",
|
||||
command: ["/bin/sh", "-c"],
|
||||
args: ["printenv COORDINATOR_HOST | tee /etc/taskinfo/coordinator-host"],
|
||||
env: [
|
||||
|
||||
@@ -408,7 +408,7 @@ export class TaskMonitor {
|
||||
const createNonZeroExitPod = async (name: string, labels?: Record<string, string>) => {
|
||||
const container = {
|
||||
name,
|
||||
image: "busybox",
|
||||
image: "docker.io/library/busybox",
|
||||
command: ["sh"],
|
||||
args: ["-c", "exit 1"],
|
||||
} satisfies k8s.V1Container;
|
||||
@@ -419,7 +419,7 @@ export class TaskMonitor {
|
||||
const createOoDiskPod = async (name: string, labels?: Record<string, string>) => {
|
||||
const container = {
|
||||
name,
|
||||
image: "busybox",
|
||||
image: "docker.io/library/busybox",
|
||||
command: ["sh"],
|
||||
args: [
|
||||
"-c",
|
||||
|
||||
@@ -206,3 +206,39 @@ export function TriggerDevStepV3() {
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function TriggerLoginStepV3() {
|
||||
return (
|
||||
<ClientTabs defaultValue="npm">
|
||||
<ClientTabsList>
|
||||
<ClientTabsTrigger value={"npm"}>npm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"pnpm"}>pnpm</ClientTabsTrigger>
|
||||
<ClientTabsTrigger value={"yarn"}>yarn</ClientTabsTrigger>
|
||||
</ClientTabsList>
|
||||
<ClientTabsContent value={"npm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@${v3PackageTag} login`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@${v3PackageTag} login`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
<ClipboardField
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@${v3PackageTag} login`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Paragraph } from "./Paragraph";
|
||||
|
||||
export function Hint({ children }: { children: React.ReactNode }) {
|
||||
return <Paragraph variant="extra-small">{children}</Paragraph>;
|
||||
export function Hint({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<Paragraph variant="extra-small" className={className}>
|
||||
{children}
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@ import { Fragment } from "react";
|
||||
import { Modifier, ShortcutDefinition } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { useOperatingSystem } from "./OperatingSystemProvider";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
|
||||
const variants = {
|
||||
export const variants = {
|
||||
small:
|
||||
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase",
|
||||
medium:
|
||||
@@ -23,7 +29,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
const isMac = platform === "mac";
|
||||
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
|
||||
const modifiers = relevantShortcut.modifiers ?? [];
|
||||
const character = keyString(relevantShortcut.key, isMac);
|
||||
const character = keyString(relevantShortcut.key, isMac, variant);
|
||||
|
||||
return (
|
||||
<span className={cn(variants[variant], className)}>
|
||||
@@ -35,10 +41,22 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
|
||||
);
|
||||
}
|
||||
|
||||
function keyString(key: String, isMac: boolean) {
|
||||
function keyString(key: String, isMac: boolean, size: "small" | "medium") {
|
||||
key = key.toLowerCase();
|
||||
|
||||
const className = size === "small" ? "w-2.5 h-4" : "w-3 h-5";
|
||||
|
||||
switch (key) {
|
||||
case "enter":
|
||||
return isMac ? "↵" : key;
|
||||
case "arrowdown":
|
||||
return <ChevronDownIcon className={className} />;
|
||||
case "arrowup":
|
||||
return <ChevronUpIcon className={className} />;
|
||||
case "arrowleft":
|
||||
return <ChevronLeftIcon className={className} />;
|
||||
case "arrowright":
|
||||
return <ChevronRightIcon className={className} />;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { VirtualItem, Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { motion } from "framer-motion";
|
||||
import { MutableRefObject, RefObject, useCallback, useEffect, useReducer, useRef } from "react";
|
||||
import { UnmountClosed } from "react-collapse";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { NodeState, NodesState, reducer } from "./reducer";
|
||||
import { applyFilterToState, concreteStateFromInput, selectedIdFromState } from "./utils";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export type TreeViewProps<TData> = {
|
||||
tree: FlatTree<TData>;
|
||||
@@ -165,6 +165,11 @@ export type UseTreeStateOutput = {
|
||||
expandNode: (id: string, scrollToNode?: boolean) => void;
|
||||
collapseNode: (id: string) => void;
|
||||
toggleExpandNode: (id: string, scrollToNode?: boolean) => void;
|
||||
expandAllBelowDepth: (depth: number) => void;
|
||||
collapseAllBelowDepth: (depth: number) => void;
|
||||
expandLevel: (level: number) => void;
|
||||
collapseLevel: (level: number) => void;
|
||||
toggleExpandLevel: (level: number) => void;
|
||||
selectFirstVisibleNode: (scrollToNode?: boolean) => void;
|
||||
selectLastVisibleNode: (scrollToNode?: boolean) => void;
|
||||
selectNextVisibleNode: (scrollToNode?: boolean) => void;
|
||||
@@ -333,6 +338,41 @@ export function useTree<TData>({
|
||||
[state]
|
||||
);
|
||||
|
||||
const expandAllBelowDepth = useCallback(
|
||||
(depth: number) => {
|
||||
dispatch({ type: "EXPAND_ALL_BELOW_DEPTH", payload: { tree, depth } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const collapseAllBelowDepth = useCallback(
|
||||
(depth: number) => {
|
||||
dispatch({ type: "COLLAPSE_ALL_BELOW_DEPTH", payload: { tree, depth } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const expandLevel = useCallback(
|
||||
(level: number) => {
|
||||
dispatch({ type: "EXPAND_LEVEL", payload: { tree, level } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const collapseLevel = useCallback(
|
||||
(level: number) => {
|
||||
dispatch({ type: "COLLAPSE_LEVEL", payload: { tree, level } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const toggleExpandLevel = useCallback(
|
||||
(level: number) => {
|
||||
dispatch({ type: "TOGGLE_EXPAND_LEVEL", payload: { tree, level } });
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
const getTreeProps = useCallback(() => {
|
||||
return {
|
||||
role: "tree",
|
||||
@@ -368,25 +408,48 @@ export function useTree<TData>({
|
||||
}
|
||||
case "Left":
|
||||
case "ArrowLeft": {
|
||||
e.preventDefault();
|
||||
|
||||
const selected = selectedIdFromState(state.nodes);
|
||||
if (selected) {
|
||||
const treeNode = tree.find((node) => node.id === selected);
|
||||
if (treeNode && treeNode.hasChildren && state.nodes[selected].expanded) {
|
||||
|
||||
if (e.altKey) {
|
||||
if (treeNode && treeNode.hasChildren) {
|
||||
collapseLevel(treeNode.level);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const shouldCollapse =
|
||||
treeNode && treeNode.hasChildren && state.nodes[selected].expanded;
|
||||
if (shouldCollapse) {
|
||||
collapseNode(selected);
|
||||
} else {
|
||||
selectParentNode(true);
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case "Right":
|
||||
case "ArrowRight": {
|
||||
e.preventDefault();
|
||||
|
||||
const selected = selectedIdFromState(state.nodes);
|
||||
|
||||
if (selected) {
|
||||
const treeNode = tree.find((node) => node.id === selected);
|
||||
|
||||
if (e.altKey) {
|
||||
if (treeNode && treeNode.hasChildren) {
|
||||
expandLevel(treeNode.level);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
expandNode(selected, true);
|
||||
}
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
@@ -427,6 +490,11 @@ export function useTree<TData>({
|
||||
expandNode,
|
||||
collapseNode,
|
||||
toggleExpandNode,
|
||||
expandAllBelowDepth,
|
||||
collapseAllBelowDepth,
|
||||
expandLevel,
|
||||
collapseLevel,
|
||||
toggleExpandLevel,
|
||||
selectFirstVisibleNode,
|
||||
selectLastVisibleNode,
|
||||
selectNextVisibleNode,
|
||||
|
||||
@@ -91,6 +91,46 @@ type ToggleExpandNodeAction = {
|
||||
} & WithScrollToNode;
|
||||
};
|
||||
|
||||
type ExpandAllBelowDepthAction = {
|
||||
type: "EXPAND_ALL_BELOW_DEPTH";
|
||||
payload: {
|
||||
depth: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type CollapseAllBelowDepthAction = {
|
||||
type: "COLLAPSE_ALL_BELOW_DEPTH";
|
||||
payload: {
|
||||
depth: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type ExpandLevelAction = {
|
||||
type: "EXPAND_LEVEL";
|
||||
payload: {
|
||||
level: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type CollapseLevelAction = {
|
||||
type: "COLLAPSE_LEVEL";
|
||||
payload: {
|
||||
level: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type ToggleExpandLevelAction = {
|
||||
type: "TOGGLE_EXPAND_LEVEL";
|
||||
payload: {
|
||||
level: number;
|
||||
tree: FlatTree<any>;
|
||||
};
|
||||
};
|
||||
|
||||
type SelectFirstVisibleNodeAction = {
|
||||
type: "SELECT_FIRST_VISIBLE_NODE";
|
||||
payload: {
|
||||
@@ -135,6 +175,11 @@ export type Action =
|
||||
| ExpandNodeAction
|
||||
| CollapseNodeAction
|
||||
| ToggleExpandNodeAction
|
||||
| ExpandAllBelowDepthAction
|
||||
| CollapseAllBelowDepthAction
|
||||
| ExpandLevelAction
|
||||
| CollapseLevelAction
|
||||
| ToggleExpandLevelAction
|
||||
| SelectFirstVisibleNodeAction
|
||||
| SelectLastVisibleNodeAction
|
||||
| SelectNextVisibleNodeAction
|
||||
@@ -229,6 +274,109 @@ export function reducer(state: TreeState, action: Action): TreeState {
|
||||
});
|
||||
}
|
||||
}
|
||||
case "EXPAND_ALL_BELOW_DEPTH": {
|
||||
const nodesToExpand = action.payload.tree.filter(
|
||||
(n) => n.level >= action.payload.depth && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "COLLAPSE_ALL_BELOW_DEPTH": {
|
||||
const nodesToCollapse = action.payload.tree.filter(
|
||||
(n) => n.level >= action.payload.depth && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "EXPAND_LEVEL": {
|
||||
const nodesToExpand = action.payload.tree.filter(
|
||||
(n) => n.level <= action.payload.level && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "COLLAPSE_LEVEL": {
|
||||
const nodesToCollapse = action.payload.tree.filter(
|
||||
(n) => n.level === action.payload.level && n.hasChildren
|
||||
);
|
||||
|
||||
const newNodes = Object.fromEntries(
|
||||
Object.entries(state.nodes).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
...value,
|
||||
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
|
||||
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
|
||||
}
|
||||
case "TOGGLE_EXPAND_LEVEL": {
|
||||
//first get the first item at that level in the tree. If it is expanded, collapse all nodes at that level
|
||||
//if it is collapsed, expand all nodes at that level
|
||||
const nodesAtLevel = action.payload.tree.filter(
|
||||
(n) => n.level === action.payload.level && n.hasChildren
|
||||
);
|
||||
const firstNode = nodesAtLevel[0];
|
||||
if (!firstNode) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const currentlyExpanded = state.nodes[firstNode.id]?.expanded ?? true;
|
||||
const currentVisible = state.nodes[firstNode.id]?.visible ?? true;
|
||||
if (currentlyExpanded && currentVisible) {
|
||||
return reducer(state, {
|
||||
type: "COLLAPSE_LEVEL",
|
||||
payload: {
|
||||
level: action.payload.level,
|
||||
tree: action.payload.tree,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return reducer(state, {
|
||||
type: "EXPAND_LEVEL",
|
||||
payload: {
|
||||
level: action.payload.level,
|
||||
tree: action.payload.tree,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
case "SELECT_FIRST_VISIBLE_NODE": {
|
||||
const node = firstVisibleNode(action.payload.tree, state.nodes);
|
||||
if (node) {
|
||||
|
||||
@@ -73,14 +73,7 @@ export function SpanCodePathAccessory({
|
||||
>
|
||||
{accessory.items.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
<span
|
||||
className={cn(
|
||||
"truncate",
|
||||
index === accessory.items.length - 1 ? "text-sun-100" : "text-text-dimmed"
|
||||
)}
|
||||
>
|
||||
{item.text}
|
||||
</span>
|
||||
<span className={cn("truncate", "text-text-dimmed")}>{item.text}</span>
|
||||
{index < accessory.items.length - 1 && (
|
||||
<span className="text-text-dimmed">
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
|
||||
@@ -27,7 +27,7 @@ type TaskFunctionNameProps = {
|
||||
|
||||
export function TaskFunctionName({ variant, functionName, className }: TaskFunctionNameProps) {
|
||||
return (
|
||||
<InlineCode variant={variant} className={cn("text-sun-100", className)}>
|
||||
<InlineCode variant={variant} className={cn("text-text-dimmed", className)}>
|
||||
{`${functionName}()`}
|
||||
</InlineCode>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,14 @@ const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
|
||||
CRASHED: "Task has crashed and won't be retried",
|
||||
};
|
||||
|
||||
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY"];
|
||||
|
||||
export const RUNNING_STATUSES: TaskRunStatus[] = [
|
||||
"EXECUTING",
|
||||
"RETRYING_AFTER_FAILURE",
|
||||
"WAITING_TO_RESUME",
|
||||
];
|
||||
|
||||
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
|
||||
return taskRunStatusDescriptions[status];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { StopIcon } from "@heroicons/react/24/outline";
|
||||
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { formatDuration } from "@trigger.dev/core/v3";
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { useEnvironments } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
|
||||
import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
||||
import { DateTime } from "../../primitives/DateTime";
|
||||
import { Paragraph } from "../../primitives/Paragraph";
|
||||
@@ -14,21 +20,14 @@ import {
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellChevron,
|
||||
TableCellMenu,
|
||||
TableHeader,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from "../../primitives/Table";
|
||||
import { formatDuration } from "@trigger.dev/core/v3";
|
||||
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
||||
import { useEnvironments } from "~/hooks/useEnvironments";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { CancelRunDialog } from "./CancelRunDialog";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { ReplayRunDialog } from "./ReplayRunDialog";
|
||||
import { TaskRunStatusCombo } from "./TaskRunStatus";
|
||||
|
||||
type RunsTableProps = {
|
||||
total: number;
|
||||
@@ -37,7 +36,6 @@ type RunsTableProps = {
|
||||
showJob?: boolean;
|
||||
runs: RunListItem[];
|
||||
isLoading?: boolean;
|
||||
currentUser: User;
|
||||
};
|
||||
|
||||
export function TaskRunsTable({
|
||||
@@ -46,7 +44,6 @@ export function TaskRunsTable({
|
||||
filters,
|
||||
runs,
|
||||
isLoading = false,
|
||||
currentUser,
|
||||
}: RunsTableProps) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
@@ -78,16 +75,17 @@ export function TaskRunsTable({
|
||||
<BlankState isLoading={isLoading} filters={filters} />
|
||||
) : (
|
||||
runs.map((run) => {
|
||||
const path = v3RunPath(organization, project, run);
|
||||
const usernameForEnv =
|
||||
currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
|
||||
const path = v3RunSpanPath(organization, project, run, { spanId: run.spanId });
|
||||
return (
|
||||
<TableRow key={run.id}>
|
||||
<TableCell to={path}>#{run.number}</TableCell>
|
||||
<TableCell to={path}>{run.taskIdentifier}</TableCell>
|
||||
<TableCell to={path}>{run.version ?? "–"}</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnvironmentLabel environment={run.environment} userName={usernameForEnv} />
|
||||
<EnvironmentLabel
|
||||
environment={run.environment}
|
||||
userName={run.environment.userName}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<TaskRunStatusCombo status={run.status} />
|
||||
@@ -197,7 +195,12 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
|
||||
{environment ? (
|
||||
<>
|
||||
{" "}
|
||||
in <EnvironmentLabel environment={environment} size="large" />
|
||||
in{" "}
|
||||
<EnvironmentLabel
|
||||
environment={environment}
|
||||
userName={environment.userName}
|
||||
size="large"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
|
||||
@@ -70,23 +70,21 @@ export { Prisma };
|
||||
|
||||
export const prisma = singleton("prisma", getClient);
|
||||
|
||||
export const $replica: Omit<PrismaClient, "$transaction"> = singleton(
|
||||
"replica",
|
||||
() => getReplicaClient() ?? prisma
|
||||
);
|
||||
|
||||
function getClient() {
|
||||
const { DATABASE_URL } = process.env;
|
||||
invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set");
|
||||
|
||||
const databaseUrl = new URL(DATABASE_URL);
|
||||
const databaseUrl = extendQueryParams(DATABASE_URL, {
|
||||
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
});
|
||||
|
||||
// We need to add the connection_limit and pool_timeout query params to the url, in a way that works if the DATABASE_URL already has query params
|
||||
const query = databaseUrl.searchParams;
|
||||
query.set("connection_limit", env.DATABASE_CONNECTION_LIMIT.toString());
|
||||
query.set("pool_timeout", env.DATABASE_POOL_TIMEOUT.toString());
|
||||
databaseUrl.search = query.toString();
|
||||
|
||||
// Remove the username:password in the url and print that to the console
|
||||
const urlWithoutCredentials = new URL(databaseUrl.href);
|
||||
urlWithoutCredentials.password = "";
|
||||
|
||||
console.log(`🔌 setting up prisma client to ${urlWithoutCredentials.toString()}`);
|
||||
console.log(`🔌 setting up prisma client to ${redactUrlSecrets(databaseUrl)}`);
|
||||
|
||||
const client = new PrismaClient({
|
||||
datasources: {
|
||||
@@ -134,6 +132,68 @@ function getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
function getReplicaClient() {
|
||||
if (!env.DATABASE_READ_REPLICA_URL) {
|
||||
console.log(`🔌 No database replica, using the regular client`);
|
||||
return;
|
||||
}
|
||||
|
||||
const replicaUrl = extendQueryParams(env.DATABASE_READ_REPLICA_URL, {
|
||||
connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(),
|
||||
pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(),
|
||||
});
|
||||
|
||||
console.log(`🔌 setting up read replica connection to ${redactUrlSecrets(replicaUrl)}`);
|
||||
|
||||
const replicaClient = new PrismaClient({
|
||||
datasources: {
|
||||
db: {
|
||||
url: replicaUrl.href,
|
||||
},
|
||||
},
|
||||
log: [
|
||||
{
|
||||
emit: "stdout",
|
||||
level: "error",
|
||||
},
|
||||
{
|
||||
emit: "stdout",
|
||||
level: "info",
|
||||
},
|
||||
{
|
||||
emit: "stdout",
|
||||
level: "warn",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// connect eagerly
|
||||
replicaClient.$connect();
|
||||
|
||||
console.log(`🔌 read replica connected`);
|
||||
|
||||
return replicaClient;
|
||||
}
|
||||
|
||||
function extendQueryParams(hrefOrUrl: string | URL, queryParams: Record<string, string>) {
|
||||
const url = new URL(hrefOrUrl);
|
||||
const query = url.searchParams;
|
||||
|
||||
for (const [key, val] of Object.entries(queryParams)) {
|
||||
query.set(key, val);
|
||||
}
|
||||
|
||||
url.search = query.toString();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function redactUrlSecrets(hrefOrUrl: string | URL) {
|
||||
const url = new URL(hrefOrUrl);
|
||||
url.password = "";
|
||||
return url.href;
|
||||
}
|
||||
|
||||
export type { PrismaClient } from "@trigger.dev/database";
|
||||
|
||||
export const PrismaErrorSchema = z.object({
|
||||
|
||||
@@ -19,6 +19,7 @@ const EnvironmentSchema = z.object({
|
||||
isValidDatabaseUrl,
|
||||
"DIRECT_URL is invalid, for details please check the additional output above this message."
|
||||
),
|
||||
DATABASE_READ_REPLICA_URL: z.string().optional(),
|
||||
SESSION_SECRET: z.string(),
|
||||
MAGIC_LINK_SECRET: z.string(),
|
||||
ENCRYPTION_KEY: z.string(),
|
||||
|
||||
@@ -10,20 +10,3 @@ export function useEnvironments(matches?: UIMatch[]) {
|
||||
|
||||
return project.environments;
|
||||
}
|
||||
|
||||
export function useDevEnvironment(matches?: UIMatch[]) {
|
||||
const user = useUser();
|
||||
const environments = useEnvironments(matches);
|
||||
if (!environments) return;
|
||||
|
||||
return environments.find(
|
||||
(environment) => environment.type === "DEVELOPMENT" && environment.userId === user.id
|
||||
);
|
||||
}
|
||||
|
||||
export function useProdEnvironment(matches?: UIMatch[]) {
|
||||
const environments = useEnvironments(matches);
|
||||
if (!environments) return;
|
||||
|
||||
return environments.find((environment) => environment.type === "PRODUCTION");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Reducer, useReducer } from "react";
|
||||
|
||||
export type ListState<T> = {
|
||||
items: T[];
|
||||
};
|
||||
|
||||
type AppendAction<T> = {
|
||||
type: "append";
|
||||
items: T[];
|
||||
};
|
||||
|
||||
type UpdateAction<T> = {
|
||||
type: "update";
|
||||
index: number;
|
||||
item: T;
|
||||
};
|
||||
|
||||
type DeleteAction<T> = {
|
||||
type: "delete";
|
||||
index: number;
|
||||
};
|
||||
|
||||
type InsertAfter<T> = {
|
||||
type: "insertAfter";
|
||||
index: number;
|
||||
items: T[];
|
||||
};
|
||||
|
||||
type Action<T> = AppendAction<T> | UpdateAction<T> | DeleteAction<T> | InsertAfter<T>;
|
||||
|
||||
function reducer<T>(state: ListState<T>, action: Action<T>): ListState<T> {
|
||||
switch (action.type) {
|
||||
case "append":
|
||||
return { items: [...state.items, ...action.items] };
|
||||
case "update":
|
||||
return {
|
||||
items: state.items.map((v, i) => (i === action.index ? action.item : v)),
|
||||
};
|
||||
case "delete":
|
||||
return { items: state.items.filter((_, i) => i !== action.index) };
|
||||
case "insertAfter":
|
||||
return {
|
||||
items: [
|
||||
...state.items.slice(0, action.index + 1),
|
||||
...action.items,
|
||||
...state.items.slice(action.index + 1),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type HookReturn<T> = {
|
||||
items: T[];
|
||||
append: (items: T[]) => void;
|
||||
update: (index: number, item: T) => void;
|
||||
delete: (index: number) => void;
|
||||
insertAfter: (index: number, items: T[]) => void;
|
||||
};
|
||||
|
||||
export function useList<T>(initialItems: T[]): HookReturn<T> {
|
||||
const [state, dispatch] = useReducer<Reducer<ListState<T>, Action<T>>>(reducer, {
|
||||
items: initialItems,
|
||||
});
|
||||
|
||||
return {
|
||||
items: state.items,
|
||||
append: (items: T[]) => dispatch({ type: "append", items }),
|
||||
update: (index: number, item: T) => dispatch({ type: "update", index, item }),
|
||||
delete: (index: number) => dispatch({ type: "delete", index }),
|
||||
insertAfter: (index: number, items: T[]) => dispatch({ type: "insertAfter", index, items }),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useOptimisticLocation } from "./useOptimisticLocation";
|
||||
import type { Location } from "@remix-run/react";
|
||||
|
||||
export function useReplaceLocation() {
|
||||
const optimisticLocation = useOptimisticLocation();
|
||||
const [location, setLocation] = useState(optimisticLocation);
|
||||
|
||||
const replaceLocation = useCallback((location: Location<any>) => {
|
||||
const fullPath = location.pathname + location.search + location.hash;
|
||||
//replace the URL in the browser
|
||||
history.replaceState(null, "", fullPath);
|
||||
//update the state (new object in case the same location ref was modified)
|
||||
const newLocation = { ...location };
|
||||
setLocation(newLocation);
|
||||
}, []);
|
||||
|
||||
const replaceSearchParam = useCallback(
|
||||
(key: string, value?: string) => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
if (value) {
|
||||
searchParams.set(key, value);
|
||||
} else {
|
||||
searchParams.delete(key);
|
||||
}
|
||||
replaceLocation({ ...optimisticLocation, search: "?" + searchParams.toString() });
|
||||
},
|
||||
[optimisticLocation, replaceLocation]
|
||||
);
|
||||
|
||||
return { location, replaceLocation, replaceSearchParam };
|
||||
}
|
||||
@@ -87,8 +87,8 @@ export async function createOrganization(
|
||||
}
|
||||
|
||||
export async function createEnvironment(
|
||||
organization: Organization,
|
||||
project: Project,
|
||||
organization: Pick<Organization, "id">,
|
||||
project: Pick<Project, "id">,
|
||||
type: RuntimeEnvironment["type"],
|
||||
member?: OrgMember,
|
||||
prismaClient: PrismaClientOrTransaction = prisma
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function createProject(
|
||||
// Create the dev and prod environments
|
||||
await createEnvironment(organization, project, "PRODUCTION");
|
||||
|
||||
if (project.version === "V2") {
|
||||
if (version === "v2") {
|
||||
await createEnvironment(organization, project, "STAGING");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import type { Prisma, RuntimeEnvironment } from "@trigger.dev/database";
|
||||
import { prisma } from "~/db.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
export type { RuntimeEnvironment };
|
||||
|
||||
@@ -118,3 +119,36 @@ export async function disconnectSession(environmentId: string) {
|
||||
return session;
|
||||
});
|
||||
}
|
||||
|
||||
type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
type: true;
|
||||
orgMember: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true;
|
||||
name: true;
|
||||
displayName: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
export function displayableEnvironments(
|
||||
environment: DisplayableInputEnvironment,
|
||||
userId: string | undefined
|
||||
) {
|
||||
return {
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
userName: environment.orgMember
|
||||
? environment.orgMember.user.id === userId
|
||||
? undefined
|
||||
: getUsername(environment.orgMember.user)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import { sortEnvironments } from "~/services/environmentSort.server";
|
||||
|
||||
export class ProjectPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -49,7 +51,13 @@ export class ProjectPresenter {
|
||||
type: true,
|
||||
orgMember: {
|
||||
select: {
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
displayName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
apiKey: true,
|
||||
@@ -76,13 +84,12 @@ export class ProjectPresenter {
|
||||
hasInactiveExternalTriggers: project._count.sources > 0,
|
||||
jobCount: project._count.jobs,
|
||||
httpEndpointCount: project._count.httpEndpoints,
|
||||
environments: project.environments.map((environment) => ({
|
||||
id: environment.id,
|
||||
slug: environment.slug,
|
||||
type: environment.type,
|
||||
apiKey: environment.apiKey,
|
||||
userId: environment.orgMember?.userId,
|
||||
})),
|
||||
environments: sortEnvironments(
|
||||
project.environments.map((environment) => ({
|
||||
...displayableEnvironments(environment, userId),
|
||||
userId: environment.orgMember?.user.id,
|
||||
}))
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ export class ApiKeysPresenter {
|
||||
environmentVariableCount: environment._count.environmentVariableValues,
|
||||
}))
|
||||
),
|
||||
hasStaging: environments.some((environment) => environment.type === "STAGING"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Prisma, TaskRunStatus } from "@trigger.dev/database";
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
|
||||
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
|
||||
|
||||
type RunListOptions = {
|
||||
userId?: string;
|
||||
projectSlug: string;
|
||||
//filters
|
||||
tasks?: string[];
|
||||
@@ -34,6 +36,7 @@ export class RunListPresenter {
|
||||
}
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
projectSlug,
|
||||
tasks,
|
||||
versions,
|
||||
@@ -106,6 +109,7 @@ export class RunListPresenter {
|
||||
lockedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
isTest: boolean;
|
||||
spanId: string;
|
||||
attempts: BigInt;
|
||||
}[]
|
||||
>`
|
||||
@@ -121,6 +125,7 @@ export class RunListPresenter {
|
||||
tr."lockedAt" AS "lockedAt",
|
||||
tra."completedAt" AS "completedAt",
|
||||
tr."isTest" AS "isTest",
|
||||
tr."spanId" AS "spanId",
|
||||
COUNT(tra.id) AS attempts
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" tr
|
||||
@@ -225,15 +230,11 @@ export class RunListPresenter {
|
||||
status: run.status,
|
||||
version: run.version,
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
spanId: run.spanId,
|
||||
attempts: Number(run.attempts),
|
||||
isReplayable: true,
|
||||
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
|
||||
environment: {
|
||||
type: environment.type,
|
||||
slug: environment.slug,
|
||||
userId: environment.orgMember?.user.id,
|
||||
userName: getUsername(environment.orgMember?.user),
|
||||
},
|
||||
environment: displayableEnvironments(environment, userId),
|
||||
};
|
||||
}),
|
||||
pagination: {
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
import { Prisma, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
|
||||
import {
|
||||
Prisma,
|
||||
RuntimeEnvironmentType,
|
||||
TaskRunStatus,
|
||||
TaskTriggerSource,
|
||||
} from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server";
|
||||
import { Organization } from "~/models/organization.server";
|
||||
import { Project } from "~/models/project.server";
|
||||
import { User } from "~/models/user.server";
|
||||
import { sortEnvironments } from "~/services/environmentSort.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { QUEUED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { displayableEnvironments } from "~/models/runtimeEnvironment.server";
|
||||
|
||||
export type Task = Awaited<ReturnType<TaskListPresenter["call"]>>[0];
|
||||
export type Task = {
|
||||
slug: string;
|
||||
exportName: string;
|
||||
filePath: string;
|
||||
createdAt: Date;
|
||||
triggerSource: TaskTriggerSource;
|
||||
environments: {
|
||||
id: string;
|
||||
type: RuntimeEnvironmentType;
|
||||
userName?: string;
|
||||
}[];
|
||||
latestRun?: {
|
||||
createdAt: Date;
|
||||
status: TaskRunStatus;
|
||||
};
|
||||
};
|
||||
|
||||
export class TaskListPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
type Return = Awaited<ReturnType<TaskListPresenter["call"]>>;
|
||||
|
||||
constructor(prismaClient: PrismaClient = prisma) {
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
export type TaskActivity = Awaited<Return["activity"]>[string];
|
||||
|
||||
export class TaskListPresenter extends BasePresenter {
|
||||
public async call({
|
||||
userId,
|
||||
projectSlug,
|
||||
@@ -23,7 +46,7 @@ export class TaskListPresenter {
|
||||
projectSlug: Project["slug"];
|
||||
organizationSlug: Organization["slug"];
|
||||
}) {
|
||||
const project = await this.#prismaClient.project.findFirstOrThrow({
|
||||
const project = await this._replica.project.findFirstOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
@@ -53,7 +76,7 @@ export class TaskListPresenter {
|
||||
},
|
||||
});
|
||||
|
||||
const tasks = await this.#prismaClient.$queryRaw<
|
||||
const tasks = await this._replica.$queryRaw<
|
||||
{
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -64,73 +87,243 @@ export class TaskListPresenter {
|
||||
triggerSource: TaskTriggerSource;
|
||||
}[]
|
||||
>`
|
||||
SELECT DISTINCT ON(bwt.slug, bwt."runtimeEnvironmentId")
|
||||
bwt.slug,
|
||||
bwt.id,
|
||||
bwt."exportName",
|
||||
bwt."filePath",
|
||||
bwt."runtimeEnvironmentId",
|
||||
bwt."createdAt",
|
||||
bwt."triggerSource"
|
||||
FROM
|
||||
${sqlDatabaseSchema}."BackgroundWorkerTask" as bwt
|
||||
WHERE bwt."projectId" = ${project.id}
|
||||
ORDER BY
|
||||
bwt.slug,
|
||||
bwt."runtimeEnvironmentId",
|
||||
bwt."createdAt" DESC;`;
|
||||
WITH workers AS (
|
||||
SELECT DISTINCT ON ("runtimeEnvironmentId") id, "runtimeEnvironmentId", version
|
||||
FROM ${sqlDatabaseSchema}."BackgroundWorker"
|
||||
WHERE "runtimeEnvironmentId" IN (${Prisma.join(project.environments.map((e) => e.id))})
|
||||
ORDER BY "runtimeEnvironmentId", "createdAt" DESC
|
||||
)
|
||||
SELECT tasks.id, slug, "filePath", "exportName", "triggerSource", tasks."runtimeEnvironmentId", tasks."createdAt"
|
||||
FROM workers
|
||||
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" tasks ON tasks."workerId" = workers.id
|
||||
ORDER BY slug ASC;`;
|
||||
|
||||
let latestRuns = [] as {
|
||||
createdAt: Date;
|
||||
status: TaskRunStatus;
|
||||
lockedById: string;
|
||||
taskIdentifier: string;
|
||||
}[];
|
||||
|
||||
if (tasks.length > 0) {
|
||||
latestRuns = await this.#prismaClient.$queryRaw<
|
||||
const uniqueTaskSlugs = new Set(tasks.map((t) => t.slug));
|
||||
latestRuns = await this._replica.$queryRaw<
|
||||
{
|
||||
createdAt: Date;
|
||||
status: TaskRunStatus;
|
||||
lockedById: string;
|
||||
taskIdentifier: string;
|
||||
}[]
|
||||
>`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
"createdAt",
|
||||
"status",
|
||||
"lockedById",
|
||||
ROW_NUMBER() OVER (PARTITION BY "lockedById" ORDER BY "updatedAt" DESC) AS rn
|
||||
"taskIdentifier",
|
||||
ROW_NUMBER() OVER (PARTITION BY "taskIdentifier" ORDER BY "updatedAt" DESC) AS rn
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun"
|
||||
WHERE
|
||||
"lockedById" IN(${Prisma.join(tasks.map((t) => t.id))})
|
||||
) t
|
||||
WHERE rn = 1;`;
|
||||
"taskIdentifier" IN(${Prisma.join(Array.from(uniqueTaskSlugs))})
|
||||
AND "projectId" = ${project.id}
|
||||
) t
|
||||
WHERE rn = 1;`;
|
||||
}
|
||||
|
||||
return tasks.map((task) => {
|
||||
const latestRun = latestRuns.find((r) => r.lockedById === task.id);
|
||||
//group by the task identifier (task.slug). Add the latestRun and add all the environments.
|
||||
const outputTasks = tasks.reduce((acc, task) => {
|
||||
const latestRun = latestRuns.find((r) => r.taskIdentifier === task.slug);
|
||||
const environment = project.environments.find((env) => env.id === task.runtimeEnvironmentId);
|
||||
if (!environment) {
|
||||
throw new Error(`Environment not found for TaskRun ${task.id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
...task,
|
||||
environment: {
|
||||
id: environment.id,
|
||||
type: environment.type,
|
||||
slug: environment.slug,
|
||||
userId: environment.orgMember?.user.id,
|
||||
userName: getUsername(environment.orgMember?.user),
|
||||
},
|
||||
latestRun: latestRun
|
||||
? {
|
||||
createdAt: latestRun.createdAt,
|
||||
status: latestRun.status,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
let existingTask = acc.find((t) => t.slug === task.slug);
|
||||
|
||||
if (!existingTask) {
|
||||
existingTask = {
|
||||
...task,
|
||||
environments: [],
|
||||
};
|
||||
acc.push(existingTask);
|
||||
}
|
||||
|
||||
existingTask.environments.push(displayableEnvironments(environment, userId));
|
||||
|
||||
//order the environments
|
||||
existingTask.environments = sortEnvironments(existingTask.environments);
|
||||
|
||||
existingTask.latestRun = latestRun
|
||||
? {
|
||||
createdAt: latestRun.createdAt,
|
||||
status: latestRun.status,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return acc;
|
||||
}, [] as Task[]);
|
||||
|
||||
//then get the activity for each task
|
||||
const activity = this.#getActivity(
|
||||
outputTasks.map((t) => t.slug),
|
||||
project.id
|
||||
);
|
||||
|
||||
const runningStats = this.#getRunningStats(
|
||||
outputTasks.map((t) => t.slug),
|
||||
project.id
|
||||
);
|
||||
|
||||
const durations = this.#getAverageDurations(
|
||||
outputTasks.map((t) => t.slug),
|
||||
project.id
|
||||
);
|
||||
|
||||
const userEnvironment = project.environments.find((e) => e.orgMember?.user.id === userId);
|
||||
const userHasTasks = userEnvironment
|
||||
? outputTasks.some((t) => t.environments.some((e) => e.id === userEnvironment.id))
|
||||
: false;
|
||||
|
||||
return { tasks: outputTasks, userHasTasks, activity, runningStats, durations };
|
||||
}
|
||||
|
||||
async #getActivity(tasks: string[], projectId: string) {
|
||||
const activity = await this._replica.$queryRaw<
|
||||
{
|
||||
taskIdentifier: string;
|
||||
status: TaskRunStatus;
|
||||
day: Date;
|
||||
count: BigInt;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
tr."taskIdentifier",
|
||||
tr."status",
|
||||
DATE(tr."createdAt") as day,
|
||||
COUNT(*)
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" as tr
|
||||
WHERE
|
||||
tr."taskIdentifier" IN (${Prisma.join(tasks)})
|
||||
AND tr."projectId" = ${projectId}
|
||||
AND tr."createdAt" >= (current_date - interval '6 days')
|
||||
GROUP BY
|
||||
tr."taskIdentifier",
|
||||
tr."status",
|
||||
day
|
||||
ORDER BY
|
||||
tr."taskIdentifier" ASC,
|
||||
day ASC,
|
||||
tr."status" ASC;`;
|
||||
|
||||
//today with no time
|
||||
const today = new Date();
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
return activity.reduce((acc, a) => {
|
||||
let existingTask = acc[a.taskIdentifier];
|
||||
|
||||
if (!existingTask) {
|
||||
existingTask = [];
|
||||
//populate the array with the past 7 days
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const day = new Date(today);
|
||||
day.setUTCDate(today.getDate() - i);
|
||||
day.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
existingTask.push({
|
||||
day: day.toISOString(),
|
||||
[TaskRunStatus.COMPLETED_SUCCESSFULLY]: 0,
|
||||
} as { day: string } & Record<TaskRunStatus, number>);
|
||||
}
|
||||
|
||||
acc[a.taskIdentifier] = existingTask;
|
||||
}
|
||||
|
||||
const dayString = a.day.toISOString();
|
||||
const day = existingTask.find((d) => d.day === dayString);
|
||||
|
||||
if (!day) {
|
||||
logger.warn(`Day not found for TaskRun`, {
|
||||
day: dayString,
|
||||
taskIdentifier: a.taskIdentifier,
|
||||
existingTask,
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
|
||||
day[a.status] = Number(a.count);
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, ({ day: string } & Record<TaskRunStatus, number>)[]>);
|
||||
}
|
||||
|
||||
async #getRunningStats(tasks: string[], projectId: string) {
|
||||
const statuses = await this._replica.$queryRaw<
|
||||
{
|
||||
taskIdentifier: string;
|
||||
status: TaskRunStatus;
|
||||
count: BigInt;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
tr."taskIdentifier",
|
||||
tr."status",
|
||||
COUNT(*)
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" as tr
|
||||
WHERE
|
||||
tr."taskIdentifier" IN (${Prisma.join(tasks)})
|
||||
AND tr."projectId" = ${projectId}
|
||||
AND tr."status" IN ('PENDING', 'WAITING_FOR_DEPLOY', 'EXECUTING', 'RETRYING_AFTER_FAILURE', 'WAITING_TO_RESUME')
|
||||
GROUP BY
|
||||
tr."taskIdentifier",
|
||||
tr."status"
|
||||
ORDER BY
|
||||
tr."taskIdentifier" ASC,
|
||||
tr."status" ASC;`;
|
||||
|
||||
return statuses.reduce((acc, a) => {
|
||||
let existingTask = acc[a.taskIdentifier];
|
||||
|
||||
if (!existingTask) {
|
||||
existingTask = {
|
||||
queued: 0,
|
||||
running: 0,
|
||||
};
|
||||
|
||||
acc[a.taskIdentifier] = existingTask;
|
||||
}
|
||||
|
||||
if (QUEUED_STATUSES.includes(a.status)) {
|
||||
existingTask.queued += Number(a.count);
|
||||
}
|
||||
if (RUNNING_STATUSES.includes(a.status)) {
|
||||
existingTask.running += Number(a.count);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, { queued: number; running: number }>);
|
||||
}
|
||||
|
||||
async #getAverageDurations(tasks: string[], projectId: string) {
|
||||
const durations = await this._replica.$queryRaw<
|
||||
{
|
||||
taskIdentifier: string;
|
||||
duration: Number;
|
||||
}[]
|
||||
>`
|
||||
SELECT
|
||||
tr."taskIdentifier",
|
||||
AVG(EXTRACT(EPOCH FROM (tr."updatedAt" - tr."lockedAt"))) as duration
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" as tr
|
||||
WHERE
|
||||
tr."taskIdentifier" IN (${Prisma.join(tasks)})
|
||||
AND tr."projectId" = ${projectId}
|
||||
AND tr."createdAt" >= (current_date - interval '6 days')
|
||||
AND tr."status" IN ('COMPLETED_SUCCESSFULLY', 'COMPLETED_WITH_ERRORS')
|
||||
GROUP BY
|
||||
tr."taskIdentifier";`;
|
||||
|
||||
return Object.fromEntries(durations.map((s) => [s.taskIdentifier, Number(s.duration)]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +63,18 @@ export class TestPresenter {
|
||||
const searchParams = createSearchParams(url, TestSearchParams);
|
||||
|
||||
//no environmentId
|
||||
if (!searchParams.success || !searchParams.params.get("environment")) {
|
||||
if (!searchParams.success) {
|
||||
return {
|
||||
hasSelectedEnvironment: false as const,
|
||||
environments,
|
||||
};
|
||||
}
|
||||
|
||||
//default to dev environment
|
||||
const environment = searchParams.params.get("environment") ?? "dev";
|
||||
|
||||
//is the environmentId valid?
|
||||
const matchingEnvironment = project.environments.find(
|
||||
(env) => env.slug === searchParams.params.get("environment")
|
||||
);
|
||||
const matchingEnvironment = project.environments.find((env) => env.slug === environment);
|
||||
if (!matchingEnvironment) {
|
||||
return {
|
||||
hasSelectedEnvironment: false as const,
|
||||
@@ -101,7 +102,7 @@ export class TestPresenter {
|
||||
WHERE "runtimeEnvironmentId" = ${matchingEnvironment.id}
|
||||
),
|
||||
latest_workers AS (SELECT * FROM workers WHERE rn = 1)
|
||||
SELECT bwt.id, version, slug as "taskIdentifier", "filePath", "exportName", bwt."friendlyId"
|
||||
SELECT bwt.id, version, slug as "taskIdentifier", "filePath", "exportName", bwt."friendlyId", bwt."triggerSource"
|
||||
FROM latest_workers
|
||||
JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" bwt ON bwt."workerId" = latest_workers.id
|
||||
ORDER BY bwt."exportName" ASC;
|
||||
|
||||
@@ -171,17 +171,23 @@ export class TestTaskPresenter {
|
||||
return {
|
||||
triggerSource: "SCHEDULED",
|
||||
task: taskWithEnvironment,
|
||||
runs: await Promise.all(
|
||||
latestRuns.map(async (r) => {
|
||||
const number = Number(r.number);
|
||||
runs: (
|
||||
await Promise.all(
|
||||
latestRuns.map(async (r) => {
|
||||
const number = Number(r.number);
|
||||
|
||||
return {
|
||||
...r,
|
||||
number,
|
||||
payload: await getScheduleTaskRunPayload(r),
|
||||
};
|
||||
})
|
||||
),
|
||||
const payload = await getScheduleTaskRunPayload(r);
|
||||
|
||||
if (payload.success) {
|
||||
return {
|
||||
...r,
|
||||
number,
|
||||
payload: payload.data,
|
||||
};
|
||||
}
|
||||
})
|
||||
)
|
||||
).filter(Boolean),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -189,6 +195,6 @@ export class TestTaskPresenter {
|
||||
|
||||
async function getScheduleTaskRunPayload(run: RawRun) {
|
||||
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
|
||||
const parsed = ScheduledTaskPayload.parse(payload);
|
||||
const parsed = ScheduledTaskPayload.safeParse(payload);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Span, SpanKind } from "@opentelemetry/api";
|
||||
import { PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { $replica, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { attributesFromAuthenticatedEnv, tracer } from "../../v3/tracer.server";
|
||||
|
||||
export abstract class BasePresenter {
|
||||
constructor(protected readonly _prisma: PrismaClientOrTransaction = prisma) {}
|
||||
constructor(
|
||||
protected readonly _prisma: PrismaClientOrTransaction = prisma,
|
||||
protected readonly _replica: PrismaClientOrTransaction = $replica
|
||||
) {}
|
||||
|
||||
protected async traceWithEnv<T>(
|
||||
trace: string,
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ export default function Page() {
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
{job.hasIntegrationsRequiringAction && (
|
||||
<Callout variant="error" to={organizationIntegrationsPath(organization)} className="mb-2">
|
||||
{simplur`This Job has ${
|
||||
@@ -96,6 +96,6 @@ export default function Page() {
|
||||
</div>
|
||||
)}
|
||||
</Help>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,5 +1,4 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { AstroLogo } from "~/assets/logos/AstroLogo";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
@@ -11,18 +10,17 @@ import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { projectSetupPath } from "~/utils/pathBuilder";
|
||||
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
|
||||
|
||||
export default function SetUpAstro() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const { apiKey } = useV2OnboardingApiKey();
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
@@ -67,7 +65,7 @@ export default function SetUpAstro() {
|
||||
title="Run the CLI 'init' command in an existing Astro project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple
|
||||
|
||||
+3
-7
@@ -1,5 +1,4 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { ExpressLogo } from "~/assets/logos/ExpressLogo";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
@@ -11,20 +10,17 @@ import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { projectSetupPath } from "~/utils/pathBuilder";
|
||||
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
|
||||
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const appOrigin = useAppOrigin();
|
||||
const { apiKey } = useV2OnboardingApiKey();
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl pt-16">
|
||||
@@ -73,7 +69,7 @@ export default function Page() {
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
value={apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
|
||||
+3
-6
@@ -1,5 +1,4 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { NestjsLogo } from "~/assets/logos/NestjsLogo";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { TriggerDevStep } from "~/components/SetupCommands";
|
||||
@@ -11,12 +10,12 @@ import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { projectSetupPath } from "~/utils/pathBuilder";
|
||||
import { CodeBlock } from "../../components/code/CodeBlock";
|
||||
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
|
||||
|
||||
const AppModuleCode = `
|
||||
import { Module } from '@nestjs/common';
|
||||
@@ -114,11 +113,9 @@ export default function SetupNestJS() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
const { apiKey } = useV2OnboardingApiKey();
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
invariant(devEnvironment, "devEnvironment is required");
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl pt-16">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
@@ -161,7 +158,7 @@ export default function SetupNestJS() {
|
||||
<CodeBlock
|
||||
fileName=".env"
|
||||
showChrome
|
||||
code={`TRIGGER_API_KEY=${devEnvironment.apiKey}\nTRIGGER_API_URL=${appOrigin}`}
|
||||
code={`TRIGGER_API_KEY=${apiKey}\nTRIGGER_API_URL=${appOrigin}`}
|
||||
/>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Add the TriggerDevModule" />
|
||||
|
||||
+4
-7
@@ -1,6 +1,5 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import { useState } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { NextjsLogo } from "~/assets/logos/NextjsLogo";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
@@ -21,11 +20,11 @@ import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { projectSetupPath } from "~/utils/pathBuilder";
|
||||
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
|
||||
|
||||
type SelectionChoices = "use-existing-project" | "create-new-next-app";
|
||||
|
||||
@@ -33,12 +32,10 @@ export default function SetupNextjs() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
const { apiKey } = useV2OnboardingApiKey();
|
||||
const appOrigin = useAppOrigin();
|
||||
const [selectedValue, setSelectedValue] = useState<SelectionChoices | null>(null);
|
||||
|
||||
invariant(devEnvironment, "devEnvironment is required");
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl pt-16">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
@@ -152,7 +149,7 @@ export default function SetupNextjs() {
|
||||
title="Run the CLI 'init' command in your new Next.js project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very
|
||||
simple example Job in <InlineCode variant="extra-small">examples.ts</InlineCode>{" "}
|
||||
@@ -179,7 +176,7 @@ export default function SetupNextjs() {
|
||||
title="Run the CLI 'init' command in an existing Next.js project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very
|
||||
|
||||
+3
-5
@@ -1,5 +1,4 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { RemixLogo } from "~/assets/logos/RemixLogo";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommand, RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
@@ -11,18 +10,17 @@ import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useAppOrigin } from "~/hooks/useAppOrigin";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { projectSetupPath } from "~/utils/pathBuilder";
|
||||
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
|
||||
|
||||
export default function SetUpRemix() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const { apiKey } = useV2OnboardingApiKey();
|
||||
const appOrigin = useAppOrigin();
|
||||
|
||||
return (
|
||||
@@ -67,7 +65,7 @@ export default function SetUpRemix() {
|
||||
title="Run the CLI 'init' command in an existing Remix project"
|
||||
/>
|
||||
<StepContentContainer>
|
||||
<InitCommand appOrigin={appOrigin} apiKey={devEnvironment.apiKey} />
|
||||
<InitCommand appOrigin={appOrigin} apiKey={apiKey} />
|
||||
|
||||
<Paragraph spacing variant="small">
|
||||
You’ll notice a new folder in your project called 'jobs'. We’ve added a very simple
|
||||
|
||||
+4
-5
@@ -1,5 +1,4 @@
|
||||
import { ChatBubbleLeftRightIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||
import invariant from "tiny-invariant";
|
||||
import { SvelteKitLogo } from "~/assets/logos/SveltekitLogo";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { RunDevCommand, TriggerDevStep } from "~/components/SetupCommands";
|
||||
@@ -11,18 +10,18 @@ import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useProjectSetupComplete } from "~/hooks/useProjectSetupComplete";
|
||||
import { projectSetupPath } from "~/utils/pathBuilder";
|
||||
import { useV2OnboardingApiKey } from "../_app.orgs.$organizationSlug.projects.$projectParam.setup/route";
|
||||
|
||||
export default function SetUpSveltekit() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
useProjectSetupComplete();
|
||||
const devEnvironment = useDevEnvironment();
|
||||
invariant(devEnvironment, "Dev environment must be defined");
|
||||
const { apiKey } = useV2OnboardingApiKey();
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl pt-16">
|
||||
<div className="mb-12 grid place-items-center">
|
||||
@@ -70,7 +69,7 @@ export default function SetUpSveltekit() {
|
||||
<ClipboardField
|
||||
secure
|
||||
className="w-fit"
|
||||
value={devEnvironment.apiKey}
|
||||
value={apiKey}
|
||||
variant={"secondary/medium"}
|
||||
icon={<Badge variant="outline">Server</Badge>}
|
||||
/>
|
||||
|
||||
+44
@@ -1,4 +1,48 @@
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson } from "remix-typedjson";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useTypedMatchData, useTypedMatchesData } from "~/hooks/useTypedMatchData";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { ProjectParamSchema } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
const environment = await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
organization: {
|
||||
slug: organizationSlug,
|
||||
},
|
||||
project: {
|
||||
slug: projectParam,
|
||||
},
|
||||
orgMember: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!environment) {
|
||||
throw new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
return typedjson({
|
||||
apiKey: environment.apiKey,
|
||||
});
|
||||
};
|
||||
|
||||
export function useV2OnboardingApiKey() {
|
||||
const routeMatch = useTypedMatchesData<typeof loader>({
|
||||
id: "routes/_app.orgs.$organizationSlug.projects.$projectParam.setup",
|
||||
});
|
||||
if (!routeMatch) {
|
||||
throw new Error("Route match not found");
|
||||
}
|
||||
|
||||
return routeMatch;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
|
||||
+261
-62
@@ -1,21 +1,24 @@
|
||||
import { ChatBubbleLeftRightIcon } from "@heroicons/react/20/solid";
|
||||
import { ChatBubbleLeftRightIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid";
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
|
||||
import { useEffect } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import invariant from "tiny-invariant";
|
||||
import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { TaskRunStatus } from "@trigger.dev/database";
|
||||
import { Fragment, Suspense, useEffect, useState } from "react";
|
||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
|
||||
import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { InitCommandV3, TriggerDevStepV3 } from "~/components/SetupCommands";
|
||||
import { InitCommandV3, TriggerDevStepV3, TriggerLoginStepV3 } from "~/components/SetupCommands";
|
||||
import { StepContentContainer } from "~/components/StepContentContainer";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime, formatDateTime } from "~/components/primitives/DateTime";
|
||||
import { Header1, Header2, Header3 } from "~/components/primitives/Headers";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { StepNumber } from "~/components/primitives/StepNumber";
|
||||
import {
|
||||
Table,
|
||||
@@ -28,18 +31,22 @@ import {
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { TaskFunctionName, TaskPath } from "~/components/runs/v3/TaskPath";
|
||||
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { TaskFunctionName } from "~/components/runs/v3/TaskPath";
|
||||
import {
|
||||
TaskRunStatusCombo,
|
||||
TaskRunStatusIcon,
|
||||
runStatusClassNameColor,
|
||||
runStatusTitle,
|
||||
} from "~/components/runs/v3/TaskRunStatus";
|
||||
import {
|
||||
TaskTriggerSourceIcon,
|
||||
taskTriggerSourceDescription,
|
||||
} from "~/components/runs/v3/TaskTriggerSource";
|
||||
import { useDevEnvironment } from "~/hooks/useEnvironments";
|
||||
import { useEventSource } from "~/hooks/useEventSource";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server";
|
||||
import { TaskActivity, TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, v3RunsPath, v3TasksStreamingPath } from "~/utils/pathBuilder";
|
||||
@@ -50,14 +57,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
|
||||
try {
|
||||
const presenter = new TaskListPresenter();
|
||||
const tasks = await presenter.call({
|
||||
const { tasks, userHasTasks, activity, runningStats, durations } = await presenter.call({
|
||||
userId,
|
||||
organizationSlug,
|
||||
projectSlug: projectParam,
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
return typeddefer({
|
||||
tasks,
|
||||
userHasTasks,
|
||||
activity,
|
||||
runningStats,
|
||||
durations,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -71,8 +82,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
const { tasks } = useTypedLoaderData<typeof loader>();
|
||||
const { tasks, userHasTasks, activity, runningStats, durations } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
const hasTasks = tasks.length > 0;
|
||||
|
||||
//live reload the page when the tasks change
|
||||
@@ -97,35 +108,30 @@ export default function Page() {
|
||||
<div className={cn("grid h-full grid-cols-1 gap-4")}>
|
||||
<div className="h-full">
|
||||
{hasTasks ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 pb-4">
|
||||
{!userHasTasks && <UserHasNoTasks />}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Task ID</TableHeaderCell>
|
||||
<TableHeaderCell>Task</TableHeaderCell>
|
||||
<TableHeaderCell>Path</TableHeaderCell>
|
||||
<TableHeaderCell>Environment</TableHeaderCell>
|
||||
<TableHeaderCell>Running</TableHeaderCell>
|
||||
<TableHeaderCell>Queued</TableHeaderCell>
|
||||
<TableHeaderCell>Activity (7d)</TableHeaderCell>
|
||||
<TableHeaderCell>Avg. duration</TableHeaderCell>
|
||||
<TableHeaderCell>Environments</TableHeaderCell>
|
||||
<TableHeaderCell>Last run</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
<div className="sr-only">Last run status</div>
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>Created at</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>Go to page</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.length > 0 ? (
|
||||
tasks.map((task) => {
|
||||
const usernameForEnv =
|
||||
user.id !== task.environment.userId
|
||||
? task.environment.userName
|
||||
: undefined;
|
||||
const path = v3RunsPath(organization, project, {
|
||||
tasks: [task.slug],
|
||||
environments: [task.environment.id],
|
||||
});
|
||||
return (
|
||||
<TableRow key={task.id} className="group">
|
||||
<TableRow key={task.slug} className="group">
|
||||
<TableCell to={path}>
|
||||
<div className="flex items-center gap-2">
|
||||
<SimpleTooltip
|
||||
@@ -135,44 +141,101 @@ export default function Page() {
|
||||
<span>{task.slug}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<TableCell to={path} className="py-0" actionClassName="py-0">
|
||||
<TaskFunctionName
|
||||
functionName={task.exportName}
|
||||
variant="extra-small"
|
||||
variant="extra-extra-small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell to={path}>{task.filePath}</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
<Spinner color="muted" />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TypedAwait resolve={runningStats}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData?.running ?? "0";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={runningStats}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData?.queued ?? "0";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0" actionClassName="py-0">
|
||||
<Suspense fallback={<TaskActivityBlankState />}>
|
||||
<TypedAwait resolve={activity}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return (
|
||||
<>
|
||||
{taskData !== undefined ? (
|
||||
<div className="h-6 w-[5.125rem] rounded-sm">
|
||||
<TaskActivityGraph activity={taskData} />
|
||||
</div>
|
||||
) : (
|
||||
<TaskActivityBlankState />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path} className="p-0">
|
||||
<Suspense fallback={<></>}>
|
||||
<TypedAwait resolve={durations}>
|
||||
{(data) => {
|
||||
const taskData = data[task.slug];
|
||||
return taskData
|
||||
? formatDurationMilliseconds(taskData * 1000, {
|
||||
style: "short",
|
||||
})
|
||||
: "–";
|
||||
}}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<EnvironmentLabel
|
||||
environment={task.environment}
|
||||
userName={usernameForEnv}
|
||||
/>
|
||||
<div className="space-x-2">
|
||||
{task.environments.map((environment) => (
|
||||
<EnvironmentLabel
|
||||
key={environment.id}
|
||||
environment={environment}
|
||||
userName={environment.userName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell to={path}>
|
||||
{task.latestRun ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
classForTaskRunStatus(task.latestRun.status)
|
||||
"flex items-center gap-1",
|
||||
runStatusClassNameColor(task.latestRun.status)
|
||||
)}
|
||||
>
|
||||
<TaskRunStatusIcon
|
||||
status={task.latestRun.status}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<DateTime date={task.latestRun.createdAt} />
|
||||
</div>
|
||||
) : (
|
||||
"Never run"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{task.latestRun ? (
|
||||
<TaskRunStatusCombo status={task.latestRun.status} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<DateTime date={task.createdAt} />
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} />
|
||||
</TableRow>
|
||||
);
|
||||
@@ -188,7 +251,9 @@ export default function Page() {
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<CreateTaskInstructions />
|
||||
<MainCenteredContainer className="max-w-prose">
|
||||
<CreateTaskInstructions />
|
||||
</MainCenteredContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,19 +262,9 @@ export default function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
function classForTaskRunStatus(status: TaskRunStatus) {
|
||||
switch (status) {
|
||||
case "SYSTEM_FAILURE":
|
||||
case "COMPLETED_WITH_ERRORS":
|
||||
return "text-error";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function CreateTaskInstructions() {
|
||||
return (
|
||||
<MainCenteredContainer className="max-w-prose">
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between border-b">
|
||||
<Header1 spacing>Get setup in 3 minutes</Header1>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -240,6 +295,150 @@ function CreateTaskInstructions() {
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</MainCenteredContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserHasNoTasks() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Callout
|
||||
variant="info"
|
||||
cta={
|
||||
<Button
|
||||
variant="tertiary/small"
|
||||
TrailingIcon={open ? ChevronUpIcon : ChevronDownIcon}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
{open ? "Close" : "Setup your dev environment"}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{open ? (
|
||||
<div>
|
||||
<Header2 spacing>Get setup in 3 minutes</Header2>
|
||||
|
||||
<StepNumber stepNumber="1" title="Open up your project" className="mt-6" />
|
||||
<StepContentContainer>
|
||||
<Paragraph>You'll need to open a terminal at the root of your project.</Paragraph>
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="2" title="Run the CLI 'login' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerLoginStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="3" title="Run the CLI 'dev' command" />
|
||||
<StepContentContainer>
|
||||
<TriggerDevStepV3 />
|
||||
</StepContentContainer>
|
||||
<StepNumber stepNumber="4" title="Waiting for tasks" displaySpinner />
|
||||
<StepContentContainer>
|
||||
<Paragraph>This page will automatically refresh.</Paragraph>
|
||||
</StepContentContainer>
|
||||
</div>
|
||||
) : (
|
||||
"Your DEV environment isn't setup yet."
|
||||
)}
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskActivityGraph({ activity }: { activity: TaskActivity }) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={activity}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
width={82}
|
||||
height={24}
|
||||
>
|
||||
<Tooltip
|
||||
cursor={{ fill: "transparent" }}
|
||||
content={<CustomTooltip />}
|
||||
allowEscapeViewBox={{ x: true, y: true }}
|
||||
wrapperStyle={{ zIndex: 1000 }}
|
||||
/>
|
||||
{/* The background */}
|
||||
<Bar
|
||||
dataKey="bg"
|
||||
background={{ fill: "#212327" }}
|
||||
strokeWidth={0}
|
||||
stackId="a"
|
||||
barSize={10}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Bar dataKey="PENDING" fill="#5F6570" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar dataKey="WAITING_FOR_DEPLOY" fill="#F59E0B" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar dataKey="EXECUTING" fill="#3B82F6" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar
|
||||
dataKey="RETRYING_AFTER_FAILURE"
|
||||
fill="#3B82F6"
|
||||
stackId="a"
|
||||
strokeWidth={0}
|
||||
barSize={10}
|
||||
/>
|
||||
<Bar dataKey="WAITING_TO_RESUME" fill="#3B82F6" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar
|
||||
dataKey="COMPLETED_SUCCESSFULLY"
|
||||
fill="#28BF5C"
|
||||
stackId="a"
|
||||
strokeWidth={0}
|
||||
barSize={10}
|
||||
/>
|
||||
<Bar dataKey="CANCELED" fill="#5F6570" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar
|
||||
dataKey="COMPLETED_WITH_ERRORS"
|
||||
fill="#F43F5E"
|
||||
stackId="a"
|
||||
strokeWidth={0}
|
||||
barSize={10}
|
||||
/>
|
||||
<Bar dataKey="INTERRUPTED" fill="#F43F5E" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar dataKey="SYSTEM_FAILURE" fill="#F43F5E" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar dataKey="PAUSED" fill="#FCD34D" stackId="a" strokeWidth={0} barSize={10} />
|
||||
<Bar dataKey="CRASHED" fill="#F43F5E" stackId="a" strokeWidth={0} barSize={10} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskActivityBlankState() {
|
||||
return (
|
||||
<div className="flex h-6 w-[5.125rem] items-center gap-0.5 rounded-sm">
|
||||
{[...Array(7)].map((_, i) => (
|
||||
<div key={i} className="h-full w-2.5 bg-[#212327]" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
|
||||
if (active && payload) {
|
||||
const items = payload.map((p) => ({
|
||||
status: p.dataKey as TaskRunStatus,
|
||||
value: p.value,
|
||||
}));
|
||||
const title = payload[0].payload.day as string;
|
||||
const formattedDate = formatDateTime(new Date(title), "UTC", [], false, false);
|
||||
return (
|
||||
<div className="rounded-sm border border-grid-bright bg-background-dimmed px-3 py-2">
|
||||
<Header3 className="border-b-charcoal-650 border-b pb-2">{formattedDate}</Header3>
|
||||
<div className="mt-2 grid grid-cols-[1fr_auto] gap-2 text-xs text-text-bright">
|
||||
{items.map((item) => (
|
||||
<Fragment key={item.status}>
|
||||
<TaskRunStatusCombo status={item.status} />
|
||||
<p>{item.value}</p>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
+95
-6
@@ -1,10 +1,12 @@
|
||||
import { BookOpenIcon } from "@heroicons/react/20/solid";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Form } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { RegenerateApiKeyModal } from "~/components/environments/RegenerateApiKeyModal";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { ClipboardField } from "~/components/primitives/ClipboardField";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
@@ -20,11 +22,15 @@ import {
|
||||
TableRow,
|
||||
} from "~/components/primitives/Table";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { createEnvironment } from "~/models/organization.server";
|
||||
import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, docsPath } from "~/utils/pathBuilder";
|
||||
import { ProjectParamSchema, docsPath, v3ApiKeysPath } from "~/utils/pathBuilder";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -32,13 +38,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
|
||||
try {
|
||||
const presenter = new ApiKeysPresenter();
|
||||
const { environments } = await presenter.call({
|
||||
const { environments, hasStaging } = await presenter.call({
|
||||
userId,
|
||||
projectSlug: projectParam,
|
||||
});
|
||||
|
||||
return typedjson({
|
||||
environments,
|
||||
hasStaging,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -49,9 +56,76 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
|
||||
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const project = await prisma.project.findUnique({
|
||||
where: {
|
||||
slug: params.projectParam,
|
||||
organization: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
environments: {
|
||||
select: {
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return redirectWithErrorMessage(
|
||||
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
|
||||
request,
|
||||
"Project not found"
|
||||
);
|
||||
}
|
||||
|
||||
if (project.environments.some((env) => env.type === "STAGING")) {
|
||||
return redirectWithErrorMessage(
|
||||
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
|
||||
request,
|
||||
"You already have a staging environment"
|
||||
);
|
||||
}
|
||||
|
||||
const environment = await createEnvironment(
|
||||
{ id: project.organizationId },
|
||||
{ id: project.id },
|
||||
"STAGING"
|
||||
);
|
||||
|
||||
if (!environment) {
|
||||
return redirectWithErrorMessage(
|
||||
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
|
||||
request,
|
||||
"Failed to create staging environment"
|
||||
);
|
||||
}
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
v3ApiKeysPath({ slug: organizationSlug }, { slug: projectParam }),
|
||||
request,
|
||||
"Staging environment created"
|
||||
);
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { environments } = useTypedLoaderData<typeof loader>();
|
||||
const project = useProject();
|
||||
const { environments, hasStaging } = useTypedLoaderData<typeof loader>();
|
||||
const { isManagedCloud } = useFeatures();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -127,6 +201,21 @@ export default function Page() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{!hasStaging && (
|
||||
<Callout
|
||||
variant="info"
|
||||
cta={
|
||||
<Form method="post">
|
||||
<Button variant="tertiary/small">Enable Staging</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
{isManagedCloud
|
||||
? "The Staging environment will be a paid feature when we add billing. In the interim you can enable it for free."
|
||||
: "You can add a Staging environment to your project."}
|
||||
</Callout>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
|
||||
+287
-98
@@ -1,35 +1,43 @@
|
||||
import { Submission, conform, useForm } from "@conform-to/react";
|
||||
import {
|
||||
FieldConfig,
|
||||
list,
|
||||
requestIntent,
|
||||
useFieldList,
|
||||
useFieldset,
|
||||
useForm,
|
||||
} from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { Form, useActionData, useLocation, useNavigate, useNavigation } from "@remix-run/react";
|
||||
import { PlusIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useActionData, useNavigate, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { RefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import {
|
||||
environmentTextClassName,
|
||||
environmentTitle,
|
||||
} from "~/components/environments/EnvironmentLabel";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Checkbox } from "~/components/primitives/Checkbox";
|
||||
import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog";
|
||||
import { Fieldset } from "~/components/primitives/Fieldset";
|
||||
import { FormButtons } from "~/components/primitives/FormButtons";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
import { Hint } from "~/components/primitives/Hint";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { InputGroup } from "~/components/primitives/InputGroup";
|
||||
import { Label } from "~/components/primitives/Label";
|
||||
import { Switch } from "~/components/primitives/Switch";
|
||||
import { prisma } from "~/db.server";
|
||||
import { useList } from "~/hooks/useList";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { EnvironmentVariablesPresenter } from "~/presenters/v3/EnvironmentVariablesPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import {
|
||||
ProjectParamSchema,
|
||||
v3EnvironmentVariablesPath,
|
||||
v3NewEnvironmentVariablesPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ProjectParamSchema, v3EnvironmentVariablesPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
||||
import { CreateEnvironmentVariable } from "~/v3/environmentVariables/repository";
|
||||
import { EnvironmentVariableKey } from "~/v3/environmentVariables/repository";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -47,7 +55,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
environments,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Response(undefined, {
|
||||
status: 400,
|
||||
statusText: "Something went wrong, if this problem persists please contact support.",
|
||||
@@ -55,9 +62,39 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
}
|
||||
};
|
||||
|
||||
const Variable = z.object({
|
||||
key: EnvironmentVariableKey,
|
||||
value: z.string().nonempty("Value is required"),
|
||||
});
|
||||
|
||||
type Variable = z.infer<typeof Variable>;
|
||||
|
||||
const schema = z.object({
|
||||
action: z.enum(["create", "create-more"]),
|
||||
...CreateEnvironmentVariable.shape,
|
||||
overwrite: z.preprocess((i) => {
|
||||
if (i === "true") return true;
|
||||
if (i === "false") return false;
|
||||
return;
|
||||
}, z.boolean()),
|
||||
environmentIds: z.preprocess((i) => {
|
||||
if (typeof i === "string") return [i];
|
||||
|
||||
if (Array.isArray(i)) {
|
||||
const ids = i.filter((v) => typeof v === "string" && v !== "");
|
||||
if (ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
return;
|
||||
}, z.array(z.string(), { required_error: "At least one environment is required" })),
|
||||
variables: z.preprocess((i) => {
|
||||
if (!Array.isArray(i)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return i;
|
||||
}, Variable.array().nonempty("At least one variable is required")),
|
||||
});
|
||||
|
||||
export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
@@ -92,22 +129,22 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
const result = await repository.create(project.id, userId, submission.value);
|
||||
|
||||
if (!result.success) {
|
||||
submission.error.key = result.error;
|
||||
if (result.variableErrors) {
|
||||
for (const { key, error } of result.variableErrors) {
|
||||
const index = submission.value.variables.findIndex((v) => v.key === key);
|
||||
|
||||
if (index !== -1) {
|
||||
submission.error[`variables[${index}].key`] = error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
submission.error.variables = result.error;
|
||||
}
|
||||
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
switch (submission.value.action) {
|
||||
case "create":
|
||||
return redirect(
|
||||
v3EnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam })
|
||||
);
|
||||
case "create-more":
|
||||
return redirectWithSuccessMessage(
|
||||
v3NewEnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam }),
|
||||
request,
|
||||
`Created ${submission.value.key} environment variable`
|
||||
);
|
||||
}
|
||||
return redirect(v3EnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam }));
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
@@ -118,15 +155,11 @@ export default function Page() {
|
||||
const navigate = useNavigate();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const keyFieldRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isLoading =
|
||||
navigation.state !== "idle" &&
|
||||
navigation.formMethod === "post" &&
|
||||
navigation.formData?.get("action") === "create";
|
||||
const isLoading = navigation.state !== "idle" && navigation.formMethod === "post";
|
||||
|
||||
const [form, { key }] = useForm({
|
||||
id: "create-environment-variable",
|
||||
const [form, { environmentIds, variables }] = useForm({
|
||||
id: "create-environment-variables",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
onValidate({ formData }) {
|
||||
@@ -141,14 +174,6 @@ export default function Page() {
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (navigation.state !== "idle") return;
|
||||
if (lastSubmission !== undefined) return;
|
||||
|
||||
form.ref.current?.reset();
|
||||
keyFieldRef.current?.focus();
|
||||
}, [navigation.state, lastSubmission]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
@@ -159,60 +184,63 @@ export default function Page() {
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>New environment variable</DialogHeader>
|
||||
<Form method="post" {...form.props}>
|
||||
<DialogHeader>New environment variables</DialogHeader>
|
||||
<Form
|
||||
method="post"
|
||||
{...form.props}
|
||||
className="max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
>
|
||||
<Fieldset className="mt-2">
|
||||
<InputGroup fullWidth>
|
||||
<Label>Key</Label>
|
||||
<Input
|
||||
{...conform.input(key)}
|
||||
placeholder="e.g. CLIENT_KEY"
|
||||
autoFocus
|
||||
ref={keyFieldRef}
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup fullWidth>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Values</Label>
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Reveal values"
|
||||
checked={revealAll}
|
||||
onCheckedChange={(e) => setRevealAll(e.valueOf())}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-2">
|
||||
{environments.map((environment, index) => {
|
||||
return (
|
||||
<Fragment key={environment.id}>
|
||||
<input
|
||||
type="hidden"
|
||||
name={`values[${index}].environmentId`}
|
||||
value={environment.id}
|
||||
/>
|
||||
<label
|
||||
className="flex items-center justify-end"
|
||||
htmlFor={`values[${index}].value`}
|
||||
<Label>Environments</Label>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{environments.map((environment) => (
|
||||
<Checkbox
|
||||
key={environment.id}
|
||||
id={environment.id}
|
||||
value={environment.id}
|
||||
name="environmentIds"
|
||||
type="radio"
|
||||
label={
|
||||
<span
|
||||
className={cn("text-xs uppercase", environmentTextClassName(environment))}
|
||||
>
|
||||
<EnvironmentLabel environment={environment} className="h-5 px-2" />
|
||||
</label>
|
||||
<Input
|
||||
type={revealAll ? "text" : "password"}
|
||||
name={`values[${index}].value`}
|
||||
placeholder="Not set"
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{environmentTitle(environment)}
|
||||
</span>
|
||||
}
|
||||
variant="button"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<FormError id={environmentIds.errorId}>{environmentIds.error}</FormError>
|
||||
<Hint>
|
||||
Dev environment variables specified here will be overridden by ones in your .env
|
||||
file when running locally.
|
||||
</Hint>
|
||||
</InputGroup>
|
||||
<Hint>Tip: Paste your .env into this form to populate it:</Hint>
|
||||
<InputGroup fullWidth>
|
||||
<FieldLayout>
|
||||
<Label>Keys</Label>
|
||||
<div className="flex justify-between gap-1">
|
||||
<Label>Values</Label>
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Reveal"
|
||||
checked={revealAll}
|
||||
onCheckedChange={(e) => setRevealAll(e.valueOf())}
|
||||
/>
|
||||
</div>
|
||||
</FieldLayout>
|
||||
<VariableFields
|
||||
revealValues={revealAll}
|
||||
formId={form.id}
|
||||
formRef={form.ref}
|
||||
variablesFields={variables}
|
||||
/>
|
||||
<FormError id={variables.errorId}>{variables.error}</FormError>
|
||||
</InputGroup>
|
||||
|
||||
<Callout variant="info" className="inline-flex">
|
||||
Dev environment variables specified here will be overriden by ones in your{" "}
|
||||
<InlineCode variant="extra-small">.env</InlineCode> file when running locally.
|
||||
</Callout>
|
||||
|
||||
<FormError id={key.errorId}>{key.error}</FormError>
|
||||
<FormError>{form.error}</FormError>
|
||||
<FormButtons
|
||||
confirmButton={
|
||||
@@ -221,18 +249,18 @@ export default function Page() {
|
||||
type="submit"
|
||||
variant="primary/small"
|
||||
disabled={isLoading}
|
||||
name="action"
|
||||
value="create-more"
|
||||
name="overwrite"
|
||||
value="false"
|
||||
>
|
||||
{isLoading ? "Saving" : "Save and add another"}
|
||||
{isLoading ? "Saving" : "Save"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary/small"
|
||||
disabled={isLoading}
|
||||
name="action"
|
||||
value="create"
|
||||
name="overwrite"
|
||||
value="true"
|
||||
>
|
||||
{isLoading ? "Saving" : "Save"}
|
||||
{isLoading ? "Overwriting" : "Overwrite"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@@ -251,3 +279,164 @@ export default function Page() {
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLayout({ children }: { children: React.ReactNode }) {
|
||||
return <div className="grid w-full grid-cols-[1fr_1fr_2rem] gap-2">{children}</div>;
|
||||
}
|
||||
|
||||
function VariableFields({
|
||||
revealValues,
|
||||
formId,
|
||||
variablesFields,
|
||||
formRef,
|
||||
}: {
|
||||
revealValues: boolean;
|
||||
formId?: string;
|
||||
variablesFields: FieldConfig<any>;
|
||||
formRef: RefObject<HTMLFormElement>;
|
||||
}) {
|
||||
const {
|
||||
items,
|
||||
append,
|
||||
update,
|
||||
delete: remove,
|
||||
insertAfter,
|
||||
} = useList<Variable>([{ key: "", value: "" }]);
|
||||
|
||||
const handlePaste = useCallback((index: number, e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const clipboardData = e.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
let text = clipboardData.getData("text");
|
||||
//replace carriage returns
|
||||
text = text.replace(/\r/g, "");
|
||||
const lines = text.split("\n");
|
||||
|
||||
const keyValuePairs = lines.flatMap((line) => {
|
||||
if (line.trim().startsWith("#")) return [];
|
||||
|
||||
const split = line.split("=");
|
||||
if (split.length === 2) {
|
||||
return [{ key: split[0], value: split[1] }];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
if (keyValuePairs.length === 0) return;
|
||||
|
||||
//prevent default pasting
|
||||
e.preventDefault();
|
||||
|
||||
const [firstPair, ...rest] = keyValuePairs;
|
||||
update(index, firstPair);
|
||||
|
||||
for (const pair of rest) {
|
||||
requestIntent(formRef.current ?? undefined, list.append(variablesFields.name));
|
||||
}
|
||||
insertAfter(index, rest);
|
||||
}, []);
|
||||
|
||||
const fields = useFieldList(formRef, variablesFields);
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
const item = items[index];
|
||||
|
||||
return (
|
||||
<VariableField
|
||||
formId={formId}
|
||||
key={index}
|
||||
index={index}
|
||||
value={item}
|
||||
onChange={(value) => update(index, value)}
|
||||
onPaste={(e) => handlePaste(index, e)}
|
||||
onDelete={() => {
|
||||
requestIntent(
|
||||
formRef.current ?? undefined,
|
||||
list.remove(variablesFields.name, { index })
|
||||
);
|
||||
remove(index);
|
||||
}}
|
||||
showDeleteButton={items.length > 1}
|
||||
showValue={revealValues}
|
||||
config={field}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="tertiary/medium"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
requestIntent(formRef.current ?? undefined, list.append(variablesFields.name));
|
||||
append([{ key: "", value: "" }]);
|
||||
}}
|
||||
LeadingIcon={PlusIcon}
|
||||
>
|
||||
Add another
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function VariableField({
|
||||
formId,
|
||||
index,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onDelete,
|
||||
showDeleteButton,
|
||||
showValue,
|
||||
config,
|
||||
}: {
|
||||
formId?: string;
|
||||
index: number;
|
||||
value: Variable;
|
||||
onChange: (value: Variable) => void;
|
||||
onPaste: (e: React.ClipboardEvent<HTMLInputElement>) => void;
|
||||
onDelete: () => void;
|
||||
showDeleteButton: boolean;
|
||||
showValue: boolean;
|
||||
config: FieldConfig<Variable>;
|
||||
}) {
|
||||
const ref = useRef<HTMLFieldSetElement>(null);
|
||||
const fields = useFieldset(ref, config);
|
||||
const baseFieldName = `variables[${index}]`;
|
||||
|
||||
return (
|
||||
<fieldset ref={ref}>
|
||||
<FieldLayout>
|
||||
<Input
|
||||
id={`${formId}-${baseFieldName}.key`}
|
||||
name={`${baseFieldName}.key`}
|
||||
placeholder="e.g. CLIENT_KEY"
|
||||
value={value.key}
|
||||
onChange={(e) => onChange({ ...value, key: e.currentTarget.value })}
|
||||
autoFocus={index === 0}
|
||||
onPaste={onPaste}
|
||||
/>
|
||||
<Input
|
||||
id={`${formId}-${baseFieldName}.value`}
|
||||
name={`${baseFieldName}.value`}
|
||||
type={showValue ? "text" : "password"}
|
||||
placeholder="Not set"
|
||||
value={value.value}
|
||||
onChange={(e) => onChange({ ...value, value: e.currentTarget.value })}
|
||||
/>
|
||||
{showDeleteButton && (
|
||||
<Button
|
||||
variant="minimal/medium"
|
||||
type="button"
|
||||
onClick={() => onDelete()}
|
||||
LeadingIcon={XMarkIcon}
|
||||
/>
|
||||
)}
|
||||
</FieldLayout>
|
||||
<div className="space-y-2">
|
||||
<FormError id={fields.key.errorId}>{fields.key.error}</FormError>
|
||||
<FormError id={fields.value.errorId}>{fields.value.error}</FormError>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-2
@@ -187,8 +187,9 @@ export default function Page() {
|
||||
to={v3NewEnvironmentVariablesPath(organization, project)}
|
||||
variant="primary/small"
|
||||
LeadingIcon={PlusIcon}
|
||||
shortcut={{ key: "n" }}
|
||||
>
|
||||
New environment variable
|
||||
Add new
|
||||
</LinkButton>
|
||||
</div>
|
||||
<Table>
|
||||
@@ -247,7 +248,7 @@ export default function Page() {
|
||||
</Table>
|
||||
|
||||
<Callout variant="info" className="mb-4">
|
||||
Dev environment variables specified here will be overriden by ones in your .env file
|
||||
Dev environment variables specified here will be overridden by ones in your .env file
|
||||
when running locally.
|
||||
</Callout>
|
||||
</div>
|
||||
|
||||
+209
-89
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
ArrowsPointingOutIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Outlet, useNavigate, useParams, useRevalidator } from "@remix-run/react";
|
||||
import type { Location } from "@remix-run/react";
|
||||
import { useParams, useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { Virtualizer } from "@tanstack/react-virtual";
|
||||
import {
|
||||
@@ -23,7 +26,7 @@ import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody } from "~/components/layout/AppLayout";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/primitives/Resizable";
|
||||
import { ShortcutKey, variants } from "~/components/primitives/ShortcutKey";
|
||||
import { Slider } from "~/components/primitives/Slider";
|
||||
import { Switch } from "~/components/primitives/Switch";
|
||||
import * as Timeline from "~/components/primitives/Timeline";
|
||||
@@ -45,8 +49,9 @@ import { useDebounce } from "~/hooks/useDebounce";
|
||||
import { useEventSource } from "~/hooks/useEventSource";
|
||||
import { useInitialDimensions } from "~/hooks/useInitialDimensions";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { useReplaceLocation } from "~/hooks/useReplaceLocation";
|
||||
import { Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { RunEvent, RunPresenter } from "~/presenters/v3/RunPresenter.server";
|
||||
import { getResizableRunSettings, setResizableRunSettings } from "~/services/resizablePanel";
|
||||
@@ -60,6 +65,11 @@ import {
|
||||
v3RunStreamingPath,
|
||||
v3RunsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route";
|
||||
import { number } from "zod";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Popover, PopoverArrowTrigger, PopoverContent } from "~/components/primitives/Popover";
|
||||
import { Header3 } from "~/components/primitives/Headers";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -82,19 +92,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
});
|
||||
};
|
||||
|
||||
function getSpanId(path: string): string | undefined {
|
||||
const regex = /spans\/([^\/]*)/;
|
||||
const match = path.match(regex);
|
||||
return match ? match[1] : undefined;
|
||||
function getSpanId(location: Location<any>): string | undefined {
|
||||
const search = new URLSearchParams(location.search);
|
||||
return search.get("span") ?? undefined;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { run, trace, resizeSettings } = useTypedLoaderData<typeof loader>();
|
||||
const navigate = useNavigate();
|
||||
const organization = useOrganization();
|
||||
const pathName = usePathName();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
const { location, replaceSearchParam } = useReplaceLocation();
|
||||
const selectedSpanId = getSpanId(location);
|
||||
|
||||
const usernameForEnv = user.id !== run.environment.userId ? run.environment.userName : undefined;
|
||||
|
||||
@@ -133,10 +142,8 @@ export default function Page() {
|
||||
|
||||
const { events, parentRunFriendlyId, duration, rootSpanStatus, rootStartedAt } = trace;
|
||||
|
||||
const selectedSpanId = getSpanId(pathName);
|
||||
|
||||
const changeToSpan = useDebounce((selectedSpan: string) => {
|
||||
navigate(v3RunSpanPath(organization, project, run, { spanId: selectedSpan }));
|
||||
replaceSearchParam("span", selectedSpan);
|
||||
}, 250);
|
||||
|
||||
const revalidator = useRevalidator();
|
||||
@@ -166,62 +173,47 @@ export default function Page() {
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
<div className={cn("grid h-full max-h-full grid-cols-1 overflow-hidden")}>
|
||||
{selectedSpanId === undefined ? (
|
||||
<TasksTreeView
|
||||
selectedId={selectedSpanId}
|
||||
key={events[0]?.id ?? "-"}
|
||||
events={events}
|
||||
parentRunFriendlyId={parentRunFriendlyId}
|
||||
onSelectedIdChanged={(selectedSpan) => {
|
||||
//instantly close the panel if no span is selected
|
||||
if (!selectedSpan) {
|
||||
navigate(v3RunPath(organization, project, run));
|
||||
return;
|
||||
}
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="h-full max-h-full"
|
||||
onLayout={(layout) => {
|
||||
if (layout.length !== 2) return;
|
||||
if (!selectedSpanId) return;
|
||||
setResizableRunSettings(document, layout);
|
||||
}}
|
||||
>
|
||||
<ResizablePanel order={1} minSize={30} defaultSize={resizeSettings.layout?.[0]}>
|
||||
<TasksTreeView
|
||||
selectedId={selectedSpanId}
|
||||
key={events[0]?.id ?? "-"}
|
||||
events={events}
|
||||
parentRunFriendlyId={parentRunFriendlyId}
|
||||
onSelectedIdChanged={(selectedSpan) => {
|
||||
//instantly close the panel if no span is selected
|
||||
if (!selectedSpan) {
|
||||
replaceSearchParam("span");
|
||||
return;
|
||||
}
|
||||
|
||||
changeToSpan(selectedSpan);
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
environmentType={run.environment.type}
|
||||
/>
|
||||
) : (
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="h-full max-h-full"
|
||||
onLayout={(layout) => {
|
||||
if (layout.length !== 2) return;
|
||||
setResizableRunSettings(document, layout);
|
||||
}}
|
||||
>
|
||||
<ResizablePanel order={1} minSize={30} defaultSize={resizeSettings.layout?.[0]}>
|
||||
<TasksTreeView
|
||||
selectedId={selectedSpanId}
|
||||
key={events[0]?.id ?? "-"}
|
||||
events={events}
|
||||
parentRunFriendlyId={parentRunFriendlyId}
|
||||
onSelectedIdChanged={(selectedSpan) => {
|
||||
//instantly close the panel if no span is selected
|
||||
if (!selectedSpan) {
|
||||
navigate(v3RunPath(organization, project, run));
|
||||
return;
|
||||
}
|
||||
|
||||
changeToSpan(selectedSpan);
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
environmentType={run.environment.type}
|
||||
changeToSpan(selectedSpan);
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
environmentType={run.environment.type}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
{selectedSpanId && (
|
||||
<ResizablePanel order={2} minSize={30} defaultSize={resizeSettings.layout?.[1]}>
|
||||
<SpanView
|
||||
runParam={run.friendlyId}
|
||||
spanId={selectedSpanId}
|
||||
closePanel={() => replaceSearchParam("span")}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel order={2} minSize={30} defaultSize={resizeSettings.layout?.[1]}>
|
||||
<Outlet key={selectedSpanId} />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
)}
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</PageBody>
|
||||
</>
|
||||
@@ -263,6 +255,9 @@ function TasksTreeView({
|
||||
getNodeProps,
|
||||
toggleNodeSelection,
|
||||
toggleExpandNode,
|
||||
expandAllBelowDepth,
|
||||
toggleExpandLevel,
|
||||
collapseAllBelowDepth,
|
||||
selectNode,
|
||||
scrollToNode,
|
||||
virtualizer,
|
||||
@@ -286,7 +281,7 @@ function TasksTreeView({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-rows-[2.5rem_1fr] overflow-hidden">
|
||||
<div className="grid h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden">
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
<Input
|
||||
placeholder="Search log"
|
||||
@@ -297,30 +292,12 @@ function TasksTreeView({
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<LiveReloadingStatus rootSpanCompleted={rootSpanStatus !== "executing"} />
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Errors only"
|
||||
checked={errorsOnly}
|
||||
onCheckedChange={(e) => setErrorsOnly(e.valueOf())}
|
||||
/>
|
||||
<Switch
|
||||
variant="small"
|
||||
label="Show durations"
|
||||
checked={showDurations}
|
||||
onCheckedChange={(e) => setShowDurations(e.valueOf())}
|
||||
/>
|
||||
<Slider
|
||||
variant={"tertiary"}
|
||||
className="w-20"
|
||||
LeadingIcon={MagnifyingGlassMinusIcon}
|
||||
TrailingIcon={MagnifyingGlassPlusIcon}
|
||||
value={[scale]}
|
||||
onValueChange={(value) => setScale(value[0])}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ResizablePanelGroup
|
||||
@@ -333,14 +310,15 @@ function TasksTreeView({
|
||||
{/* Tree list */}
|
||||
<ResizablePanel order={1} minSize={20} defaultSize={50} className="pl-3">
|
||||
<div className="grid h-full grid-rows-[2rem_1fr] overflow-hidden">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center pr-2">
|
||||
{parentRunFriendlyId ? (
|
||||
<ShowParentLink runFriendlyId={parentRunFriendlyId} />
|
||||
) : (
|
||||
<Paragraph variant="small" className="text-charcoal-500">
|
||||
<Paragraph variant="small" className="flex-1 text-charcoal-500">
|
||||
This is the root task
|
||||
</Paragraph>
|
||||
)}
|
||||
<LiveReloadingStatus rootSpanCompleted={rootSpanStatus !== "executing"} />
|
||||
</div>
|
||||
<TreeView
|
||||
parentRef={parentRef}
|
||||
@@ -355,13 +333,13 @@ function TasksTreeView({
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"delay-[25ms] flex h-8 cursor-pointer items-center overflow-hidden rounded-l-sm pr-2 transition-colors",
|
||||
"flex h-8 cursor-pointer items-center overflow-hidden rounded-l-sm pr-2",
|
||||
state.selected
|
||||
? "bg-grid-dimmed hover:bg-grid-bright"
|
||||
: "bg-transparent hover:bg-grid-dimmed"
|
||||
)}
|
||||
onClick={() => {
|
||||
toggleNodeSelection(node.id);
|
||||
selectNode(node.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 items-center">
|
||||
@@ -379,7 +357,15 @@ function TasksTreeView({
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
toggleExpandNode(node.id);
|
||||
if (e.altKey) {
|
||||
if (state.expanded) {
|
||||
collapseAllBelowDepth(node.level);
|
||||
} else {
|
||||
expandAllBelowDepth(node.level);
|
||||
}
|
||||
} else {
|
||||
toggleExpandNode(node.id);
|
||||
}
|
||||
scrollToNode(node.id);
|
||||
}}
|
||||
>
|
||||
@@ -445,6 +431,50 @@ function TasksTreeView({
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
|
||||
<div className="grow @container">
|
||||
<div className="hidden items-center gap-4 @[42rem]:flex">
|
||||
<KeyboardShortcuts
|
||||
expandAllBelowDepth={expandAllBelowDepth}
|
||||
collapseAllBelowDepth={collapseAllBelowDepth}
|
||||
toggleExpandLevel={toggleExpandLevel}
|
||||
setShowDurations={setShowDurations}
|
||||
/>
|
||||
</div>
|
||||
<div className="@[42rem]:hidden">
|
||||
<Popover>
|
||||
<PopoverArrowTrigger>Shortcuts</PopoverArrowTrigger>
|
||||
<PopoverContent
|
||||
className="min-w-[20rem] overflow-y-auto p-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
align="start"
|
||||
>
|
||||
<Header3 spacing>Keyboard shortcuts</Header3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<KeyboardShortcuts
|
||||
expandAllBelowDepth={expandAllBelowDepth}
|
||||
collapseAllBelowDepth={collapseAllBelowDepth}
|
||||
toggleExpandLevel={toggleExpandLevel}
|
||||
setShowDurations={setShowDurations}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Slider
|
||||
variant={"tertiary"}
|
||||
className="w-20"
|
||||
LeadingIcon={MagnifyingGlassMinusIcon}
|
||||
TrailingIcon={MagnifyingGlassPlusIcon}
|
||||
value={[scale]}
|
||||
onValueChange={(value) => setScale(value[0])}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -738,6 +768,7 @@ function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) {
|
||||
fullWidth
|
||||
textAlignLeft
|
||||
shortcut={{ key: "p" }}
|
||||
className="flex-1"
|
||||
>
|
||||
{mouseOver ? (
|
||||
<ShowParentIconSelected className="h-4 w-4 text-indigo-500" />
|
||||
@@ -884,3 +915,92 @@ function ConnectedDevWarning() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardShortcuts({
|
||||
expandAllBelowDepth,
|
||||
collapseAllBelowDepth,
|
||||
toggleExpandLevel,
|
||||
setShowDurations,
|
||||
}: {
|
||||
expandAllBelowDepth: (depth: number) => void;
|
||||
collapseAllBelowDepth: (depth: number) => void;
|
||||
toggleExpandLevel: (depth: number) => void;
|
||||
setShowDurations: (show: (show: boolean) => boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ArrowKeyShortcuts />
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "e" }}
|
||||
action={() => expandAllBelowDepth(0)}
|
||||
title="Expand all"
|
||||
/>
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "c" }}
|
||||
action={() => collapseAllBelowDepth(1)}
|
||||
title="Collapse all"
|
||||
/>
|
||||
<NumberShortcuts toggleLevel={(number) => toggleExpandLevel(number)} />
|
||||
<ShortcutWithAction
|
||||
shortcut={{ key: "d" }}
|
||||
action={() => setShowDurations((d) => !d)}
|
||||
title="Toggle durations"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrowKeyShortcuts() {
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ShortcutKey shortcut={{ key: "arrowup" }} variant="medium" className="ml-0 mr-0" />
|
||||
<ShortcutKey shortcut={{ key: "arrowdown" }} variant="medium" className="ml-0 mr-0" />
|
||||
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium" className="ml-0 mr-0" />
|
||||
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium" className="ml-0 mr-0" />
|
||||
<Paragraph variant="extra-small" className="ml-1.5 whitespace-nowrap">
|
||||
Navigate
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ShortcutWithAction({
|
||||
shortcut,
|
||||
title,
|
||||
action,
|
||||
}: {
|
||||
shortcut: Shortcut;
|
||||
title: string;
|
||||
action: () => void;
|
||||
}) {
|
||||
useShortcutKeys({
|
||||
shortcut,
|
||||
action,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ShortcutKey shortcut={shortcut} variant="medium" className="ml-0 mr-0" />
|
||||
<Paragraph variant="extra-small" className="ml-1.5 whitespace-nowrap">
|
||||
{title}
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberShortcuts({ toggleLevel }: { toggleLevel: (depth: number) => void }) {
|
||||
useHotkeys(["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], (event, hotkeysEvent) => {
|
||||
toggleLevel(Number(event.key));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className={cn(variants.medium, "ml-0 mr-0")}>0</span>
|
||||
<span className="text-[0.75rem] text-text-dimmed">–</span>
|
||||
<span className={cn(variants.medium, "ml-0 mr-0")}>9</span>
|
||||
<Paragraph variant="extra-small" className="ml-1.5 whitespace-nowrap">
|
||||
Toggle level
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -34,6 +34,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
|
||||
const presenter = new RunListPresenter();
|
||||
const list = await presenter.call({
|
||||
userId,
|
||||
projectSlug: projectParam,
|
||||
tasks,
|
||||
versions,
|
||||
@@ -88,7 +89,6 @@ export default function Page() {
|
||||
filters={list.filters}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
currentUser={user}
|
||||
/>
|
||||
<ListPagination list={list} className="mt-2 justify-end" />
|
||||
</div>
|
||||
|
||||
-1
@@ -247,7 +247,6 @@ export default function Page() {
|
||||
}}
|
||||
runs={schedule.runs}
|
||||
isLoading={false}
|
||||
currentUser={user}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
||||
+7
-3
@@ -6,7 +6,6 @@ import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runt
|
||||
import { TaskRunStatus } from "@trigger.dev/database";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { z } from "zod";
|
||||
import { JSONEditor } from "~/components/code/JSONEditor";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
@@ -37,7 +36,7 @@ import {
|
||||
TestTaskPresenter,
|
||||
} from "~/presenters/v3/TestTaskPresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { docsPath, v3RunPath, v3TaskParamsSchema } from "~/utils/pathBuilder";
|
||||
import { docsPath, v3RunSpanPath, v3TaskParamsSchema } from "~/utils/pathBuilder";
|
||||
import { TestTaskService } from "~/v3/services/testTask.server";
|
||||
import { TestTaskData } from "~/v3/testTask";
|
||||
|
||||
@@ -77,7 +76,12 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
}
|
||||
|
||||
return redirectWithSuccessMessage(
|
||||
v3RunPath({ slug: organizationSlug }, { slug: projectParam }, { friendlyId: run.friendlyId }),
|
||||
v3RunSpanPath(
|
||||
{ slug: organizationSlug },
|
||||
{ slug: projectParam },
|
||||
{ friendlyId: run.friendlyId },
|
||||
{ spanId: run.spanId }
|
||||
),
|
||||
request,
|
||||
"Test run created"
|
||||
);
|
||||
|
||||
+96
-39
@@ -9,6 +9,7 @@ import {
|
||||
} from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { RadioButtonCircle } from "~/components/primitives/RadioButton";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
@@ -32,6 +34,7 @@ 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,
|
||||
@@ -67,7 +70,7 @@ export default function Page() {
|
||||
|
||||
//get optimistic location for the segment control
|
||||
const optimisticLocation = useOptimisticLocation();
|
||||
const environment = new URLSearchParams(optimisticLocation.search).get("environment");
|
||||
const environment = new URLSearchParams(optimisticLocation.search).get("environment") ?? "dev";
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
@@ -150,11 +153,50 @@ function TaskSelector({
|
||||
tasks: TaskListItem[];
|
||||
environmentSlug: string;
|
||||
}) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-charcoal-800 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<div className="px-2 pb-2">
|
||||
<Input
|
||||
placeholder="Search tasks"
|
||||
variant="medium"
|
||||
icon="search"
|
||||
fullWidth={true}
|
||||
value={filterText}
|
||||
autoFocus
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -166,42 +208,17 @@ function TaskSelector({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tasks.map((t) => {
|
||||
const path = v3TestTaskPath(organization, project, t, environmentSlug);
|
||||
const { isActive, isPending } = useLinkStatus(path);
|
||||
return (
|
||||
<TableRow
|
||||
key={t.taskIdentifier}
|
||||
className={cn(
|
||||
(isActive || isPending) &&
|
||||
"z-20 rounded-sm outline outline-1 outline-offset-[-1px] outline-secondary"
|
||||
)}
|
||||
>
|
||||
<TableCell to={path} actionClassName="pl-2.5 pr-1 py-1">
|
||||
<RadioButtonCircle checked={isActive || isPending} />
|
||||
</TableCell>
|
||||
<TableCell to={path} actionClassName="pl-1 pr-2 py-1">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<TaskFunctionName
|
||||
variant="extra-small"
|
||||
functionName={t.exportName}
|
||||
className="-ml-1 inline-flex"
|
||||
/>
|
||||
<div className="flex items-start gap-1">
|
||||
<TaskTriggerSourceIcon source={t.triggerSource} className="size-3.5" />
|
||||
<Paragraph variant="extra-small" className="text-text-dimmed">
|
||||
{t.taskIdentifier}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell to={path} actionClassName="px-2 py-1">
|
||||
{t.filePath}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{filteredItems.length > 0 ? (
|
||||
filteredItems.map((t) => (
|
||||
<TaskRow key={t.friendlyId} task={t} environmentSlug={environmentSlug} />
|
||||
))
|
||||
) : (
|
||||
<TableBlankRow colSpan={3}>
|
||||
<Paragraph spacing variant="small">
|
||||
No tasks match "{filterText}"
|
||||
</Paragraph>
|
||||
</TableBlankRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -217,3 +234,43 @@ function NoTaskInstructions({ environment }: { environment?: SelectedEnvironment
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskRow({ task, environmentSlug }: { task: TaskListItem; environmentSlug: string }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
|
||||
const path = v3TestTaskPath(organization, project, task, environmentSlug);
|
||||
const { isActive, isPending } = useLinkStatus(path);
|
||||
return (
|
||||
<TableRow
|
||||
key={task.taskIdentifier}
|
||||
className={cn(
|
||||
(isActive || isPending) &&
|
||||
"z-20 rounded-sm outline outline-1 outline-offset-[-1px] outline-secondary"
|
||||
)}
|
||||
>
|
||||
<TableCell to={path} actionClassName="pl-2.5 pr-1 py-1">
|
||||
<RadioButtonCircle checked={isActive || isPending} />
|
||||
</TableCell>
|
||||
<TableCell to={path} actionClassName="pl-1 pr-2 py-1">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<TaskFunctionName
|
||||
variant="extra-small"
|
||||
functionName={task.exportName}
|
||||
className="-ml-1 inline-flex"
|
||||
/>
|
||||
<div className="flex items-start gap-1">
|
||||
<TaskTriggerSourceIcon source={task.triggerSource} className="size-3.5" />
|
||||
<Paragraph variant="extra-small" className="text-text-dimmed">
|
||||
{task.taskIdentifier}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell to={path} actionClassName="px-2 py-1">
|
||||
{task.filePath}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { parseBatchTriggerTaskRequestBody } from "@trigger.dev/core/v3";
|
||||
import { BatchTriggerTaskRequestBody } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { MAX_BATCH_TRIGGER_ITEMS } from "~/consts";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
@@ -46,7 +46,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
const body = parseBatchTriggerTaskRequestBody(anyBody);
|
||||
const body = BatchTriggerTaskRequestBody.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { parseTriggerTaskRequestBody } from "@trigger.dev/core/v3";
|
||||
import { TriggerTaskRequestBody } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
@@ -52,7 +52,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
// Now parse the request body
|
||||
const anyBody = await request.json();
|
||||
|
||||
const body = parseTriggerTaskRequestBody(anyBody);
|
||||
const body = TriggerTaskRequestBody.safeParse(anyBody);
|
||||
|
||||
if (!body.success) {
|
||||
return json({ error: "Invalid request body" }, { status: 400 });
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { v3RunSpanPath } from "~/utils/pathBuilder";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectRef: z.string(),
|
||||
@@ -33,8 +34,20 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
const run = await prisma.taskRun.findUnique({
|
||||
where: {
|
||||
friendlyId: validatedParams.runParam,
|
||||
},
|
||||
});
|
||||
|
||||
if (!run) {
|
||||
throw new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
// Redirect to the project's runs page
|
||||
return redirect(
|
||||
`/orgs/${project.organization.slug}/projects/v3/${project.slug}/runs/${validatedParams.runParam}`
|
||||
v3RunSpanPath({ slug: project.organization.slug }, { slug: project.slug }, run, {
|
||||
spanId: run.spanId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
+53
-13
@@ -4,10 +4,11 @@ import {
|
||||
QueueListIcon,
|
||||
StopCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useParams } from "@remix-run/react";
|
||||
import { useFetcher, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDurationNanoseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEffect } from "react";
|
||||
import { typedjson, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
@@ -17,6 +18,7 @@ import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { Spinner } from "~/components/primitives/Spinner";
|
||||
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
|
||||
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
|
||||
import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog";
|
||||
@@ -58,19 +60,57 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
return typedjson({ span });
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const {
|
||||
span: { event },
|
||||
} = useTypedLoaderData<typeof loader>();
|
||||
export function SpanView({
|
||||
runParam,
|
||||
spanId,
|
||||
closePanel,
|
||||
}: {
|
||||
runParam: string;
|
||||
spanId: string | undefined;
|
||||
closePanel: () => void;
|
||||
}) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const { runParam } = useParams();
|
||||
const fetcher = useTypedFetcher<typeof loader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (spanId === undefined) return;
|
||||
fetcher.load(
|
||||
`/resources/orgs/${organization.slug}/projects/v3/${project.slug}/runs/${runParam}/spans/${spanId}`
|
||||
);
|
||||
}, [organization.slug, project.slug, runParam, spanId]);
|
||||
|
||||
if (spanId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (fetcher.state !== "idle" || fetcher.data === undefined) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright"
|
||||
)}
|
||||
>
|
||||
<div className="mx-3 flex items-center gap-2 overflow-x-hidden border-b border-grid-dimmed">
|
||||
<div className="size-4 bg-grid-dimmed" />
|
||||
<div className="h-6 w-[60%] bg-grid-dimmed" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
span: { event },
|
||||
} = fetcher.data;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full max-h-full overflow-hidden bg-background-bright",
|
||||
event.showActionBar ? "grid-rows-[2.5rem_1fr_2.5rem]" : "grid-rows-[2.5rem_1fr]"
|
||||
event.showActionBar ? "grid-rows-[2.5rem_1fr_3.25rem]" : "grid-rows-[2.5rem_1fr]"
|
||||
)}
|
||||
>
|
||||
<div className="mx-3 flex items-center justify-between gap-2 overflow-x-hidden border-b border-grid-dimmed">
|
||||
@@ -85,8 +125,8 @@ export default function Page() {
|
||||
</Header2>
|
||||
</div>
|
||||
{runParam && (
|
||||
<LinkButton
|
||||
to={v3RunPath(organization, project, { friendlyId: runParam })}
|
||||
<Button
|
||||
onClick={closePanel}
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={ExitIcon}
|
||||
shortcut={{ key: "esc" }}
|
||||
@@ -188,7 +228,7 @@ export default function Page() {
|
||||
{ friendlyId: event.runId },
|
||||
{ spanId: event.spanId }
|
||||
)}
|
||||
variant="minimal/small"
|
||||
variant="minimal/medium"
|
||||
LeadingIcon={QueueListIcon}
|
||||
shortcut={{ key: "f" }}
|
||||
>
|
||||
@@ -216,7 +256,7 @@ function RunActionButtons({ span }: { span: Span }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="danger/small" LeadingIcon={StopCircleIcon}>
|
||||
<Button variant="danger/medium" LeadingIcon={StopCircleIcon}>
|
||||
Cancel run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -236,7 +276,7 @@ function RunActionButtons({ span }: { span: Span }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="tertiary/small" LeadingIcon={ArrowPathIcon}>
|
||||
<Button variant="tertiary/medium" LeadingIcon={ArrowPathIcon}>
|
||||
Replay run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { v3RunPath } from "~/utils/pathBuilder";
|
||||
import { v3RunSpanPath } from "~/utils/pathBuilder";
|
||||
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
|
||||
|
||||
const FormSchema = z.object({
|
||||
@@ -54,12 +54,13 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const runPath = v3RunPath(
|
||||
const runPath = v3RunSpanPath(
|
||||
{
|
||||
slug: taskRun.project.organization.slug,
|
||||
},
|
||||
{ slug: taskRun.project.slug },
|
||||
{ friendlyId: newRun.friendlyId }
|
||||
{ friendlyId: newRun.friendlyId },
|
||||
{ spanId: newRun.spanId }
|
||||
);
|
||||
|
||||
return redirectWithSuccessMessage(runPath, request, `Replaying run`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { logger } from "./logger.server";
|
||||
|
||||
const environmentSortOrder: RuntimeEnvironmentType[] = [
|
||||
"DEVELOPMENT",
|
||||
@@ -9,12 +10,23 @@ const environmentSortOrder: RuntimeEnvironmentType[] = [
|
||||
|
||||
type SortType = {
|
||||
type: RuntimeEnvironmentType;
|
||||
userName?: string | null;
|
||||
};
|
||||
|
||||
export function sortEnvironments<T extends SortType>(environments: T[]): T[] {
|
||||
return environments.sort((a, b) => {
|
||||
const aIndex = environmentSortOrder.indexOf(a.type);
|
||||
const bIndex = environmentSortOrder.indexOf(b.type);
|
||||
return aIndex - bIndex;
|
||||
|
||||
const difference = aIndex - bIndex;
|
||||
|
||||
if (difference === 0) {
|
||||
//same environment so sort by name
|
||||
const usernameA = a.userName || "";
|
||||
const usernameB = b.userName || "";
|
||||
return usernameA.localeCompare(usernameB);
|
||||
}
|
||||
|
||||
return difference;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,20 +134,27 @@ export async function authenticatePersonalAccessToken(
|
||||
|
||||
const hashedToken = hashToken(token);
|
||||
|
||||
const personalAccessToken = await prisma.personalAccessToken.update({
|
||||
const personalAccessToken = await prisma.personalAccessToken.findFirst({
|
||||
where: {
|
||||
hashedToken,
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!personalAccessToken) {
|
||||
// The token may have been revoked or is entirely invalid
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.personalAccessToken.update({
|
||||
where: {
|
||||
id: personalAccessToken.id,
|
||||
},
|
||||
data: {
|
||||
lastAccessedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!personalAccessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decryptedToken = decryptPersonalAccessToken(personalAccessToken);
|
||||
|
||||
if (decryptedToken !== token) {
|
||||
@@ -210,6 +217,18 @@ export async function createPersonalAccessTokenFromAuthorizationCode(
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCliPersonalAccessToken.revokedAt) {
|
||||
// re-activate revoked CLI PAT so we can use it again
|
||||
await prisma.personalAccessToken.update({
|
||||
where: {
|
||||
id: existingCliPersonalAccessToken.id,
|
||||
},
|
||||
data: {
|
||||
revokedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//we don't return the decrypted token
|
||||
return {
|
||||
id: existingCliPersonalAccessToken.id,
|
||||
|
||||
@@ -32,6 +32,14 @@ export class ContinueRunService {
|
||||
throw new Error("Run is not resumable");
|
||||
}
|
||||
|
||||
// Delete any tasks that are errored
|
||||
const erroredTasks = await tx.task.findMany({
|
||||
where: {
|
||||
runId: runId,
|
||||
status: "ERRORED",
|
||||
},
|
||||
});
|
||||
|
||||
await tx.jobRun.update({
|
||||
where: { id: runId },
|
||||
data: {
|
||||
@@ -45,9 +53,15 @@ export class ContinueRunService {
|
||||
},
|
||||
});
|
||||
|
||||
for (const task of erroredTasks) {
|
||||
await tx.task.delete({
|
||||
where: { id: task.id },
|
||||
});
|
||||
}
|
||||
|
||||
await ResumeRunService.enqueue(run, tx);
|
||||
},
|
||||
{ timeout: 10000 }
|
||||
{ timeout: 30_000 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ import { Job } from "~/models/job.server";
|
||||
import type { Organization } from "~/models/organization.server";
|
||||
import type { Project } from "~/models/project.server";
|
||||
import { objectToSearchParams } from "./searchParams";
|
||||
import { ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
|
||||
export type OrgForPath = Pick<Organization, "slug">;
|
||||
export type ProjectForPath = Pick<Project, "slug">;
|
||||
@@ -368,7 +366,7 @@ export function v3RunSpanPath(
|
||||
run: v3RunForPath,
|
||||
span: v3SpanForPath
|
||||
) {
|
||||
return `${v3RunPath(organization, project, run)}/spans/${span.spanId}`;
|
||||
return `${v3RunPath(organization, project, run)}?span=${span.spanId}`;
|
||||
}
|
||||
|
||||
export function v3TraceSpanPath(
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
ZodMessageHandler,
|
||||
ZodMessageSender,
|
||||
clientWebsocketMessages,
|
||||
serverWebsocketMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { clientWebsocketMessages, serverWebsocketMessages } from "@trigger.dev/core/v3";
|
||||
import { ZodMessageHandler, ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
|
||||
import { Evt } from "evt";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { CloseEvent, ErrorEvent, MessageEvent, WebSocket } from "ws";
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Prisma, PrismaClient } from "@trigger.dev/database";
|
||||
import { Prisma, PrismaClient, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { environmentTitle } from "~/components/environments/EnvironmentLabel";
|
||||
import { $transaction, prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { getSecretStore } from "~/services/secrets/secretStore.server";
|
||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { EnvironmentVariable, ProjectEnvironmentVariable, Repository, Result } from "./repository";
|
||||
import { env } from "~/env.server";
|
||||
import {
|
||||
CreateResult,
|
||||
EnvironmentVariable,
|
||||
ProjectEnvironmentVariable,
|
||||
Repository,
|
||||
Result,
|
||||
} from "./repository";
|
||||
|
||||
function secretKeyProjectPrefix(projectId: string) {
|
||||
return `environmentvariable:${projectId}:`;
|
||||
@@ -35,8 +42,15 @@ export class EnvironmentVariablesRepository implements Repository {
|
||||
async create(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
options: { key: string; values: { value: string; environmentId: string }[] }
|
||||
): Promise<Result> {
|
||||
options: {
|
||||
overwrite: boolean;
|
||||
environmentIds: string[];
|
||||
variables: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
): Promise<CreateResult> {
|
||||
const project = await this.prismaClient.project.findUnique({
|
||||
where: {
|
||||
id: projectId,
|
||||
@@ -55,6 +69,18 @@ export class EnvironmentVariablesRepository implements Repository {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
environmentVariables: {
|
||||
select: {
|
||||
key: true,
|
||||
values: {
|
||||
select: {
|
||||
environment: {
|
||||
select: { id: true, type: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -62,58 +88,109 @@ export class EnvironmentVariablesRepository implements Repository {
|
||||
return { success: false as const, error: "Project not found" };
|
||||
}
|
||||
|
||||
if (options.values.every((v) => !project.environments.some((e) => e.id === v.environmentId))) {
|
||||
if (options.environmentIds.every((v) => !project.environments.some((e) => e.id === v))) {
|
||||
return { success: false as const, error: `Environment not found` };
|
||||
}
|
||||
|
||||
//get rid of empty strings
|
||||
const values = options.values.filter((v) => v.value.trim() !== "");
|
||||
|
||||
//get rid of empty variables
|
||||
const values = options.variables.filter((v) => v.key.trim() !== "" && v.value.trim() !== "");
|
||||
if (values.length === 0) {
|
||||
return { success: false as const, error: `You must set at least one value` };
|
||||
}
|
||||
|
||||
//check if any of them exist in an environment we're setting
|
||||
if (!options.overwrite) {
|
||||
const existingVariableKeys: { key: string; environments: RuntimeEnvironmentType[] }[] = [];
|
||||
for (const variable of values) {
|
||||
const existingVariable = project.environmentVariables.find((v) => v.key === variable.key);
|
||||
if (
|
||||
existingVariable &&
|
||||
existingVariable.values.some((v) => options.environmentIds.includes(v.environment.id))
|
||||
) {
|
||||
existingVariableKeys.push({
|
||||
key: variable.key,
|
||||
environments: existingVariable.values
|
||||
.filter((v) => options.environmentIds.includes(v.environment.id))
|
||||
.map((v) => v.environment.type),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (existingVariableKeys.length > 0) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: `Some of the variables are already set for these environments`,
|
||||
variableErrors: existingVariableKeys.map((val) => ({
|
||||
key: val.key,
|
||||
error: `Variable already set in ${val.environments
|
||||
.map((e) => environmentTitle({ type: e }))
|
||||
.join(", ")}.`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await $transaction(this.prismaClient, async (tx) => {
|
||||
const environmentVariable = await tx.environmentVariable.create({
|
||||
data: {
|
||||
key: options.key,
|
||||
friendlyId: generateFriendlyId("envvar"),
|
||||
project: {
|
||||
connect: {
|
||||
id: projectId,
|
||||
for (const variable of values) {
|
||||
const environmentVariable = await tx.environmentVariable.upsert({
|
||||
where: {
|
||||
projectId_key: {
|
||||
key: variable.key,
|
||||
projectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const secretStore = getSecretStore("DATABASE", {
|
||||
prismaClient: tx,
|
||||
});
|
||||
|
||||
//create the secret values and references
|
||||
for (const value of values) {
|
||||
const key = secretKey(projectId, value.environmentId, options.key);
|
||||
|
||||
//create the secret reference
|
||||
const secretReference = await tx.secretReference.create({
|
||||
data: {
|
||||
key,
|
||||
provider: "DATABASE",
|
||||
create: {
|
||||
key: variable.key,
|
||||
friendlyId: generateFriendlyId("envvar"),
|
||||
project: {
|
||||
connect: {
|
||||
id: projectId,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const variableValue = await tx.environmentVariableValue.create({
|
||||
data: {
|
||||
variableId: environmentVariable.id,
|
||||
environmentId: value.environmentId,
|
||||
valueReferenceId: secretReference.id,
|
||||
},
|
||||
const secretStore = getSecretStore("DATABASE", {
|
||||
prismaClient: tx,
|
||||
});
|
||||
|
||||
await secretStore.setSecret<{ secret: string }>(key, {
|
||||
secret: value.value,
|
||||
});
|
||||
//set the secret values and references
|
||||
for (const environmentId of options.environmentIds) {
|
||||
const key = secretKey(projectId, environmentId, variable.key);
|
||||
|
||||
//create the secret reference
|
||||
const secretReference = await tx.secretReference.upsert({
|
||||
where: {
|
||||
key,
|
||||
},
|
||||
create: {
|
||||
key,
|
||||
provider: "DATABASE",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const variableValue = await tx.environmentVariableValue.upsert({
|
||||
where: {
|
||||
variableId_environmentId: {
|
||||
variableId: environmentVariable.id,
|
||||
environmentId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
variableId: environmentVariable.id,
|
||||
environmentId: environmentId,
|
||||
valueReferenceId: secretReference.id,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
await secretStore.setSecret<{ secret: string }>(key, {
|
||||
secret: variable.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,7 +203,7 @@ export class EnvironmentVariablesRepository implements Repository {
|
||||
if (error.code === "P2002") {
|
||||
return {
|
||||
success: false as const,
|
||||
error: `There's already an environment variable called ${options.key}.`,
|
||||
error: `There was already an existing field`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
|
||||
const EnvironmentVariable = z
|
||||
export const EnvironmentVariableKey = z
|
||||
.string()
|
||||
.nonempty("Environment variable key is required")
|
||||
.regex(/^\w+$/, "Environment variables can only contain alphanumeric characters and underscores");
|
||||
.nonempty("Key is required")
|
||||
.regex(/^\w+$/, "Keys can only use alphanumeric characters and underscores");
|
||||
|
||||
export const CreateEnvironmentVariable = z.object({
|
||||
key: EnvironmentVariable,
|
||||
values: z.array(
|
||||
z.object({
|
||||
environmentId: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
export const CreateEnvironmentVariables = z.object({
|
||||
environmentIds: z.array(z.string()),
|
||||
variables: z.array(z.object({ key: EnvironmentVariableKey, value: z.string() })),
|
||||
});
|
||||
|
||||
export type CreateEnvironmentVariable = z.infer<typeof CreateEnvironmentVariable>;
|
||||
export type CreateEnvironmentVariables = z.infer<typeof CreateEnvironmentVariables>;
|
||||
|
||||
export type CreateResult =
|
||||
| {
|
||||
success: true;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
variableErrors?: { key: string; error: string }[];
|
||||
};
|
||||
|
||||
export const EditEnvironmentVariable = z.object({
|
||||
id: z.string(),
|
||||
@@ -60,7 +65,11 @@ export type EnvironmentVariable = {
|
||||
};
|
||||
|
||||
export interface Repository {
|
||||
create(projectId: string, userId: string, options: CreateEnvironmentVariable): Promise<Result>;
|
||||
create(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
options: CreateEnvironmentVariables
|
||||
): Promise<CreateResult>;
|
||||
edit(projectId: string, userId: string, options: EditEnvironmentVariable): Promise<Result>;
|
||||
getProject(projectId: string, userId: string): Promise<ProjectEnvironmentVariable[]>;
|
||||
getEnvironment(
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
PlatformToProviderMessages,
|
||||
ProviderToPlatformMessages,
|
||||
SharedQueueToClientMessages,
|
||||
ZodNamespace,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { ZodNamespace } from "@trigger.dev/core/v3/zodNamespace";
|
||||
import { Server } from "socket.io";
|
||||
import { env } from "~/env.server";
|
||||
import { singleton } from "~/utils/singleton";
|
||||
@@ -71,6 +71,7 @@ function initializeSocketIOServerInstance() {
|
||||
|
||||
function createCoordinatorNamespace(io: Server) {
|
||||
const coordinator = new ZodNamespace({
|
||||
// @ts-ignore - for some reason the built ZodNamespace Server type is not compatible with the Server type here, but only when doing typechecking
|
||||
io,
|
||||
name: "coordinator",
|
||||
authToken: env.COORDINATOR_SECRET,
|
||||
@@ -147,6 +148,7 @@ function createCoordinatorNamespace(io: Server) {
|
||||
|
||||
function createProviderNamespace(io: Server) {
|
||||
const provider = new ZodNamespace({
|
||||
// @ts-ignore - for some reason the built ZodNamespace Server type is not compatible with the Server type here, but only when doing typechecking
|
||||
io,
|
||||
name: "provider",
|
||||
authToken: env.PROVIDER_SECRET,
|
||||
@@ -181,6 +183,7 @@ function createProviderNamespace(io: Server) {
|
||||
|
||||
function createSharedQueueConsumerNamespace(io: Server) {
|
||||
const sharedQueue = new ZodNamespace({
|
||||
// @ts-ignore - for some reason the built ZodNamespace Server type is not compatible with the Server type here, but only when doing typechecking
|
||||
io,
|
||||
name: "shared-queue",
|
||||
authToken: env.PROVIDER_SECRET,
|
||||
@@ -188,7 +191,9 @@ function createSharedQueueConsumerNamespace(io: Server) {
|
||||
serverMessages: SharedQueueToClientMessages,
|
||||
onConnection: async (socket, handler, sender, logger) => {
|
||||
const sharedSocketConnection = new SharedSocketConnection({
|
||||
// @ts-ignore - for some reason the built ZodNamespace Server type is not compatible with the Server type here, but only when doing typechecking
|
||||
namespace: sharedQueue.namespace,
|
||||
// @ts-ignore - for some reason the built ZodNamespace Server type is not compatible with the Server type here, but only when doing typechecking
|
||||
socket,
|
||||
logger,
|
||||
poolSize: env.SHARED_QUEUE_CONSUMER_POOL_SIZE,
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
TaskRunExecution,
|
||||
TaskRunExecutionPayload,
|
||||
TaskRunExecutionResult,
|
||||
ZodMessageSender,
|
||||
serverWebsocketMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
|
||||
import { BackgroundWorker, BackgroundWorkerTask } from "@trigger.dev/database";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
TaskRunExecutionResult,
|
||||
TaskRunFailedExecutionResult,
|
||||
TaskRunSuccessfulExecutionResult,
|
||||
ZodMessageSender,
|
||||
serverWebsocketMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
|
||||
import {
|
||||
BackgroundWorker,
|
||||
BackgroundWorkerTask,
|
||||
@@ -26,7 +26,6 @@ import { EnvironmentVariablesRepository } from "../environmentVariables/environm
|
||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||
import { socketIo } from "../handleSocketIo.server";
|
||||
import { findCurrentWorkerDeployment } from "../models/workerDeployment.server";
|
||||
import { CancelAttemptService } from "../services/cancelAttempt.server";
|
||||
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
|
||||
import { tracer } from "../tracer.server";
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CoordinatorToPlatformMessages, InferSocketMessageSchema } from "@trigger.dev/core/v3";
|
||||
import { CoordinatorToPlatformMessages } from "@trigger.dev/core/v3";
|
||||
import type { InferSocketMessageSchema } from "@trigger.dev/core/v3/zodSocket";
|
||||
import type {
|
||||
CheckpointRestoreEvent,
|
||||
TaskRunAttemptStatus,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
CoordinatorToPlatformMessages,
|
||||
InferSocketMessageSchema,
|
||||
TaskRunExecution,
|
||||
TaskRunExecutionResult,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import type { InferSocketMessageSchema } from "@trigger.dev/core/v3/zodSocket";
|
||||
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { marqs } from "~/v3/marqs/index.server";
|
||||
|
||||
@@ -76,6 +76,15 @@ export class TriggerTaskService extends BaseService {
|
||||
immediate: true,
|
||||
},
|
||||
async (event, traceContext) => {
|
||||
const runFriendlyId = generateFriendlyId("run");
|
||||
|
||||
const payloadPacket = await this.#handlePayloadPacket(
|
||||
body.payload,
|
||||
body.options?.payloadType ?? "application/json",
|
||||
runFriendlyId,
|
||||
environment
|
||||
);
|
||||
|
||||
const lockId = taskIdentifierToLockId(taskId);
|
||||
|
||||
const run = await $transaction(this._prisma, async (tx) => {
|
||||
@@ -105,15 +114,6 @@ export class TriggerTaskService extends BaseService {
|
||||
event.setAttribute("queueName", queueName);
|
||||
span.setAttribute("queueName", queueName);
|
||||
|
||||
const runFriendlyId = generateFriendlyId("run");
|
||||
|
||||
const payloadPacket = await this.#handlePayloadPacket(
|
||||
body.payload,
|
||||
body.options?.payloadType ?? "application/json",
|
||||
runFriendlyId,
|
||||
environment
|
||||
);
|
||||
|
||||
const taskRun = await tx.taskRun.create({
|
||||
data: {
|
||||
status: "PENDING",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { clientWebsocketMessages, serverWebsocketMessages } from "@trigger.dev/core/v3";
|
||||
import {
|
||||
MessageCatalogToSocketIoEvents,
|
||||
StructuredLogger,
|
||||
ZodMessageHandler,
|
||||
ZodMessageSender,
|
||||
clientWebsocketMessages,
|
||||
serverWebsocketMessages,
|
||||
} from "@trigger.dev/core/v3";
|
||||
MessageCatalogToSocketIoEvents,
|
||||
} from "@trigger.dev/core/v3/zodMessageHandler";
|
||||
import type { StructuredLogger } from "@trigger.dev/core/v3/utils/structuredLogger";
|
||||
import { Evt } from "evt";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Logger } from "@trigger.dev/core-backend";
|
||||
import { ZodMessageCatalogSchema, ZodMessageHandler, ZodMessageSender } from "@trigger.dev/core/v3";
|
||||
import { ZodMessageCatalogSchema, ZodMessageHandler } from "@trigger.dev/core/v3/zodMessageHandler";
|
||||
import { Evt } from "evt";
|
||||
import Redis, { RedisOptions } from "ioredis";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"react-stately": "^3.29.1",
|
||||
"react-use": "^17.4.0",
|
||||
"recharts": "^2.8.0",
|
||||
"recharts": "^2.12.6",
|
||||
"remix-auth": "^3.6.0",
|
||||
"remix-auth-email-link": "2.0.2",
|
||||
"remix-auth-github": "^1.6.0",
|
||||
@@ -150,7 +150,7 @@
|
||||
"simple-oauth2": "^5.0.0",
|
||||
"simplur": "^3.0.1",
|
||||
"slug": "^6.0.0",
|
||||
"socket.io": "^4.7.4",
|
||||
"socket.io": "4.7.4",
|
||||
"socket.io-adapter": "^2.5.4",
|
||||
"sonner": "^1.0.3",
|
||||
"sqs-consumer": "^7.4.0",
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
The CLI `dev` command runs a server for your tasks. It will watches for changes in your `/trigger` directory and communicates with the Trigger.dev platform to register your tasks, perform runs, and send data back and forth.
|
||||
|
||||
It can also update your `@trigger.dev/*` packages to prevent version mismatches and failed deploys. You will always be prompted first.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
|
||||
@@ -21,13 +21,16 @@ yarn dlx trigger.dev@beta deploy
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Warning>Will fail in CI if any version mismatches are detected. Ensure everything runs locally first using the [dev](/v3/cli-dev) command and don't bypass the version checks!</Warning>
|
||||
|
||||
It performs a few steps to deploy:
|
||||
|
||||
1. Typechecks the code.
|
||||
2. Compiles and bundles the code.
|
||||
3. Checks that [environment variables](/v3/deploy-environment-variables) are set.
|
||||
4. Deploys the code to the cloud.
|
||||
5. Registers the tasks as a new version in the environment (prod by default).
|
||||
1. Optionally updates packages when running locally.
|
||||
2. Typechecks the code.
|
||||
3. Compiles and bundles the code.
|
||||
4. Checks that [environment variables](/v3/deploy-environment-variables) are set.
|
||||
5. Deploys the code to the cloud.
|
||||
6. Registers the tasks as a new version in the environment (prod by default).
|
||||
|
||||
## Options
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ yarn dlx trigger.dev@beta dev
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
It will first perform an update check to prevent version mismatches, failed deploys, and other errors. You will always be prompted first.
|
||||
|
||||
You will see in the terminal that the server is running and listening for requests. When you run a task, you will see it in the terminal along with a link to view it in the dashboard.
|
||||
|
||||
It is worth noting that each task runs in a separate Node process. This means that if you have a long-running task, it will not block other tasks from running.
|
||||
|
||||
@@ -5,8 +5,12 @@ description: "You can easily deploy your tasks with GitHub actions."
|
||||
|
||||
This simple GitHub action file will deploy you Trigger.dev tasks when new code is pushed to the `main` branch and the `trigger` directory has changes in it.
|
||||
|
||||
```yaml .github/workflows/release-trigger.yml
|
||||
name: Deploy to Trigger.dev
|
||||
<Warning>The deploy step will fail if any version mismatches are detected. Please see the [version pinning](/v3/github-actions#version-pinning) section for more details.</Warning>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```yaml .github/workflows/release-trigger-prod.yml
|
||||
name: Deploy to Trigger.dev (prod)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -37,8 +41,55 @@ jobs:
|
||||
npx trigger.dev@beta deploy
|
||||
```
|
||||
|
||||
|
||||
```yaml .github/workflows/release-trigger-staging.yml
|
||||
name: Deploy to Trigger.dev (staging)
|
||||
|
||||
# Requires manually calling the workflow from a branch / commit to deploy to staging
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: 🚀 Deploy Trigger.dev
|
||||
env:
|
||||
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
|
||||
run: |
|
||||
npx trigger.dev@beta deploy --env staging
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
If you already have a GitHub action file, you can just add the final step "🚀 Deploy Trigger.dev" to your existing file.
|
||||
|
||||
You need to add the `TRIGGER_ACCESS_TOKEN` secret to your repository. You can create a new access token by going to your profile page and then clicking on the "Personal Access Tokens" tab.
|
||||
|
||||
To set it in GitHub go to your repository, click on "Settings", "Secrets and variables" and then "Actions". Add a new secret with the name `TRIGGER_ACCESS_TOKEN` and use the value of your access token.
|
||||
|
||||
## Version pinning
|
||||
|
||||
The CLI and `@trigger.dev/*` package versions need to be in sync, otherwise there will be errors and unpredictable behavior. Hence, the `deploy` command will automatically fail during CI on any version mismatches.
|
||||
|
||||
To ensure a smooth CI experience you can pin the CLI version in the deploy step, like so:
|
||||
|
||||
```yaml .github/workflows/release-trigger.yml
|
||||
- name: 🚀 Deploy Trigger.dev
|
||||
env:
|
||||
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
|
||||
run: |
|
||||
npx trigger.dev@3.0.0-beta.16 deploy
|
||||
```
|
||||
|
||||
You should use the version you run locally during dev and manual deploy. The current version is displayed in the banner, but you can also check it by appending `--version` to any command.
|
||||
|
||||
+126
-10
@@ -79,7 +79,7 @@ export const taskWithRetries = task({
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: false,
|
||||
},
|
||||
run: async ({ payload, ctx }) => {
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
@@ -99,7 +99,7 @@ export const oneAtATime = task({
|
||||
queue: {
|
||||
concurrencyLimit: 1,
|
||||
},
|
||||
run: async ({ payload, ctx }) => {
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
@@ -116,7 +116,7 @@ export const heavyTask = task({
|
||||
cpu: 2,
|
||||
memory: 4,
|
||||
},
|
||||
run: async ({ payload, ctx }) => {
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
@@ -124,27 +124,143 @@ export const heavyTask = task({
|
||||
|
||||
### `init` function
|
||||
|
||||
This function is called before a run attempt.
|
||||
This function is called before a run attempt:
|
||||
|
||||
```ts /trigger/init.ts
|
||||
export const taskWithInit = task({
|
||||
id: "task-with-init",
|
||||
init: async (payload, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also return data from the `init` function that will be available in the params of the `run`, `cleanup`, `onSuccess`, and `onFailure` functions.
|
||||
|
||||
```ts /trigger/init-return.ts
|
||||
export const taskWithInitReturn = task({
|
||||
id: "task-with-init-return",
|
||||
init: async (payload, { ctx }) => {
|
||||
return { someData: "someValue" };
|
||||
},
|
||||
run: async (payload: any, { ctx, init }) => {
|
||||
console.log(init.someData); // "someValue"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `cleanup` function
|
||||
|
||||
This function is called after a run attempt has succeeded or failed.
|
||||
This function is called after the `run` function is executed, regardless of whether the run was successful or not. It's useful for cleaning up resources, logging, or other side effects.
|
||||
|
||||
```ts /trigger/cleanup.ts
|
||||
export const taskWithCleanup = task({
|
||||
id: "task-with-cleanup",
|
||||
cleanup: async (payload, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `middleware` function
|
||||
|
||||
This function is called before the `run` function, it allows you to wrap the run function with custom code. For more information [read the guide](/v3/middleware).
|
||||
|
||||
### `onStart` function
|
||||
|
||||
When a task run starts, the `onStart` function is called. It's useful for sending notifications, logging, and other side effects. This function will only be called one per run (not per retry). If you want to run code before each retry, use the `init` function.
|
||||
|
||||
```ts /trigger/on-start.ts
|
||||
export const taskWithOnStart = task({
|
||||
id: "task-with-on-start",
|
||||
onStart: async (payload, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define an `onStart` function in your `trigger.config.ts` file to get notified when any task starts.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
onStart: async (payload, { ctx }) => {
|
||||
console.log("Task started", ctx.task.id);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### `onSuccess` function
|
||||
|
||||
When a task attempt succeeds, the `onSuccess` function is called. It's useful for sending notifications, logging, or other side effects.
|
||||
When a task run succeeds, the `onSuccess` function is called. It's useful for sending notifications, logging, syncing state to your database, or other side effects.
|
||||
|
||||
<Snippet file="coming-soon-slim.mdx" />
|
||||
```ts /trigger/on-success.ts
|
||||
export const taskWithOnSuccess = task({
|
||||
id: "task-with-on-success",
|
||||
onSuccess: async (payload, output, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `onError` function
|
||||
You can also define an `onSuccess` function in your `trigger.config.ts` file to get notified when any task succeeds.
|
||||
|
||||
When a task attempt fails, the `onError` function is called. It's useful for sending notifications, logging, or other side effects.
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
|
||||
<Snippet file="coming-soon-slim.mdx" />
|
||||
export const config: TriggerConfig = {
|
||||
onSuccess: async (payload, output, { ctx }) => {
|
||||
console.log("Task succeeded", ctx.task.id);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### `onFailure` function
|
||||
|
||||
When a task run fails, the `onFailure` function is called. It's useful for sending notifications, logging, or other side effects. It will only be executed once the task run has exhausted all its retries.
|
||||
|
||||
```ts /trigger/on-failure.ts
|
||||
export const taskWithOnFailure = task({
|
||||
id: "task-with-on-failure",
|
||||
onFailure: async (payload, error, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define an `onFailure` function in your `trigger.config.ts` file to get notified when any task fails.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
onFailure: async (payload, error, { ctx }) => {
|
||||
console.log("Task failed", ctx.task.id);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### `handleError` functions
|
||||
|
||||
You can define a function that will be called when an error is thrown in the `run` function, that allows you to control how the error is handled and whether the task should be retried.
|
||||
|
||||
Read more about `handleError` in our [Errors and Retrying guide](/v3/errors-retrying).
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
+140
-2
@@ -31,6 +31,46 @@ export const config: TriggerConfig = {
|
||||
|
||||
Most of the time you don't need to change anything in this file, or if you do then we will tell you when you the run the CLI command.
|
||||
|
||||
## Global initialization
|
||||
|
||||
You can run code before any task is run by adding a `init` function to your `trigger.config.ts` file.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
//..other stuff
|
||||
init: async (payload, { ctx }) => {
|
||||
console.log("I run before any task is run");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
You'll have access to the run payload and the context object. Currently you cannot return anything from this function.
|
||||
|
||||
## Lifecycle functions
|
||||
|
||||
You can add lifecycle functions to get notified when any task starts, succeeds, or fails using `onStart`, `onSuccess` and `onFailure`:
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
//..other stuff
|
||||
onSuccess: async (payload, output, { ctx }) => {
|
||||
console.log("Task succeeded", ctx.task.id);
|
||||
},
|
||||
onFailure: async (payload, error, { ctx }) => {
|
||||
console.log("Task failed", ctx.task.id);
|
||||
},
|
||||
onStart: async (payload, { ctx }) => {
|
||||
console.log("Task started", ctx.task.id);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Read more about task lifecycle functions in the [tasks overview](/v3/tasks-overview).
|
||||
|
||||
## Instrumentations
|
||||
|
||||
We use OpenTelemetry (OTEL) for our run logs. This means you get a lot of information about your tasks with no effort. But you probably want to add more information to your logs. For example, here's all the Prisma calls automatically logged:
|
||||
@@ -53,7 +93,7 @@ export const config: TriggerConfig = {
|
||||
|
||||
## ESM-only packages
|
||||
|
||||
We'll let you know when run the CLI dev command if this is a problem. Some packages are ESM-only so they don't work directly from CJS when using Node.js. In that case you need to add them to the `dependenciesToBundle` array in your `trigger.config.ts` file.
|
||||
We'll let you know when running the CLI dev command if this is a problem. Some packages are ESM-only so they don't work directly from CJS when using Node.js. In that case you need to add them to the `dependenciesToBundle` array in your `trigger.config.ts` file.
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
@@ -92,7 +132,9 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
|
||||
|
||||
<Step title="package.json postinstall `prisma generate`">
|
||||
|
||||
```json
|
||||
<CodeGroup>
|
||||
|
||||
```json default path
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "prisma generate"
|
||||
@@ -100,6 +142,16 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
|
||||
}
|
||||
```
|
||||
|
||||
```json custom path
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "prisma generate --schema=./custom/path/to/schema.prisma"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Anything you put in `postinstall` will be run as part of the install step. This is how Next.js recommends you set up Prisma anyway.
|
||||
|
||||
</Step>
|
||||
@@ -111,7 +163,12 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
//..other stuff
|
||||
|
||||
// using the default path
|
||||
additionalFiles: ["./prisma/schema.prisma"],
|
||||
// or a custom path, for example in a monorepo
|
||||
additionalFiles: ["../../custom/path/to/schema.prisma"],
|
||||
|
||||
additionalPackages: ["prisma@5.11.0"],
|
||||
};
|
||||
```
|
||||
@@ -122,6 +179,87 @@ Prisma works by generating a client from your `prisma.schema` file. This means y
|
||||
|
||||
</Steps>
|
||||
|
||||
## TypeORM support
|
||||
|
||||
We support using TypeORM with Trigger. You can use decorators in your entities and then use them in your tasks. Here's an example:
|
||||
|
||||
```ts orm/index.ts
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
import { Entity, Column, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class Photo {
|
||||
@PrimaryColumn()
|
||||
id!: number;
|
||||
|
||||
@Column()
|
||||
name!: string;
|
||||
|
||||
@Column()
|
||||
description!: string;
|
||||
|
||||
@Column()
|
||||
filename!: string;
|
||||
|
||||
@Column()
|
||||
views!: number;
|
||||
|
||||
@Column()
|
||||
isPublished!: boolean;
|
||||
}
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: "postgres",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "my-database",
|
||||
entities: [Photo],
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
});
|
||||
```
|
||||
|
||||
And then in your trigger.config.ts file you can initialize the datasource using the `onStart` lifecycle function option:
|
||||
|
||||
```ts trigger.config.ts
|
||||
import type { TriggerConfig } from "@trigger.dev/sdk/v3";
|
||||
import { AppDataSource } from "@/trigger/orm";
|
||||
|
||||
export const config: TriggerConfig = {
|
||||
// ... other options here
|
||||
onStart: async (payload, { ctx }) => {
|
||||
await AppDataSource.initialize();
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Now you are ready to use this in your tasks:
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk/v3";
|
||||
import { AppDataSource, Photo } from "./orm";
|
||||
|
||||
export const taskThatUsesDecorators = task({
|
||||
id: "task-that-uses-decorators",
|
||||
run: async (payload: { message: string }) => {
|
||||
console.log("Creating a photo...");
|
||||
|
||||
const photo = new Photo();
|
||||
photo.id = 2;
|
||||
photo.name = "Me and Bears";
|
||||
photo.description = "I am near polar bears";
|
||||
photo.filename = "photo-with-bears.jpg";
|
||||
photo.views = 1;
|
||||
photo.isPublished = true;
|
||||
|
||||
await AppDataSource.manager.save(photo);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you have an issue with bundling let us know on [Discord](https://trigger.dev/discord) or [via email](https://trigger.dev/contact).
|
||||
|
||||
@@ -171,25 +171,7 @@ async function yourBackendFunction() {
|
||||
|
||||
## Upgrading your project
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Upgrade the v2 Trigger.dev packages">
|
||||
|
||||
You can run this command to upgrade all the packages to the beta:
|
||||
|
||||
```bash
|
||||
npx @trigger.dev/cli@beta update --to beta
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Follow the v3 quick start">
|
||||
|
||||
Follow the [v3 quick start](/v3/quick-start) to get started with v3.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
Just follow the [v3 quick start](/v3/quick-start) to get started with v3. Our new CLI will take care of the rest.
|
||||
|
||||
## Using v2 together with v3
|
||||
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
# @trigger.dev/airtable
|
||||
|
||||
## 3.0.0-beta.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.23
|
||||
- @trigger.dev/sdk@3.0.0-beta.23
|
||||
|
||||
## 3.0.0-beta.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.22
|
||||
- @trigger.dev/sdk@3.0.0-beta.22
|
||||
|
||||
## 3.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9491a1649]
|
||||
- @trigger.dev/sdk@3.0.0-beta.21
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.21
|
||||
|
||||
## 3.0.0-beta.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.20
|
||||
- @trigger.dev/sdk@3.0.0-beta.20
|
||||
|
||||
## 3.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e9a63a486]
|
||||
- @trigger.dev/sdk@3.0.0-beta.19
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.19
|
||||
|
||||
## 3.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [b68012f81]
|
||||
- Updated dependencies [b68012f81]
|
||||
- Updated dependencies [b68012f81]
|
||||
- @trigger.dev/sdk@3.0.0-beta.18
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.18
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/airtable",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.0-beta.23",
|
||||
"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.15",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.15",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
|
||||
"airtable": "^0.12.1",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
# @trigger.dev/github
|
||||
|
||||
## 3.0.0-beta.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.23
|
||||
- @trigger.dev/sdk@3.0.0-beta.23
|
||||
|
||||
## 3.0.0-beta.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.22
|
||||
- @trigger.dev/sdk@3.0.0-beta.22
|
||||
|
||||
## 3.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9491a1649]
|
||||
- @trigger.dev/sdk@3.0.0-beta.21
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.21
|
||||
|
||||
## 3.0.0-beta.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.20
|
||||
- @trigger.dev/sdk@3.0.0-beta.20
|
||||
|
||||
## 3.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e9a63a486]
|
||||
- @trigger.dev/sdk@3.0.0-beta.19
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.19
|
||||
|
||||
## 3.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [b68012f81]
|
||||
- Updated dependencies [b68012f81]
|
||||
- Updated dependencies [b68012f81]
|
||||
- @trigger.dev/sdk@3.0.0-beta.18
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.18
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@trigger.dev/github",
|
||||
"version": "3.0.0-beta.15",
|
||||
"version": "3.0.0-beta.23",
|
||||
"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.15",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.15",
|
||||
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.23",
|
||||
"@trigger.dev/sdk": "workspace:^3.0.0-beta.23",
|
||||
"zod": "3.22.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,66 @@
|
||||
# @trigger.dev/linear
|
||||
|
||||
## 3.0.0-beta.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.23
|
||||
- @trigger.dev/sdk@3.0.0-beta.23
|
||||
|
||||
## 3.0.0-beta.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.22
|
||||
- @trigger.dev/sdk@3.0.0-beta.22
|
||||
|
||||
## 3.0.0-beta.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9491a1649]
|
||||
- @trigger.dev/sdk@3.0.0-beta.21
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.21
|
||||
|
||||
## 3.0.0-beta.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.20
|
||||
- @trigger.dev/sdk@3.0.0-beta.20
|
||||
|
||||
## 3.0.0-beta.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e9a63a486]
|
||||
- @trigger.dev/sdk@3.0.0-beta.19
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.19
|
||||
|
||||
## 3.0.0-beta.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [b68012f81]
|
||||
- Updated dependencies [b68012f81]
|
||||
- Updated dependencies [b68012f81]
|
||||
- @trigger.dev/sdk@3.0.0-beta.18
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.18
|
||||
|
||||
## 3.0.0-beta.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.17
|
||||
- @trigger.dev/sdk@3.0.0-beta.17
|
||||
|
||||
## 3.0.0-beta.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @trigger.dev/integration-kit@3.0.0-beta.16
|
||||
- @trigger.dev/sdk@3.0.0-beta.16
|
||||
|
||||
## 3.0.0-beta.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user