From 4f6a68f9e7d6f2f2ac97395c9ffd3e98942c3d64 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sat, 8 Mar 2025 15:00:52 +0000 Subject: [PATCH 01/95] Delete the proxy app (was v2) --- apps/proxy/.dev.vars.example | 7 -- apps/proxy/.editorconfig | 13 -- apps/proxy/.gitignore | 172 --------------------------- apps/proxy/.prettierrc | 11 -- apps/proxy/CHANGELOG.md | 72 ----------- apps/proxy/README.md | 68 ----------- apps/proxy/package.json | 21 ---- apps/proxy/src/apikey.ts | 20 ---- apps/proxy/src/events/queueEvent.ts | 87 -------------- apps/proxy/src/events/queueEvents.ts | 112 ----------------- apps/proxy/src/events/utils.ts | 15 --- apps/proxy/src/index.ts | 53 --------- apps/proxy/src/json.ts | 13 -- apps/proxy/src/rateLimit.ts | 46 ------- apps/proxy/src/rateLimiter.ts | 23 ---- apps/proxy/tsconfig.json | 35 ------ apps/proxy/wrangler.toml | 33 ----- 17 files changed, 801 deletions(-) delete mode 100644 apps/proxy/.dev.vars.example delete mode 100644 apps/proxy/.editorconfig delete mode 100644 apps/proxy/.gitignore delete mode 100644 apps/proxy/.prettierrc delete mode 100644 apps/proxy/CHANGELOG.md delete mode 100644 apps/proxy/README.md delete mode 100644 apps/proxy/package.json delete mode 100644 apps/proxy/src/apikey.ts delete mode 100644 apps/proxy/src/events/queueEvent.ts delete mode 100644 apps/proxy/src/events/queueEvents.ts delete mode 100644 apps/proxy/src/events/utils.ts delete mode 100644 apps/proxy/src/index.ts delete mode 100644 apps/proxy/src/json.ts delete mode 100644 apps/proxy/src/rateLimit.ts delete mode 100644 apps/proxy/src/rateLimiter.ts delete mode 100644 apps/proxy/tsconfig.json delete mode 100644 apps/proxy/wrangler.toml diff --git a/apps/proxy/.dev.vars.example b/apps/proxy/.dev.vars.example deleted file mode 100644 index 76de44a191..0000000000 --- a/apps/proxy/.dev.vars.example +++ /dev/null @@ -1,7 +0,0 @@ -REWRITE_HOSTNAME= -AWS_SQS_ACCESS_KEY_ID= -AWS_SQS_SECRET_ACCESS_KEY= -AWS_SQS_QUEUE_URL= -AWS_SQS_REGION= -#optional -#REWRITE_PORT= \ No newline at end of file diff --git a/apps/proxy/.editorconfig b/apps/proxy/.editorconfig deleted file mode 100644 index 64ab2601f9..0000000000 --- a/apps/proxy/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = tab -tab_width = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.yml] -indent_style = space diff --git a/apps/proxy/.gitignore b/apps/proxy/.gitignore deleted file mode 100644 index 3b0fe33c47..0000000000 --- a/apps/proxy/.gitignore +++ /dev/null @@ -1,172 +0,0 @@ -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -\*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -\*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) - -web_modules/ - -# TypeScript cache - -\*.tsbuildinfo - -# Optional npm cache directory - -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -\*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.cache -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -.cache/ - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp -.cache - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VSCode versions used for testing VSCode extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.\* - -# wrangler project - -.dev.vars -.wrangler/ diff --git a/apps/proxy/.prettierrc b/apps/proxy/.prettierrc deleted file mode 100644 index 89c93d85a8..0000000000 --- a/apps/proxy/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "semi": true, - "singleQuote": false, - "jsxSingleQuote": false, - "trailingComma": "es5", - "bracketSpacing": true, - "bracketSameLine": false, - "printWidth": 100, - "tabWidth": 2, - "useTabs": false -} diff --git a/apps/proxy/CHANGELOG.md b/apps/proxy/CHANGELOG.md deleted file mode 100644 index 6544c5fee1..0000000000 --- a/apps/proxy/CHANGELOG.md +++ /dev/null @@ -1,72 +0,0 @@ -# proxy - -## 0.0.11 - -### Patch Changes - -- @trigger.dev/core@2.3.5 - -## 0.0.10 - -### Patch Changes - -- @trigger.dev/core@2.3.4 - -## 0.0.9 - -### Patch Changes - -- @trigger.dev/core@2.3.3 - -## 0.0.8 - -### Patch Changes - -- @trigger.dev/core@2.3.2 - -## 0.0.7 - -### Patch Changes - -- Updated dependencies [f3efcc0c] - - @trigger.dev/core@2.3.1 - -## 0.0.6 - -### Patch Changes - -- Updated dependencies [17f6f29d] - - @trigger.dev/core@2.3.0 - -## 0.0.5 - -### Patch Changes - -- @trigger.dev/core@2.2.11 - -## 0.0.4 - -### Patch Changes - -- @trigger.dev/core@2.2.10 - -## 0.0.3 - -### Patch Changes - -- Updated dependencies [6ebd435e] - - @trigger.dev/core@2.2.9 - -## 0.0.2 - -### Patch Changes - -- Updated dependencies [067e19fe] - - @trigger.dev/core@2.2.8 - -## 0.0.1 - -### Patch Changes - -- Updated dependencies [756024da] - - @trigger.dev/core@2.2.7 diff --git a/apps/proxy/README.md b/apps/proxy/README.md deleted file mode 100644 index f3010f2af7..0000000000 --- a/apps/proxy/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Trigger.dev proxy - -This is an optional module that can be used to proxy and queue requests to the Trigger.dev API. - -## Why? - -The Trigger.dev API is designed to be fast and reliable. However, if you have a lot of traffic, you may want to use this proxy to queue requests to the API. It intercepts some requests to the API and adds them to an AWS SQS queue, then the webapp can be setup to process the queue. - -## Current features - -- Intercepts `sendEvent` requests and adds them to an AWS SQS queue. The webapp then reads from the queue and creates the events. - -## Setup - -### Create an AWS SQS queue - -In AWS you should create a new AWS SQS queue with appropriate security settings. You will need the queue URL for the next step. - -### Environment variables - -#### Cloudflare secrets - -Locally you should copy the `.dev.var.example` file to `.dev.var` and fill in the values. - -When deploying you should use `wrangler` (the Cloudflare CLI tool) to set secrets. Make sure you set the correct --env ("staging" or "prod") - -```bash -wrangler secret put REWRITE_HOSTNAME --env staging -wrangler secret put AWS_SQS_ACCESS_KEY_ID --env staging -wrangler secret put AWS_SQS_SECRET_ACCESS_KEY --env staging -wrangler secret put AWS_SQS_QUEUE_URL --env staging -wrangler secret put AWS_SQS_REGION --env staging -``` - -You need to set your API CNAME entry to be proxied by Cloudflare. You can do this in the Cloudflare dashboard. - -#### Webapp - -These env vars also need setting in the webapp. - -```bash -AWS_SQS_REGION -AWS_SQS_ACCESS_KEY_ID -AWS_SQS_SECRET_ACCESS_KEY -AWS_SQS_QUEUE_URL -AWS_SQS_BATCH_SIZE -``` - -## Deployment - -Staging: - -```bash -npx wrangler@latest deploy --route "/*" --env staging -``` - -Prod: - -```bash -npx wrangler@latest deploy --route "/*" --env prod -``` - -## Development - -Set the environment variables as described above. - -1. `pnpm install` -2. `pnpm run dev --filter proxy` diff --git a/apps/proxy/package.json b/apps/proxy/package.json deleted file mode 100644 index 80646e60a0..0000000000 --- a/apps/proxy/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "proxy", - "version": "0.0.11", - "private": true, - "scripts": { - "deploy": "wrangler deploy", - "dev": "wrangler dev", - "dry-run:staging": "wrangler deploy --dry-run --outdir=dist --env staging" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20240512.0", - "wrangler": "^3.57.1" - }, - "dependencies": { - "@aws-sdk/client-sqs": "^3.445.0", - "@trigger.dev/core": "workspace:*", - "ulidx": "^2.2.1", - "zod": "3.23.8", - "zod-error": "1.5.0" - } -} \ No newline at end of file diff --git a/apps/proxy/src/apikey.ts b/apps/proxy/src/apikey.ts deleted file mode 100644 index cb6c9c2344..0000000000 --- a/apps/proxy/src/apikey.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from "zod"; - -const AuthorizationHeaderSchema = z.string().regex(/^Bearer .+$/); - -export function getApiKeyFromRequest(request: Request) { - const rawAuthorization = request.headers.get("Authorization"); - - const authorization = AuthorizationHeaderSchema.safeParse(rawAuthorization); - if (!authorization.success) { - return; - } - - const apiKey = authorization.data.replace(/^Bearer /, ""); - const type = isPrivateApiKey(apiKey) ? ("PRIVATE" as const) : ("PUBLIC" as const); - return { apiKey, type }; -} - -function isPrivateApiKey(key: string) { - return key.startsWith("tr_"); -} diff --git a/apps/proxy/src/events/queueEvent.ts b/apps/proxy/src/events/queueEvent.ts deleted file mode 100644 index d3b2dcce54..0000000000 --- a/apps/proxy/src/events/queueEvent.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; -import { ApiEventLog, SendEventBodySchema } from "@trigger.dev/core"; -import { generateErrorMessage } from "zod-error"; -import { Env } from ".."; -import { getApiKeyFromRequest } from "../apikey"; -import { json } from "../json"; -import { calculateDeliverAt } from "./utils"; - -/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */ -export async function queueEvent(request: Request, env: Env): Promise { - //check there's a private API key - const apiKeyResult = getApiKeyFromRequest(request); - if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") { - return json( - { error: "Invalid or Missing API key" }, - { - status: 401, - } - ); - } - - //parse the request body - try { - const anyBody = await request.json(); - const body = SendEventBodySchema.safeParse(anyBody); - if (!body.success) { - return json( - { error: generateErrorMessage(body.error.issues) }, - { - status: 422, - } - ); - } - - // The AWS SDK tries to use crypto from off of the window, - // so we need to trick it into finding it where it expects it - globalThis.global = globalThis; - - const client = new SQSClient({ - region: env.AWS_SQS_REGION, - credentials: { - accessKeyId: env.AWS_SQS_ACCESS_KEY_ID, - secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY, - }, - }); - - const timestamp = body.data.event.timestamp ?? new Date(); - - //add the event to the queue - const send = new SendMessageCommand({ - // use wrangler secrets to provide this global variable - QueueUrl: env.AWS_SQS_QUEUE_URL, - MessageBody: JSON.stringify({ - event: { ...body.data.event, timestamp }, - options: body.data.options, - apiKey: apiKeyResult.apiKey, - }), - }); - - const queuedEvent = await client.send(send); - console.log("Queued event", queuedEvent); - - //respond with the event - const event: ApiEventLog = { - id: body.data.event.id, - name: body.data.event.name, - payload: body.data.event.payload, - context: body.data.event.context, - timestamp, - deliverAt: calculateDeliverAt(body.data.options), - }; - - return json(event, { - status: 200, - }); - } catch (e) { - console.error("queueEvent error", e); - return json( - { - error: `Failed to send event: ${e instanceof Error ? e.message : JSON.stringify(e)}`, - }, - { - status: 422, - } - ); - } -} diff --git a/apps/proxy/src/events/queueEvents.ts b/apps/proxy/src/events/queueEvents.ts deleted file mode 100644 index 412db29acd..0000000000 --- a/apps/proxy/src/events/queueEvents.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { SQSClient, SendMessageBatchCommand } from "@aws-sdk/client-sqs"; -import { ApiEventLog, SendBulkEventsBodySchema } from "@trigger.dev/core"; -import { generateErrorMessage } from "zod-error"; -import { Env } from ".."; -import { getApiKeyFromRequest } from "../apikey"; -import { json } from "../json"; -import { calculateDeliverAt } from "./utils"; - -/** Adds the event to an AWS SQS queue, so it can be consumed from the main Trigger.dev API */ -export async function queueEvents(request: Request, env: Env): Promise { - //check there's a private API key - const apiKeyResult = getApiKeyFromRequest(request); - if (!apiKeyResult || apiKeyResult.type !== "PRIVATE") { - return json( - { error: "Invalid or Missing API key" }, - { - status: 401, - } - ); - } - - //parse the request body - try { - const anyBody = await request.json(); - const body = SendBulkEventsBodySchema.safeParse(anyBody); - if (!body.success) { - return json( - { error: generateErrorMessage(body.error.issues) }, - { - status: 422, - } - ); - } - - // The AWS SDK tries to use crypto from off of the window, - // so we need to trick it into finding it where it expects it - globalThis.global = globalThis; - - const client = new SQSClient({ - region: env.AWS_SQS_REGION, - credentials: { - accessKeyId: env.AWS_SQS_ACCESS_KEY_ID, - secretAccessKey: env.AWS_SQS_SECRET_ACCESS_KEY, - }, - }); - - const updatedEvents: ApiEventLog[] = body.data.events.map((event) => { - const timestamp = event.timestamp ?? new Date(); - return { - ...event, - payload: event.payload, - timestamp, - }; - }); - - //divide updatedEvents into multiple batches of 10 (max size SQS accepts) - const batches: ApiEventLog[][] = []; - let currentBatch: ApiEventLog[] = []; - for (let i = 0; i < updatedEvents.length; i++) { - currentBatch.push(updatedEvents[i]); - if (currentBatch.length === 10) { - batches.push(currentBatch); - currentBatch = []; - } - } - if (currentBatch.length > 0) { - batches.push(currentBatch); - } - - //loop through the batches and send them - for (let i = 0; i < batches.length; i++) { - const batch = batches[i]; - //add the event to the queue - const send = new SendMessageBatchCommand({ - // use wrangler secrets to provide this global variable - QueueUrl: env.AWS_SQS_QUEUE_URL, - Entries: batch.map((event, index) => ({ - Id: `event-${index}`, - MessageBody: JSON.stringify({ - event, - options: body.data.options, - apiKey: apiKeyResult.apiKey, - }), - })), - }); - - const queuedEvent = await client.send(send); - console.log("Queued events", queuedEvent); - } - - //respond with the events - const events: ApiEventLog[] = updatedEvents.map((event) => ({ - ...event, - payload: event.payload, - deliverAt: calculateDeliverAt(body.data.options), - })); - - return json(events, { - status: 200, - }); - } catch (e) { - console.error("queueEvents error", e); - return json( - { - error: `Failed to send events: ${e instanceof Error ? e.message : JSON.stringify(e)}`, - }, - { - status: 422, - } - ); - } -} diff --git a/apps/proxy/src/events/utils.ts b/apps/proxy/src/events/utils.ts deleted file mode 100644 index e68643d88e..0000000000 --- a/apps/proxy/src/events/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SendEventOptions } from "@trigger.dev/core"; - -export function calculateDeliverAt(options?: SendEventOptions) { - // If deliverAt is a string and a valid date, convert it to a Date object - if (options?.deliverAt) { - return options?.deliverAt; - } - - // deliverAfter is the number of seconds to wait before delivering the event - if (options?.deliverAfter) { - return new Date(Date.now() + options.deliverAfter * 1000); - } - - return undefined; -} diff --git a/apps/proxy/src/index.ts b/apps/proxy/src/index.ts deleted file mode 100644 index 26d7d3b00d..0000000000 --- a/apps/proxy/src/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { queueEvent } from "./events/queueEvent"; -import { queueEvents } from "./events/queueEvents"; -import { applyRateLimit } from "./rateLimit"; -import { Ratelimit } from "./rateLimiter"; - -export interface Env { - /** The hostname needs to be changed to allow requests to pass to the Trigger.dev platform */ - REWRITE_HOSTNAME: string; - REWRITE_PORT?: string; - AWS_SQS_ACCESS_KEY_ID: string; - AWS_SQS_SECRET_ACCESS_KEY: string; - AWS_SQS_QUEUE_URL: string; - AWS_SQS_REGION: string; - //rate limiter - API_RATE_LIMITER: Ratelimit; -} - -export default { - async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { - if (!queueingIsEnabled(env)) { - console.log("Missing AWS credentials. Passing through to the origin."); - return fetch(request); - } - - const url = new URL(request.url); - switch (url.pathname) { - case "/api/v1/events": { - if (request.method === "POST") { - return applyRateLimit(request, env, () => queueEvent(request, env)); - } - break; - } - case "/api/v1/events/bulk": { - if (request.method === "POST") { - return applyRateLimit(request, env, () => queueEvents(request, env)); - } - break; - } - } - - //the same request but with the hostname (and port) changed - return fetch(request); - }, -}; - -function queueingIsEnabled(env: Env) { - return ( - env.AWS_SQS_ACCESS_KEY_ID && - env.AWS_SQS_SECRET_ACCESS_KEY && - env.AWS_SQS_QUEUE_URL && - env.AWS_SQS_REGION - ); -} diff --git a/apps/proxy/src/json.ts b/apps/proxy/src/json.ts deleted file mode 100644 index c8c2aca7bf..0000000000 --- a/apps/proxy/src/json.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function json(body: any, init?: ResponseInit) { - const headers = { - "content-type": "application/json", - ...(init?.headers ?? {}), - }; - - const responseInit: ResponseInit = { - ...(init ?? {}), - headers, - }; - - return new Response(JSON.stringify(body), responseInit); -} diff --git a/apps/proxy/src/rateLimit.ts b/apps/proxy/src/rateLimit.ts deleted file mode 100644 index ccbd7b4338..0000000000 --- a/apps/proxy/src/rateLimit.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Env } from "src"; -import { getApiKeyFromRequest } from "./apikey"; -import { json } from "./json"; - -export async function applyRateLimit( - request: Request, - env: Env, - fn: () => Promise -): Promise { - const apiKey = getApiKeyFromRequest(request); - if (apiKey) { - const result = await env.API_RATE_LIMITER.limit({ key: `apikey-${apiKey.apiKey}` }); - const { success } = result; - console.log(`Rate limiter`, { - success, - key: `${apiKey.apiKey.substring(0, 12)}...`, - }); - if (!success) { - //60s in the future - const reset = Date.now() + 60 * 1000; - const secondsUntilReset = Math.max(0, (reset - new Date().getTime()) / 1000); - - return json( - { - title: "Rate Limit Exceeded", - status: 429, - type: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429", - detail: `Rate limit exceeded. Retry in ${secondsUntilReset} seconds.`, - error: `Rate limit exceeded. Retry in ${secondsUntilReset} seconds.`, - reset, - }, - { - status: 429, - headers: { - "x-ratelimit-reset": reset.toString(), - }, - } - ); - } - } else { - console.log(`Rate limiter: no API key for request`); - } - - //call the original function - return fn(); -} diff --git a/apps/proxy/src/rateLimiter.ts b/apps/proxy/src/rateLimiter.ts deleted file mode 100644 index 4143323431..0000000000 --- a/apps/proxy/src/rateLimiter.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface Ratelimit { - /* - * The ratelimit function - * @param {RatelimitOptions} options - * @returns {Promise} - */ - limit: (options: RatelimitOptions) => Promise; -} - -export interface RatelimitOptions { - /* - * The key to identify the user, can be an IP address, user ID, etc. - */ - key: string; -} - -export interface RatelimitResponse { - /* - * The ratelimit success status - * @returns {boolean} - */ - success: boolean; -} diff --git a/apps/proxy/tsconfig.json b/apps/proxy/tsconfig.json deleted file mode 100644 index b35efe3073..0000000000 --- a/apps/proxy/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "compilerOptions": { - "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": [ - "es2021" - ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, - "jsx": "react" /* Specify what JSX code is generated. */, - - "module": "es2022" /* Specify what module code is generated. */, - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - - "types": [ - "@cloudflare/workers-types" - ] /* Specify type package names to be included without being referenced in a source file. */, - "resolveJsonModule": true /* Enable importing .json files */, - - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, - "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, - - "noEmit": true /* Disable emitting files from a compilation. */, - - "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, - "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - "strict": true /* Enable all strict type-checking options. */, - - "skipLibCheck": true /* Skip type checking all .d.ts files. */, - "baseUrl": ".", - "paths": { - "@trigger.dev/core": ["../../packages/core/src/index"], - "@trigger.dev/core/*": ["../../packages/core/src/*"] - } - } -} diff --git a/apps/proxy/wrangler.toml b/apps/proxy/wrangler.toml deleted file mode 100644 index 3cbfb66cd8..0000000000 --- a/apps/proxy/wrangler.toml +++ /dev/null @@ -1,33 +0,0 @@ -name = "proxy" -main = "src/index.ts" -compatibility_date = "2024-05-13" -compatibility_flags = [ "nodejs_compat" ] - -[env.staging] - # The rate limiting API is in open beta. - [[env.staging.unsafe.bindings]] - name = "API_RATE_LIMITER" - type = "ratelimit" - # An identifier you define, that is unique to your Cloudflare account. - # Must be an integer. - namespace_id = "1" - - # Limit: the number of tokens allowed within a given period in a single - # Cloudflare location - # Period: the duration of the period, in seconds. Must be either 10 or 60 - simple = { limit = 100, period = 60 } - - -[env.prod] - # The rate limiting API is in open beta. - [[env.prod.unsafe.bindings]] - name = "API_RATE_LIMITER" - type = "ratelimit" - # An identifier you define, that is unique to your Cloudflare account. - # Must be an integer. - namespace_id = "2" - - # Limit: the number of tokens allowed within a given period in a single - # Cloudflare location - # Period: the duration of the period, in seconds. Must be either 10 or 60 - simple = { limit = 300, period = 60 } \ No newline at end of file From 327211be17c84334a2567984719af0a1694d93fb Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sat, 8 Mar 2025 15:02:12 +0000 Subject: [PATCH 02/95] Delete RunPresenterElectric --- .../v3/RunPresenterElectric.server.ts | 87 ------------------- 1 file changed, 87 deletions(-) delete mode 100644 apps/webapp/app/presenters/v3/RunPresenterElectric.server.ts diff --git a/apps/webapp/app/presenters/v3/RunPresenterElectric.server.ts b/apps/webapp/app/presenters/v3/RunPresenterElectric.server.ts deleted file mode 100644 index 7e1e3ef1b8..0000000000 --- a/apps/webapp/app/presenters/v3/RunPresenterElectric.server.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { PrismaClient, prisma } from "~/db.server"; -import { getUsername } from "~/utils/username"; -import { isFinalRunStatus } from "~/v3/taskStatus"; - -type Result = Awaited>; -export type Run = Result["run"]; - -export class RunPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ - userId, - projectSlug, - organizationSlug, - runFriendlyId, - }: { - userId: string; - projectSlug: string; - organizationSlug: string; - runFriendlyId: string; - }) { - const run = await this.#prismaClient.taskRun.findFirstOrThrow({ - select: { - id: true, - number: true, - traceId: true, - spanId: true, - friendlyId: true, - status: true, - completedAt: true, - logsDeletedAt: true, - runtimeEnvironment: { - select: { - id: true, - type: true, - slug: true, - organizationId: true, - orgMember: { - select: { - user: { - select: { - id: true, - name: true, - displayName: true, - }, - }, - }, - }, - }, - }, - }, - where: { - friendlyId: runFriendlyId, - project: { - slug: projectSlug, - }, - }, - }); - - return { - run: { - id: run.id, - number: run.number, - friendlyId: run.friendlyId, - traceId: run.traceId, - spanId: run.spanId, - status: run.status, - isFinished: isFinalRunStatus(run.status), - completedAt: run.completedAt, - logsDeletedAt: run.logsDeletedAt, - - environment: { - id: run.runtimeEnvironment.id, - organizationId: run.runtimeEnvironment.organizationId, - type: run.runtimeEnvironment.type, - slug: run.runtimeEnvironment.slug, - userId: run.runtimeEnvironment.orgMember?.user.id, - userName: getUsername(run.runtimeEnvironment.orgMember?.user), - }, - }, - }; - } -} From 9d6b44b16ffef04fd5827b7c352908b1344f9265 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sat, 8 Mar 2025 15:42:32 +0000 Subject: [PATCH 03/95] Select the best proj/org/env --- .../SelectBestProjectPresenter.server.ts | 77 ++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts index 32f666b077..2469baa9d8 100644 --- a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts +++ b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts @@ -1,5 +1,6 @@ -import { PrismaClient } from "@trigger.dev/database"; +import { type PrismaClient } from "@trigger.dev/database"; import { prisma } from "~/db.server"; +import { getCurrentEnvironmentType } from "~/services/currentEnvironmentType.server"; import { getCurrentProjectId } from "~/services/currentProject.server"; export class SelectBestProjectPresenter { @@ -10,12 +11,84 @@ export class SelectBestProjectPresenter { } public async call({ userId, request }: { userId: string; request: Request }) { + const { project, organization } = await this.#getBestProject(request, userId); + + //try get current environment from cookie + const environmentType = await getCurrentEnvironmentType(request); + + const environments = await this.#prismaClient.runtimeEnvironment.findMany({ + select: { + slug: true, + type: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + where: { + projectId: project.id, + }, + }); + + const relevantEnvironments = environments.filter((env) => env.type === environmentType); + + if (relevantEnvironments.length === 0) { + const yourDevEnvironment = environments.find( + (env) => env.type === "DEVELOPMENT" && env.orgMember?.userId === userId + ); + if (yourDevEnvironment) { + return { project, organization, environment: yourDevEnvironment }; + } + + const prodEnvironment = environments.find((env) => env.type === "PRODUCTION"); + if (prodEnvironment) { + return { project, organization, environment: prodEnvironment }; + } + + throw new Error("No environments found"); + } + + if (relevantEnvironments.length === 1) { + return { project, organization, environment: environments[0] }; + } + + const yourDevEnvironment = environments.find( + (env) => env.type === "DEVELOPMENT" && env.orgMember?.userId === userId + ); + if (yourDevEnvironment) { + return { project, organization, environment: yourDevEnvironment }; + } + + const prodEnvironment = environments.find((env) => env.type === "PRODUCTION"); + if (prodEnvironment) { + return { project, organization, environment: prodEnvironment }; + } + + throw new Error("No environments found"); + } + + async #getBestProject(request: Request, userId: string) { //try get current project from cookie const projectId = await getCurrentProjectId(request); + if (projectId) { const project = await this.#prismaClient.project.findUnique({ where: { id: projectId, deletedAt: null, organization: { members: { some: { userId } } } }, - include: { organization: true }, + include: { + organization: true, + environments: { + select: { + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, }); if (project) { return { project, organization: project.organization }; From d99df7a801e706de39794237ed9e9ec04c97cfa0 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 10 Mar 2025 19:04:14 +0000 Subject: [PATCH 04/95] Storing current proj/env in DB. Initial selection logic working with tasks page --- .../app/assets/icons/EnvironmentIcons.tsx | 92 +++++ apps/webapp/app/components/ErrorDisplay.tsx | 2 +- .../environments/EnvironmentLabel.tsx | 57 ++++ .../app/components/navigation/SideMenu.tsx | 237 ++++++++----- .../app/components/primitives/Popover.tsx | 2 +- .../app/components/runs/v3/TaskRunStatus.tsx | 2 +- apps/webapp/app/hooks/useEnvironment.tsx | 23 ++ apps/webapp/app/hooks/useFeatures.ts | 2 +- apps/webapp/app/hooks/useProject.tsx | 4 +- apps/webapp/app/models/user.server.ts | 21 +- .../OrganizationsPresenter.server.ts | 318 +++++++++--------- .../SelectBestEnvironmentPresenter.server.ts | 127 +++++++ .../SelectBestProjectPresenter.server.ts | 121 ------- .../presenters/v3/TaskListPresenter.server.ts | 212 +++++------- apps/webapp/app/routes/_app._index/route.tsx | 13 +- .../route.tsx | 3 +- .../route.tsx | 57 ++-- .../route.tsx | 79 +++++ .../route.tsx | 29 ++ .../route.tsx | 6 + .../route.tsx | 69 ---- .../route.tsx | 25 +- .../_app.orgs.$organizationSlug/route.tsx | 25 +- apps/webapp/app/routes/logout.tsx | 28 +- apps/webapp/app/routes/projects.new.ts | 8 +- .../app/services/currentProject.server.ts | 39 --- .../services/dashboardPreferences.server.ts | 101 ++++++ .../app/services/impersonation.server.ts | 3 +- apps/webapp/app/services/session.server.ts | 19 +- apps/webapp/app/utils/environmentSort.ts | 2 +- apps/webapp/app/utils/pathBuilder.ts | 35 +- .../migration.sql | 2 + .../database/prisma/schema.prisma | 14 +- pnpm-lock.yaml | 197 +++-------- 34 files changed, 1092 insertions(+), 882 deletions(-) create mode 100644 apps/webapp/app/assets/icons/EnvironmentIcons.tsx create mode 100644 apps/webapp/app/hooks/useEnvironment.tsx create mode 100644 apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts delete mode 100644 apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam._index => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index}/route.tsx (93%) create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.projectParam._index/route.tsx delete mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam/route.tsx delete mode 100644 apps/webapp/app/services/currentProject.server.ts create mode 100644 apps/webapp/app/services/dashboardPreferences.server.ts create mode 100644 internal-packages/database/prisma/migrations/20250310132020_added_dashboard_preferences_to_user/migration.sql diff --git a/apps/webapp/app/assets/icons/EnvironmentIcons.tsx b/apps/webapp/app/assets/icons/EnvironmentIcons.tsx new file mode 100644 index 0000000000..d8d8a7b66b --- /dev/null +++ b/apps/webapp/app/assets/icons/EnvironmentIcons.tsx @@ -0,0 +1,92 @@ +export function DevEnvironmentIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + ); +} + +export function ProdEnvironmentIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + ); +} + +export function DeployedEnvironmentIcon({ className }: { className?: string }) { + return ( + + + + + ); +} diff --git a/apps/webapp/app/components/ErrorDisplay.tsx b/apps/webapp/app/components/ErrorDisplay.tsx index c6544f6496..1a8f4b2ad9 100644 --- a/apps/webapp/app/components/ErrorDisplay.tsx +++ b/apps/webapp/app/components/ErrorDisplay.tsx @@ -6,7 +6,7 @@ import { LinkButton } from "./primitives/Buttons"; import { Header1 } from "./primitives/Headers"; import { Paragraph } from "./primitives/Paragraph"; import Spline from "@splinetool/react-spline"; -import { ReactNode } from "react"; +import { type ReactNode } from "react"; type ErrorDisplayOptions = { button?: { diff --git a/apps/webapp/app/components/environments/EnvironmentLabel.tsx b/apps/webapp/app/components/environments/EnvironmentLabel.tsx index 2b7a3d28c0..f5e6c8f376 100644 --- a/apps/webapp/app/components/environments/EnvironmentLabel.tsx +++ b/apps/webapp/app/components/environments/EnvironmentLabel.tsx @@ -2,6 +2,11 @@ import type { RuntimeEnvironment } from "~/models/runtimeEnvironment.server"; import { cn } from "~/utils/cn"; import { sortEnvironments } from "~/utils/environmentSort"; import { SimpleTooltip } from "../primitives/Tooltip"; +import { + DeployedEnvironmentIcon, + DevEnvironmentIcon, + ProdEnvironmentIcon, +} from "~/assets/icons/EnvironmentIcons"; type Environment = Pick; const variants = { @@ -127,6 +132,45 @@ export function EnvironmentLabels({ ); } +export function EnvironmentIcon({ + environment, + className, +}: { + environment: Environment; + className?: string; +}) { + switch (environment.type) { + case "DEVELOPMENT": + return ( + + ); + case "PRODUCTION": + return ( + + ); + case "STAGING": + case "PREVIEW": + return ( + + ); + } +} + +export function FullEnvironmentCombo({ + environment, + className, +}: { + environment: Environment; + className?: string; +}) { + return ( +
+ +
{environmentFullTitle(environment)}
+
+ ); +} + export function environmentTitle(environment: Environment, username?: string) { switch (environment.type) { case "PRODUCTION": @@ -140,6 +184,19 @@ export function environmentTitle(environment: Environment, username?: string) { } } +export function environmentFullTitle(environment: Environment) { + switch (environment.type) { + case "PRODUCTION": + return "Production"; + case "STAGING": + return "Staging"; + case "DEVELOPMENT": + return "Development"; + case "PREVIEW": + return "Preview"; + } +} + export function environmentTypeTitle(environment: Environment) { switch (environment.type) { case "PRODUCTION": diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index e8ae3bf92d..ea0e6d5acd 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -18,7 +18,7 @@ import { } from "@heroicons/react/20/solid"; import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid"; import { useNavigation } from "@remix-run/react"; -import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; +import { Fragment, type ReactNode, useEffect, useRef, useState } from "react"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { useFeatures } from "~/hooks/useFeatures"; @@ -26,7 +26,7 @@ import { type MatchedOrganization } from "~/hooks/useOrganizations"; import { type MatchedProject } from "~/hooks/useProject"; import { type User } from "~/models/user.server"; import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; -import { FeedbackType } from "~/routes/resources.feedback"; +import { type FeedbackType } from "~/routes/resources.feedback"; import { cn } from "~/utils/cn"; import { accountPath, @@ -43,6 +43,7 @@ import { v3BillingPath, v3ConcurrencyPath, v3DeploymentsPath, + v3EnvironmentPath, v3EnvironmentVariablesPath, v3ProjectAlertsPath, v3ProjectPath, @@ -67,27 +68,39 @@ import { import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; +import { type MatchedEnvironment } from "~/hooks/useEnvironment"; +import { Paragraph } from "../primitives/Paragraph"; +import { + environmentFullTitle, + EnvironmentIcon, + FullEnvironmentCombo, +} from "../environments/EnvironmentLabel"; type SideMenuUser = Pick & { isImpersonating: boolean }; -type SideMenuProject = Pick; +type SideMenuProject = Pick; +type SideMenuEnvironment = MatchedEnvironment; type SideMenuProps = { user: SideMenuUser; project: SideMenuProject; + environment: SideMenuEnvironment; organization: MatchedOrganization; organizations: MatchedOrganization[]; button?: ReactNode; defaultValue?: FeedbackType; }; -export function SideMenu({ user, project, organization, organizations }: SideMenuProps) { +export function SideMenu({ + user, + project, + environment, + organization, + organizations, +}: SideMenuProps) { const borderRef = useRef(null); const [showHeaderDivider, setShowHeaderDivider] = useState(false); const currentPlan = useCurrentPlan(); - const { isManagedCloud } = useFeatures(); - - const isV3Project = project.version === "V3"; - const isFreeV3User = currentPlan?.v3Subscription?.isPaying === false; + const isFreeUser = currentPlan?.v3Subscription?.isPaying === false; useEffect(() => { const handleScroll = () => { @@ -124,7 +137,93 @@ export function SideMenu({ user, project, organization, organizations }: SideMen ref={borderRef} >
- + + + + <> + + + + + + + + + + + + + +
@@ -183,7 +282,7 @@ export function SideMenu({ user, project, organization, organizations }: SideMen
- {isFreeV3User && ( + {isFreeUser && ( - - - - - - - - + const [isMenuOpen, setIsMenuOpen] = useState(false); + const navigation = useNavigation(); + + useEffect(() => { + setIsMenuOpen(false); + }, [navigation.location?.pathname]); - - - - - + return ( + setIsMenuOpen(open)} open={isMenuOpen}> + + + + + {project.environments.map((env) => ( + } + isSelected={env.id === environment.id} + /> + ))} + + ); } diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 3a05575d4f..3be75e52c2 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -59,7 +59,7 @@ function PopoverMenuItem({ leadingIconClassName, }: { to: string; - icon: React.ComponentType; + icon?: React.ComponentType; title: React.ReactNode; isSelected?: boolean; variant?: ButtonContentPropsType; diff --git a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx index 29fa33472e..325d20d9e9 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunStatus.tsx @@ -11,7 +11,7 @@ import { TrashIcon, XCircleIcon, } from "@heroicons/react/20/solid"; -import { TaskRunStatus } from "@trigger.dev/database"; +import { type TaskRunStatus } from "@trigger.dev/database"; import assertNever from "assert-never"; import { HourglassIcon } from "lucide-react"; import { TimedOutIcon } from "~/assets/icons/TimedOutIcon"; diff --git a/apps/webapp/app/hooks/useEnvironment.tsx b/apps/webapp/app/hooks/useEnvironment.tsx new file mode 100644 index 0000000000..5b99aeb80c --- /dev/null +++ b/apps/webapp/app/hooks/useEnvironment.tsx @@ -0,0 +1,23 @@ +import { type UIMatch } from "@remix-run/react"; +import { type UseDataFunctionReturn } from "remix-typedjson"; +import invariant from "tiny-invariant"; +import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route"; +import { organizationMatchId } from "./useOrganizations"; +import { useTypedMatchesData } from "./useTypedMatchData"; + +export type MatchedEnvironment = UseDataFunctionReturn["environment"]; + +export function useOptionalEnvironment(matches?: UIMatch[]) { + const routeMatch = useTypedMatchesData({ + id: organizationMatchId, + matches, + }); + + return routeMatch?.environment; +} + +export function useEnvironment(matches?: UIMatch[]) { + const environment = useOptionalEnvironment(matches); + invariant(environment, "Environment must be defined"); + return environment; +} diff --git a/apps/webapp/app/hooks/useFeatures.ts b/apps/webapp/app/hooks/useFeatures.ts index 6a726d74b1..e05d9ed7eb 100644 --- a/apps/webapp/app/hooks/useFeatures.ts +++ b/apps/webapp/app/hooks/useFeatures.ts @@ -1,5 +1,5 @@ import { useTypedRouteLoaderData } from "remix-typedjson"; -import { loader } from "../root"; +import { type loader } from "../root"; import type { TriggerFeatures } from "~/features.server"; export function useFeatures(): TriggerFeatures { diff --git a/apps/webapp/app/hooks/useProject.tsx b/apps/webapp/app/hooks/useProject.tsx index 28b847d30b..2280694c10 100644 --- a/apps/webapp/app/hooks/useProject.tsx +++ b/apps/webapp/app/hooks/useProject.tsx @@ -1,5 +1,5 @@ -import { UIMatch } from "@remix-run/react"; -import { UseDataFunctionReturn } from "remix-typedjson"; +import { type UIMatch } from "@remix-run/react"; +import { type UseDataFunctionReturn } from "remix-typedjson"; import invariant from "tiny-invariant"; import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route"; import { useChanged } from "./useChanged"; diff --git a/apps/webapp/app/models/user.server.ts b/apps/webapp/app/models/user.server.ts index df4d43ca43..2e16f2e956 100644 --- a/apps/webapp/app/models/user.server.ts +++ b/apps/webapp/app/models/user.server.ts @@ -2,6 +2,10 @@ import type { Prisma, User } from "@trigger.dev/database"; import type { GitHubProfile } from "remix-auth-github"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; +import { + DashboardPreferences, + getDashboardPreferences, +} from "~/services/dashboardPreferences.server"; export type { User } from "@trigger.dev/database"; type FindOrCreateMagicLink = { @@ -158,8 +162,23 @@ export async function findOrCreateGithubUser({ }; } +export type UserWithDashboardPreferences = User & { + dashboardPreferences: DashboardPreferences; +}; + export async function getUserById(id: User["id"]) { - return prisma.user.findUnique({ where: { id } }); + const user = await prisma.user.findUnique({ where: { id } }); + + if (!user) { + return null; + } + + const dashboardPreferences = getDashboardPreferences(user.dashboardPreferences); + + return { + ...user, + dashboardPreferences, + }; } export async function getUserByEmail(email: User["email"]) { diff --git a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts index 48941d3792..6b6164a309 100644 --- a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts +++ b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts @@ -1,15 +1,12 @@ -import { PrismaClient } from "@trigger.dev/database"; +import { type PrismaClient } from "@trigger.dev/database"; import { redirect } from "remix-typedjson"; import { prisma } from "~/db.server"; -import { redirectWithErrorMessage } from "~/models/message.server"; -import { - commitCurrentProjectSession, - getCurrentProjectId, - setCurrentProjectId, -} from "~/services/currentProject.server"; +import { type UserWithDashboardPreferences } from "~/models/user.server"; import { logger } from "~/services/logger.server"; -import { newProjectPath } from "~/utils/pathBuilder"; -import { ProjectPresenter } from "./ProjectPresenter.server"; +import { type UserFromSession } from "~/services/session.server"; +import { newOrganizationPath, newProjectPath } from "~/utils/pathBuilder"; +import { type MinimumEnvironment } from "./SelectBestEnvironmentPresenter.server"; +import { sortEnvironments } from "~/utils/environmentSort"; export class OrganizationsPresenter { #prismaClient: PrismaClient; @@ -19,25 +16,27 @@ export class OrganizationsPresenter { } public async call({ - userId, + user, organizationSlug, projectSlug, + environmentSlug, request, }: { - userId: string; + user: UserFromSession; organizationSlug: string; projectSlug: string | undefined; + environmentSlug: string | undefined; request: Request; }) { - //first get the project id, this redirects if there's no session - const projectId = await this.#getProjectId({ - request, - projectSlug, - organizationSlug, - userId, - }); + const organizations = await this.#getOrganizations(user.id); + if (organizations.length === 0) { + logger.info("No organizations", { + organizationSlug, + request, + }); + throw redirect(newOrganizationPath()); + } - const organizations = await this.#getOrganizations(userId); const organization = organizations.find((o) => o.slug === organizationSlug); if (!organization) { logger.info("Not Found: organization", { @@ -45,130 +44,74 @@ export class OrganizationsPresenter { request, organization, }); - throw new Response("Not Found", { status: 404 }); + throw new Response("Organization not Found", { status: 404 }); } - const projectPresenter = new ProjectPresenter(this.#prismaClient); - const project = await projectPresenter.call({ - id: projectId, - userId, + const bestProject = this.#getProject({ + user, + projectSlug, + projects: organization.projects, }); - - if (!project) { - throw redirectWithErrorMessage( - newProjectPath({ slug: organizationSlug }), + if (!bestProject) { + logger.info("Not Found: project", { + projectSlug, request, - "No projects found in organization" - ); - } - - if (project.organizationId !== organization.id) { - throw redirect(newProjectPath({ slug: organizationSlug }), request); - } - - return { organizations, organization, project }; - } - - async #getProjectId({ - request, - projectSlug, - organizationSlug, - userId, - }: { - request: Request; - projectSlug: string | undefined; - organizationSlug: string; - userId: string; - }): Promise { - const sessionProjectId = await getCurrentProjectId(request); - - //no project in session, let's set one - if (!sessionProjectId) { - //no session id and no project slug so we need to select the best project - if (!projectSlug) { - const bestProject = await this.#selectBestProjectForOrganization( - organizationSlug, - userId, - request - ); - const session = await setCurrentProjectId(bestProject.id, request); - throw redirect(request.url, { - headers: { "Set-Cookie": await commitCurrentProjectSession(session) }, - }); - } - - //get all the projects - const projects = await prisma.project.findMany({ - select: { - id: true, - slug: true, - }, - where: { - organization: { - slug: organizationSlug, - }, - deletedAt: null, - slug: projectSlug, - }, - orderBy: { - updatedAt: "desc", - }, - }); - - if (projects.length === 0) { - throw redirectWithErrorMessage( - newProjectPath({ slug: organizationSlug }), - request, - "No projects in this organization" - ); - } - - //try get the project which matches the URL - let matchingProject = projects.find((p) => p.slug === projectSlug); - - //if there's no matching project, just use the most recently updated one - if (!matchingProject) { - matchingProject = projects[0]; - } - - //set the session - const session = await setCurrentProjectId(matchingProject.id, request); - throw redirect(request.url, { - headers: { "Set-Cookie": await commitCurrentProjectSession(session) }, + project: bestProject, }); + throw redirect(newProjectPath(organization)); } - if (!projectSlug) { - return sessionProjectId; - } - - //check session id matches the project slug - const project = await prisma.project.findFirst({ - select: { - id: true, - slug: true, - }, + const fullProject = await this.#prismaClient.project.findFirst({ where: { - slug: projectSlug, - organization: { - slug: organizationSlug, + id: bestProject.id, + }, + include: { + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, }, - deletedAt: null, }, }); - if (!project) { - throw new Response("Project not found in organization", { status: 404 }); - } - - if (project.id !== sessionProjectId) { - const session = await setCurrentProjectId(project.id, request); - throw redirect(request.url, { - headers: { "Set-Cookie": await commitCurrentProjectSession(session) }, + if (!fullProject) { + logger.info("Not Found: project", { + projectSlug, + request, + project: bestProject, }); + throw redirect(newProjectPath(organization)); } - return project.id; + const environment = this.#getEnvironment({ + user, + projectId: fullProject.id, + environments: fullProject.environments, + environmentSlug, + }); + + return { + organizations, + organization, + project: { + ...fullProject, + environments: sortEnvironments( + fullProject.environments.filter((env) => { + if (env.type !== "DEVELOPMENT") return true; + if (env.orgMember?.userId === user.id) return true; + return false; + }) + ), + }, + environment, + }; } async #getOrganizations(userId: string) { @@ -179,14 +122,13 @@ export class OrganizationsPresenter { id: true, slug: true, title: true, - runsEnabled: true, projects: { - where: { deletedAt: null }, + where: { deletedAt: null, version: "V3" }, select: { id: true, slug: true, name: true, - version: true, + updatedAt: true, }, orderBy: { name: "asc" }, }, @@ -202,43 +144,97 @@ export class OrganizationsPresenter { id: project.id, slug: project.slug, name: project.name, - version: project.version, + updatedAt: project.updatedAt, })), - runsEnabled: org.runsEnabled, }; }); } - async #selectBestProjectForOrganization( - organizationSlug: string, - userId: string, - request: Request - ) { - const projects = await this.#prismaClient.project.findMany({ - select: { - id: true, - slug: true, - }, - where: { - deletedAt: null, - organization: { - deletedAt: null, - slug: organizationSlug, - members: { some: { userId } }, - }, - }, - orderBy: { - jobs: { - _count: "desc", - }, - }, - take: 1, - }); + #getProject({ + user, + projectSlug, + projects, + }: { + user: UserFromSession; + projectSlug: string | undefined; + projects: { + id: string; + slug: string; + name: string; + updatedAt: Date; + }[]; + }) { + if (projectSlug) { + const proj = projects.find((p) => p.slug === projectSlug); + if (proj) { + return proj; + } + + if (!proj) { + logger.info("Not Found: project", { + projectSlug, + projects, + }); + } + } + + const currentProjectId = user.dashboardPreferences.currentProjectId; + const project = projects.find((p) => p.id === currentProjectId); + if (project) { + return project; + } + + //most recently updated + return projects.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()).at(0); + } + + #getEnvironment({ + user, + projectId, + environmentSlug, + environments, + }: { + user: UserFromSession; + projectId: string; + environmentSlug: string | undefined; + environments: MinimumEnvironment[]; + }) { + if (environmentSlug) { + const env = environments.find((e) => e.slug === environmentSlug); + if (env) { + return env; + } + + if (!env) { + logger.info("Not Found: environment", { + environmentSlug, + environments, + }); + } + } + + const currentEnvironmentId: string | undefined = + user.dashboardPreferences.projects[projectId]?.currentEnvironment.id; + + const environment = environments.find((e) => e.id === currentEnvironmentId); + if (environment) { + return environment; + } + + //otherwise show their dev environment + const yourDevEnvironment = environments.find( + (env) => env.type === "DEVELOPMENT" && env.orgMember?.userId === user.id + ); + if (yourDevEnvironment) { + return yourDevEnvironment; + } - if (projects.length === 0) { - throw redirect(newProjectPath({ slug: organizationSlug }), request); + //otherwise show their prod environment + const prodEnvironment = environments.find((env) => env.type === "PRODUCTION"); + if (prodEnvironment) { + return prodEnvironment; } - return projects[0]; + throw new Error("No environments found"); } } diff --git a/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts b/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts new file mode 100644 index 0000000000..ac60b46686 --- /dev/null +++ b/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts @@ -0,0 +1,127 @@ +import { type RuntimeEnvironment, type PrismaClient } from "@trigger.dev/database"; +import { prisma } from "~/db.server"; +import { type UserFromSession } from "~/services/session.server"; + +export type MinimumEnvironment = Pick & { + orgMember: null | { + userId: string | undefined; + }; +}; + +export class SelectBestEnvironmentPresenter { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call({ user }: { user: UserFromSession }) { + const { project, organization } = await this.getBestProject(user); + const environment = await this.selectBestEnvironment(project.id, user, project.environments); + + return { + project, + organization, + environment, + }; + } + + async getBestProject(user: UserFromSession) { + //try get current project from cookie + const projectId = user.dashboardPreferences.currentProjectId; + + if (projectId) { + const project = await this.#prismaClient.project.findUnique({ + where: { + id: projectId, + deletedAt: null, + organization: { members: { some: { userId: user.id } } }, + }, + include: { + organization: true, + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }); + if (project) { + return { project, organization: project.organization }; + } + } + + //failing that, we pick the most recently modified project + const projects = await this.#prismaClient.project.findMany({ + include: { + organization: true, + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + where: { + deletedAt: null, + organization: { + members: { some: { userId: user.id } }, + }, + }, + orderBy: { + updatedAt: "desc", + }, + take: 1, + }); + + if (projects.length === 0) { + throw new Response("Not Found", { status: 404 }); + } + + return { project: projects[0], organization: projects[0].organization }; + } + + async selectBestEnvironment( + projectId: string, + user: UserFromSession, + environments: MinimumEnvironment[] + ): Promise { + //try get current environment from prefs + const currentEnvironmentId: string | undefined = + user.dashboardPreferences.projects[projectId]?.currentEnvironment.id; + + const currentEnvironment = environments.find((env) => env.id === currentEnvironmentId); + if (currentEnvironment) { + return currentEnvironment; + } + + //otherwise show their dev environment + const yourDevEnvironment = environments.find( + (env) => env.type === "DEVELOPMENT" && env.orgMember?.userId === user.id + ); + if (yourDevEnvironment) { + return yourDevEnvironment; + } + + //otherwise show their prod environment + const prodEnvironment = environments.find((env) => env.type === "PRODUCTION"); + if (prodEnvironment) { + return prodEnvironment; + } + + throw new Error("No environments found"); + } +} diff --git a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts b/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts deleted file mode 100644 index 2469baa9d8..0000000000 --- a/apps/webapp/app/presenters/SelectBestProjectPresenter.server.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { type PrismaClient } from "@trigger.dev/database"; -import { prisma } from "~/db.server"; -import { getCurrentEnvironmentType } from "~/services/currentEnvironmentType.server"; -import { getCurrentProjectId } from "~/services/currentProject.server"; - -export class SelectBestProjectPresenter { - #prismaClient: PrismaClient; - - constructor(prismaClient: PrismaClient = prisma) { - this.#prismaClient = prismaClient; - } - - public async call({ userId, request }: { userId: string; request: Request }) { - const { project, organization } = await this.#getBestProject(request, userId); - - //try get current environment from cookie - const environmentType = await getCurrentEnvironmentType(request); - - const environments = await this.#prismaClient.runtimeEnvironment.findMany({ - select: { - slug: true, - type: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - where: { - projectId: project.id, - }, - }); - - const relevantEnvironments = environments.filter((env) => env.type === environmentType); - - if (relevantEnvironments.length === 0) { - const yourDevEnvironment = environments.find( - (env) => env.type === "DEVELOPMENT" && env.orgMember?.userId === userId - ); - if (yourDevEnvironment) { - return { project, organization, environment: yourDevEnvironment }; - } - - const prodEnvironment = environments.find((env) => env.type === "PRODUCTION"); - if (prodEnvironment) { - return { project, organization, environment: prodEnvironment }; - } - - throw new Error("No environments found"); - } - - if (relevantEnvironments.length === 1) { - return { project, organization, environment: environments[0] }; - } - - const yourDevEnvironment = environments.find( - (env) => env.type === "DEVELOPMENT" && env.orgMember?.userId === userId - ); - if (yourDevEnvironment) { - return { project, organization, environment: yourDevEnvironment }; - } - - const prodEnvironment = environments.find((env) => env.type === "PRODUCTION"); - if (prodEnvironment) { - return { project, organization, environment: prodEnvironment }; - } - - throw new Error("No environments found"); - } - - async #getBestProject(request: Request, userId: string) { - //try get current project from cookie - const projectId = await getCurrentProjectId(request); - - if (projectId) { - const project = await this.#prismaClient.project.findUnique({ - where: { id: projectId, deletedAt: null, organization: { members: { some: { userId } } } }, - include: { - organization: true, - environments: { - select: { - type: true, - slug: true, - orgMember: { - select: { - userId: true, - }, - }, - }, - }, - }, - }); - if (project) { - return { project, organization: project.organization }; - } - } - - //failing that, we pick the most recently modified project - const projects = await this.#prismaClient.project.findMany({ - include: { - organization: true, - }, - where: { - deletedAt: null, - organization: { - members: { some: { userId } }, - }, - }, - orderBy: { - updatedAt: "desc", - }, - take: 1, - }); - - if (projects.length === 0) { - throw new Response("Not Found", { status: 404 }); - } - - return { project: projects[0], organization: projects[0].organization }; - } -} diff --git a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts index 1aed45d4d5..24dff779e0 100644 --- a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts @@ -1,25 +1,18 @@ -import { Prisma } from "@trigger.dev/database"; -import type { - RuntimeEnvironmentType, - TaskTriggerSource, - TaskRunStatus as TaskRunStatusType, +import { + Prisma, + type TaskTriggerSource, + type TaskRunStatus as TaskRunStatusType, + type RuntimeEnvironment, + type TaskRunStatus as DBTaskRunStatus, } from "@trigger.dev/database"; -import { QUEUED_STATUSES, RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus"; +import { QUEUED_STATUSES } from "~/components/runs/v3/TaskRunStatus"; import { sqlDatabaseSchema } from "~/db.server"; import type { Organization } from "~/models/organization.server"; import type { Project } from "~/models/project.server"; -import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; import type { User } from "~/models/user.server"; -import { - filterOrphanedEnvironments, - onlyDevEnvironments, - exceptDevEnvironments, - sortEnvironments, -} from "~/utils/environmentSort"; import { logger } from "~/services/logger.server"; import { BasePresenter } from "./basePresenter.server"; import { TaskRunStatus } from "~/database-types"; -import { concurrencyTracker } from "~/v3/services/taskRunConcurrencyTracker.server"; import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic"; export type Task = { @@ -28,12 +21,6 @@ export type Task = { filePath: string; createdAt: Date; triggerSource: TaskTriggerSource; - environments: { - id: string; - type: RuntimeEnvironmentType; - slug: string; - userName?: string; - }[]; }; type Return = Awaited>; @@ -45,51 +32,35 @@ export class TaskListPresenter extends BasePresenter { userId, projectSlug, organizationSlug, + environmentSlug, }: { userId: User["id"]; projectSlug: Project["slug"]; organizationSlug: Organization["slug"]; + environmentSlug: RuntimeEnvironment["slug"]; }) { - const project = await this._replica.project.findFirstOrThrow({ + const environment = await this._replica.runtimeEnvironment.findFirstOrThrow({ select: { id: true, - environments: { - select: { - id: true, - type: true, - slug: true, - orgMember: { - select: { - user: { - select: { - id: true, - name: true, - displayName: true, - }, - }, - }, - }, - }, - }, + type: true, }, where: { - slug: projectSlug, + slug: environmentSlug, + project: { + slug: projectSlug, + }, organization: { slug: organizationSlug, }, }, }); - const devEnvironments = onlyDevEnvironments(project.environments); - const nonDevEnvironments = exceptDevEnvironments(project.environments); - const tasks = await this._replica.$queryRaw< { id: string; slug: string; exportName: string; filePath: string; - runtimeEnvironmentId: string; createdAt: Date; triggerSource: TaskTriggerSource; }[] @@ -99,15 +70,13 @@ export class TaskListPresenter extends BasePresenter { FROM ${sqlDatabaseSchema}."WorkerDeploymentPromotion" wdp INNER JOIN ${sqlDatabaseSchema}."WorkerDeployment" wd ON wd.id = wdp."deploymentId" - WHERE wdp."environmentId" IN (${Prisma.join(nonDevEnvironments.map((e) => e.id))}) + WHERE wdp."environmentId" = ${environment.id} AND wdp."label" = ${CURRENT_DEPLOYMENT_LABEL} ), - workers AS ( + workers AS ( SELECT DISTINCT ON ("runtimeEnvironmentId") id, "runtimeEnvironmentId", version FROM ${sqlDatabaseSchema}."BackgroundWorker" - WHERE "runtimeEnvironmentId" IN (${Prisma.join( - filterOrphanedEnvironments(devEnvironments).map((e) => e.id) - )}) + WHERE "runtimeEnvironmentId" = ${environment.id} OR id IN (SELECT id FROM non_dev_workers) ORDER BY "runtimeEnvironmentId", "createdAt" DESC ) @@ -116,64 +85,26 @@ export class TaskListPresenter extends BasePresenter { JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" tasks ON tasks."workerId" = workers.id ORDER BY slug ASC;`; - //group by the task identifier (task.slug). - const outputTasks = tasks.reduce((acc, task) => { - const environment = project.environments.find((env) => env.id === task.runtimeEnvironmentId); - if (!environment) { - throw new Error(`Environment not found for TaskRun ${task.id}`); - } - - let existingTask = acc.find((t) => t.slug === task.slug); - - if (!existingTask) { - existingTask = { - ...task, - environments: [], - }; - acc.push(existingTask); - } - - //favour newer tasks - if (task.createdAt > existingTask.createdAt) { - existingTask.createdAt = task.createdAt; - existingTask.exportName = task.exportName; - existingTask.filePath = task.filePath; - existingTask.triggerSource = task.triggerSource; - } - - existingTask.environments.push(displayableEnvironment(environment, userId)); - - //order the environments - existingTask.environments = sortEnvironments(existingTask.environments); - - return acc; - }, [] as Task[]); - //then get the activity for each task const activity = this.#getActivity( - outputTasks.map((t) => t.slug), - project.id + tasks.map((t) => t.slug), + environment.id ); const runningStats = this.#getRunningStats( - outputTasks.map((t) => t.slug), - project.id + tasks.map((t) => t.slug), + environment.id ); const durations = this.#getAverageDurations( - outputTasks.map((t) => t.slug), - project.id + tasks.map((t) => t.slug), + environment.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 }; + return { tasks, environment, activity, runningStats, durations }; } - async #getActivity(tasks: string[], projectId: string) { + async #getActivity(tasks: string[], environmentId: string) { if (tasks.length === 0) { return {}; } @@ -186,22 +117,22 @@ export class TaskListPresenter extends BasePresenter { count: BigInt; }[] >` - SELECT - tr."taskIdentifier", + SELECT + tr."taskIdentifier", tr."status", - DATE(tr."createdAt") as day, - COUNT(*) - FROM + DATE(tr."createdAt") as day, + COUNT(*) + FROM ${sqlDatabaseSchema}."TaskRun" as tr - WHERE + WHERE tr."taskIdentifier" IN (${Prisma.join(tasks)}) - AND tr."projectId" = ${projectId} + AND tr."runtimeEnvironmentId" = ${environmentId} AND tr."createdAt" >= (current_date - interval '6 days') - GROUP BY - tr."taskIdentifier", - tr."status", + GROUP BY + tr."taskIdentifier", + tr."status", day - ORDER BY + ORDER BY tr."taskIdentifier" ASC, day ASC, tr."status" ASC;`; @@ -248,48 +179,67 @@ export class TaskListPresenter extends BasePresenter { }, {} as Record)[]>); } - async #getRunningStats(tasks: string[], projectId: string) { + async #getRunningStats(tasks: string[], environmentId: string) { if (tasks.length === 0) { return {}; } - const concurrencies = await concurrencyTracker.taskConcurrentRunCounts(projectId, tasks); - - const queued = await this._replica.$queryRaw< + const stats = await this._replica.$queryRaw< { taskIdentifier: string; + status: DBTaskRunStatus; count: BigInt; }[] >` - SELECT + SELECT tr."taskIdentifier", - COUNT(*) - FROM + tr.status, + COUNT(*) + FROM ${sqlDatabaseSchema}."TaskRun" as tr - WHERE + WHERE tr."taskIdentifier" IN (${Prisma.join(tasks)}) - AND tr."projectId" = ${projectId} - AND tr."status" = ANY(ARRAY[${Prisma.join(QUEUED_STATUSES)}]::\"TaskRunStatus\"[]) - GROUP BY - tr."taskIdentifier" - ORDER BY + AND tr."runtimeEnvironmentId" = ${environmentId} + AND tr."status" = ANY(ARRAY[${Prisma.join([ + ...QUEUED_STATUSES, + "EXECUTING", + ])}]::\"TaskRunStatus\"[]) + GROUP BY + tr."taskIdentifier", + tr.status + ORDER BY tr."taskIdentifier" ASC`; //create an object combining the queued and concurrency counts const result: Record = {}; for (const task of tasks) { - const concurrency = concurrencies[task] ?? 0; - const queuedCount = queued.find((q) => q.taskIdentifier === task)?.count ?? 0; + const queued = stats.filter( + (q) => q.taskIdentifier === task && QUEUED_STATUSES.includes(q.status) + ); + const queuedCount = + queued.length === 0 + ? 0 + : queued.reduce((acc, q) => { + return acc + Number(q.count); + }, 0); + + const running = stats.filter((r) => r.taskIdentifier === task && r.status === "EXECUTING"); + const runningCount = + running.length === 0 + ? 0 + : running.reduce((acc, r) => { + return acc + Number(r.count); + }, 0); result[task] = { - queued: Number(queuedCount), - running: concurrency, + queued: queuedCount, + running: runningCount, }; } return result; } - async #getAverageDurations(tasks: string[], projectId: string) { + async #getAverageDurations(tasks: string[], environmentId: string) { if (tasks.length === 0) { return {}; } @@ -299,18 +249,18 @@ export class TaskListPresenter extends BasePresenter { taskIdentifier: string; duration: Number; }[] - >` - SELECT - tr."taskIdentifier", + >` + SELECT + tr."taskIdentifier", AVG(EXTRACT(EPOCH FROM (tr."updatedAt" - COALESCE(tr."startedAt", tr."lockedAt")))) as duration - FROM + FROM ${sqlDatabaseSchema}."TaskRun" as tr - WHERE + WHERE tr."taskIdentifier" IN (${Prisma.join(tasks)}) - AND tr."projectId" = ${projectId} + AND tr."runtimeEnvironmentId" = ${environmentId} AND tr."createdAt" >= (current_date - interval '6 days') AND tr."status" IN ('COMPLETED_SUCCESSFULLY', 'COMPLETED_WITH_ERRORS') - GROUP BY + GROUP BY tr."taskIdentifier";`; return Object.fromEntries(durations.map((s) => [s.taskIdentifier, Number(s.duration)])); diff --git a/apps/webapp/app/routes/_app._index/route.tsx b/apps/webapp/app/routes/_app._index/route.tsx index 041f8aee11..cc4fbea8be 100644 --- a/apps/webapp/app/routes/_app._index/route.tsx +++ b/apps/webapp/app/routes/_app._index/route.tsx @@ -1,12 +1,13 @@ -import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; import { prisma } from "~/db.server"; import { getUsersInvites } from "~/models/member.server"; -import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server"; +import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server"; import { requireUser } from "~/services/session.server"; import { invitesPath, newOrganizationPath, newProjectPath, + v3EnvironmentPath, v3ProjectPath, } from "~/utils/pathBuilder"; @@ -20,11 +21,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { return redirect(invitesPath()); } - const presenter = new SelectBestProjectPresenter(); + const presenter = new SelectBestEnvironmentPresenter(); try { - const { project, organization } = await presenter.call({ userId: user.id, request }); + const { project, organization, environment } = await presenter.call({ + user, + }); //redirect them to the most appropriate project - return redirect(v3ProjectPath(organization, project)); + return redirect(v3EnvironmentPath(organization, project, environment)); } catch (e) { const organization = await prisma.organization.findFirst({ where: { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx index 34f098a241..f669b0db18 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx @@ -1,5 +1,5 @@ import { FolderIcon } from "@heroicons/react/20/solid"; -import { Link, MetaFunction } from "@remix-run/react"; +import { Link, type MetaFunction } from "@remix-run/react"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { LinkButton } from "~/components/primitives/Buttons"; @@ -50,7 +50,6 @@ export default function Page() {
{project.name} - {project.version}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx similarity index 93% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam._index/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index 3b32688e4e..c0a94cf014 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -11,12 +11,12 @@ import { } from "@heroicons/react/20/solid"; import { json, type MetaFunction } from "@remix-run/node"; import { Link, useRevalidator, useSubmit } from "@remix-run/react"; -import { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { DiscordIcon } from "@trigger.dev/companyicons"; import { formatDurationMilliseconds } from "@trigger.dev/core/v3"; -import { TaskRunStatus } from "@trigger.dev/database"; +import { type TaskRunStatus } from "@trigger.dev/database"; import { Fragment, Suspense, useEffect, useState } from "react"; -import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps } from "recharts"; +import { Bar, BarChart, ResponsiveContainer, Tooltip, type TooltipProps } from "recharts"; import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { RunsIcon } from "~/assets/icons/RunsIcon"; @@ -69,11 +69,16 @@ import { taskTriggerSourceDescription, TaskTriggerSourceIcon, } from "~/components/runs/v3/TaskTriggerSource"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useEventSource } from "~/hooks/useEventSource"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useTextFilter } from "~/hooks/useTextFilter"; -import { Task, TaskActivity, TaskListPresenter } from "~/presenters/v3/TaskListPresenter.server"; +import { + type Task, + type TaskActivity, + TaskListPresenter, +} from "~/presenters/v3/TaskListPresenter.server"; import { getUsefulLinksPreference, setUsefulLinksPreference, @@ -83,6 +88,7 @@ import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { docsPath, + EnvironmentParamSchema, inviteTeamMemberPath, ProjectParamSchema, v3RunsPath, @@ -101,21 +107,22 @@ export const meta: MetaFunction = () => { export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); try { const presenter = new TaskListPresenter(); - const { tasks, userHasTasks, activity, runningStats, durations } = await presenter.call({ + const { tasks, environment, activity, runningStats, durations } = await presenter.call({ userId, organizationSlug, projectSlug: projectParam, + environmentSlug: envParam, }); const usefulLinksPreference = await getUsefulLinksPreference(request); return typeddefer({ tasks, - userHasTasks, + environment, activity, runningStats, durations, @@ -149,7 +156,8 @@ export async function action({ request }: ActionFunctionArgs) { export default function Page() { const organization = useOrganization(); const project = useProject(); - const { tasks, userHasTasks, activity, runningStats, durations, usefulLinksPreference } = + const environment = useEnvironment(); + const { tasks, activity, runningStats, durations, usefulLinksPreference } = useTypedLoaderData(); const { filterText, setFilterText, filteredItems } = useTextFilter({ items: tasks, @@ -211,13 +219,6 @@ export default function Page() { {tasks.map((task) => ( {task.exportName} - - {task.environments - .map((e) => - e.userName ? `${e.userName}/${e.id}` : `${e.type.slice(0, 3)}/${e.id}` - ) - .join(", ")} - ))} @@ -237,7 +238,7 @@ export default function Page() {
{hasTasks ? (
- {!userHasTasks && } + {tasks.length === 0 ? : null}
Queued Activity (7d) Avg. duration - Environments Go to page @@ -278,22 +278,12 @@ export default function Page() { tasks: [task.slug], }); - const devYouEnvironment = task.environments.find( - (e) => e.type === "DEVELOPMENT" && !e.userName + const testPath = v3TestTaskPath( + organization, + project, + { taskIdentifier: task.slug }, + environment ); - const firstDeployedEnvironment = task.environments - .filter((e) => e.type !== "DEVELOPMENT") - .at(0); - const testEnvironment = devYouEnvironment ?? firstDeployedEnvironment; - - const testPath = testEnvironment - ? v3TestTaskPath( - organization, - project, - { taskIdentifier: task.slug }, - testEnvironment.slug - ) - : v3TestPath(organization, project); return ( @@ -372,9 +362,6 @@ export default function Page() { - - - { + const user = await requireUser(request); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); + + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + organization: { + slug: organizationSlug, + members: { + some: { + userId: user.id, + }, + }, + }, + deletedAt: null, + }, + select: { + id: true, + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }); + + if (!project) { + logger.error("Project not found", { params, user }); + throw new Response("Project not Found", { status: 404, statusText: "Project not found" }); + } + + const environments = project.environments.filter((env) => env.slug === envParam); + if (environments.length === 0) { + throw new Response("Environment not Found", { + status: 404, + statusText: "Environment not found", + }); + } + + let environmentId: string | undefined = undefined; + + if (environments.length > 1) { + const bestEnvironment = environments.find((env) => env.orgMember?.userId === user.id); + if (!bestEnvironment) { + throw new Response("Environment not Found", { + status: 404, + statusText: "Environment not found", + }); + } + + environmentId = bestEnvironment.id; + } else { + environmentId = environments[0].id; + } + + await updateCurrentProjectEnvironmentId({ user: user, projectId: project.id, environmentId }); + + return project; +}; + +export default function Page() { + return ; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx new file mode 100644 index 0000000000..938af52804 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx @@ -0,0 +1,29 @@ +import { Outlet } from "@remix-run/react"; +import { RouteErrorDisplay } from "~/components/ErrorDisplay"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { type Handle } from "~/utils/handle"; +import { v3ProjectPath } from "~/utils/pathBuilder"; + +export const handle: Handle = { + scripts: () => [ + { + src: "https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js", + crossOrigin: "anonymous", + }, + ], +}; + +export default function Project() { + return ( + <> + + + ); +} + +export function ErrorBoundary() { + const org = useOrganization(); + const project = useProject(); + return ; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.projectParam._index/route.tsx new file mode 100644 index 0000000000..ef042b350d --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.projectParam._index/route.tsx @@ -0,0 +1,6 @@ +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { requireUserId } from "~/services/session.server"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + throw new Response("Not Implemented", { status: 501 }); +}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam/route.tsx deleted file mode 100644 index 597ae53b9b..0000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam/route.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Outlet } from "@remix-run/react"; -import { json, LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; -import { ErrorDisplay, RouteErrorDisplay } from "~/components/ErrorDisplay"; -import { TextLink } from "~/components/primitives/TextLink"; -import { prisma } from "~/db.server"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { Handle } from "~/utils/handle"; -import { ProjectParamSchema, v3ProjectPath } from "~/utils/pathBuilder"; - -export const loader = async ({ params }: LoaderFunctionArgs) => { - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); - - const project = await prisma.project.findUnique({ - select: { version: true }, - where: { slug: projectParam }, - }); - - if (!project) { - throw new Response("Project not found", { status: 404, statusText: "Project not found" }); - } - - return typedjson({ - version: project.version, - }); -}; - -export const handle: Handle = { - scripts: (match) => [ - { - src: "https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js", - crossOrigin: "anonymous", - }, - ], -}; - -export default function Project() { - const { version } = useTypedLoaderData(); - - if (version === "V2") { - return ( - - This project is v2, which was deprecated on Jan 31 2025 after{" "} - - our announcement in August 2024 - - . - - } - /> - ); - } - - return ( - <> - - - ); -} - -export function ErrorBoundary() { - const org = useOrganization(); - const project = useProject(); - return ; -} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx index ca338afac9..89b4782b6d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx @@ -1,8 +1,8 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid"; -import { Form, MetaFunction, useActionData, useNavigation } from "@remix-run/react"; -import { ActionFunction, json } from "@remix-run/server-runtime"; +import { Form, type MetaFunction, useActionData, useNavigation } from "@remix-run/react"; +import { type ActionFunction, json } from "@remix-run/server-runtime"; import { redirect } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; @@ -21,13 +21,10 @@ import { SpinnerWhite } from "~/components/primitives/Spinner"; import { prisma } from "~/db.server"; import { useOrganization } from "~/hooks/useOrganizations"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; -import { - clearCurrentProjectId, - commitCurrentProjectSession, -} from "~/services/currentProject.server"; +import { clearCurrentProject } from "~/services/dashboardPreferences.server"; import { DeleteOrganizationService } from "~/services/deleteOrganization.server"; import { logger } from "~/services/logger.server"; -import { requireUserId } from "~/services/session.server"; +import { requireUser, requireUserId } from "~/services/session.server"; import { organizationPath, organizationSettingsPath, rootPath } from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { @@ -76,7 +73,7 @@ export function createSchema( } export const action: ActionFunction = async ({ request, params }) => { - const userId = await requireUserId(request); + const user = await requireUser(request); const { organizationSlug } = params; if (!organizationSlug) { return json({ errors: { body: "organizationSlug is required" } }, { status: 400 }); @@ -102,7 +99,7 @@ export const action: ActionFunction = async ({ request, params }) => { slug: organizationSlug, members: { some: { - userId, + userId: user.id, }, }, }, @@ -120,15 +117,13 @@ export const action: ActionFunction = async ({ request, params }) => { case "delete": { const deleteOrganizationService = new DeleteOrganizationService(); try { - await deleteOrganizationService.call({ organizationSlug, userId, request }); + await deleteOrganizationService.call({ organizationSlug, userId: user.id, request }); //we need to clear the project from the session - const removeProjectIdSession = await clearCurrentProjectId(request); - return redirect(rootPath(), { - headers: { - "Set-Cookie": await commitCurrentProjectSession(removeProjectIdSession), - }, + await clearCurrentProject({ + user, }); + return redirect(rootPath()); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : JSON.stringify(error); logger.error("Organization could not be deleted", { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx index 4941f148ce..cd9da35342 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx @@ -1,4 +1,4 @@ -import { Outlet, ShouldRevalidateFunction, UIMatch } from "@remix-run/react"; +import { Outlet, type ShouldRevalidateFunction, type UIMatch } from "@remix-run/react"; import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; @@ -10,14 +10,15 @@ import { useTypedMatchesData } from "~/hooks/useTypedMatchData"; import { useUser } from "~/hooks/useUser"; import { OrganizationsPresenter } from "~/presenters/OrganizationsPresenter.server"; import { getImpersonationId } from "~/services/impersonation.server"; -import { getCachedUsage, getCurrentPlan, getUsage } from "~/services/platform.v3.server"; -import { requireUserId } from "~/services/session.server"; +import { getCachedUsage, getCurrentPlan } from "~/services/platform.v3.server"; +import { requireUser } from "~/services/session.server"; import { telemetry } from "~/services/telemetry.server"; import { organizationPath } from "~/utils/pathBuilder"; const ParamsSchema = z.object({ organizationSlug: z.string(), projectParam: z.string().optional(), + envParam: z.string().optional(), }); export function useCurrentPlan(matches?: UIMatch[]) { @@ -42,6 +43,9 @@ export const shouldRevalidate: ShouldRevalidateFunction = (params) => { if (current.data.projectParam !== next.data.projectParam) { return true; } + if (current.data.envParam !== next.data.envParam) { + return true; + } } // This prevents revalidation when there are search params changes @@ -51,17 +55,18 @@ export const shouldRevalidate: ShouldRevalidateFunction = (params) => { // IMPORTANT: Make sure to update shouldRevalidate if this loader depends on search params export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); + const user = await requireUser(request); const impersonationId = await getImpersonationId(request); - const { organizationSlug, projectParam } = ParamsSchema.parse(params); + const { organizationSlug, projectParam, envParam } = ParamsSchema.parse(params); const orgsPresenter = new OrganizationsPresenter(); - const { organizations, organization, project } = await orgsPresenter.call({ - userId, + const { organizations, organization, project, environment } = await orgsPresenter.call({ + user, request, organizationSlug, projectSlug: projectParam, + environmentSlug: envParam, }); telemetry.organization.identify({ organization }); @@ -95,22 +100,26 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { organizations, organization, project, + environment, isImpersonating: !!impersonationId, currentPlan: { ...plan, v3Usage: { ...usage, hasExceededFreeTier, usagePercentage } }, }); }; export default function Organization() { - const { organization, project, organizations, isImpersonating } = + const { organization, project, organizations, environment, isImpersonating } = useTypedLoaderData(); const user = useUser(); + console.log(project); + return ( <>
diff --git a/apps/webapp/app/routes/logout.tsx b/apps/webapp/app/routes/logout.tsx index c0c133f2c4..bd7cd1394b 100644 --- a/apps/webapp/app/routes/logout.tsx +++ b/apps/webapp/app/routes/logout.tsx @@ -1,36 +1,10 @@ -import { redirect, type ActionFunction, type LoaderFunction } from "@remix-run/node"; +import { type ActionFunction, type LoaderFunction } from "@remix-run/node"; import { authenticator } from "~/services/auth.server"; -import { - clearCurrentProjectId, - commitCurrentProjectSession, - getCurrentProjectId, -} from "~/services/currentProject.server"; -import { logoutPath } from "~/utils/pathBuilder"; export const action: ActionFunction = async ({ request }) => { - const projectId = await getCurrentProjectId(request); - if (projectId) { - const removeProjectIdSession = await clearCurrentProjectId(request); - return redirect(logoutPath(), { - headers: { - "Set-Cookie": await commitCurrentProjectSession(removeProjectIdSession), - }, - }); - } - return await authenticator.logout(request, { redirectTo: "/" }); }; export const loader: LoaderFunction = async ({ request }) => { - const projectId = await getCurrentProjectId(request); - if (projectId) { - const removeProjectIdSession = await clearCurrentProjectId(request); - return redirect(logoutPath(), { - headers: { - "Set-Cookie": await commitCurrentProjectSession(removeProjectIdSession), - }, - }); - } - return await authenticator.logout(request, { redirectTo: "/" }); }; diff --git a/apps/webapp/app/routes/projects.new.ts b/apps/webapp/app/routes/projects.new.ts index ec9a67f936..604bcc0fa9 100644 --- a/apps/webapp/app/routes/projects.new.ts +++ b/apps/webapp/app/routes/projects.new.ts @@ -1,6 +1,6 @@ -import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; import { getUsersInvites } from "~/models/member.server"; -import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server"; +import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server"; import { requireUser } from "~/services/session.server"; import { invitesPath, newOrganizationPath, newProjectPath } from "~/utils/pathBuilder"; @@ -10,10 +10,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); - const presenter = new SelectBestProjectPresenter(); + const presenter = new SelectBestEnvironmentPresenter(); try { - const { organization } = await presenter.call({ userId: user.id, request }); + const { organization } = await presenter.call({ user: user }); //redirect them to the most appropriate project return redirect(`${newProjectPath(organization)}${url.search}`); } catch (e) { diff --git a/apps/webapp/app/services/currentProject.server.ts b/apps/webapp/app/services/currentProject.server.ts deleted file mode 100644 index cebb20f108..0000000000 --- a/apps/webapp/app/services/currentProject.server.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Session, createCookieSessionStorage } from "@remix-run/node"; -import { env } from "~/env.server"; - -export const currentProjectSessionStorage = createCookieSessionStorage({ - cookie: { - name: "__project", // use any name you want here - sameSite: "lax", // this helps with CSRF - path: "/", // remember to add this so the cookie will work in all routes - httpOnly: true, // for security reasons, make this cookie http only - secrets: [env.SESSION_SECRET], - secure: env.NODE_ENV === "production", // enable this in prod only - maxAge: 60 * 60 * 365, // 1 year - }, -}); - -function getCurrentProjectSession(request: Request) { - return currentProjectSessionStorage.getSession(request.headers.get("Cookie")); -} - -export function commitCurrentProjectSession(session: Session) { - return currentProjectSessionStorage.commitSession(session); -} - -export async function getCurrentProjectId(request: Request): Promise { - const session = await getCurrentProjectSession(request); - return session.get("currentProjectId"); -} - -export async function setCurrentProjectId(id: string, request: Request) { - const session = await getCurrentProjectSession(request); - session.set("currentProjectId", id); - return session; -} - -export async function clearCurrentProjectId(request: Request) { - const session = await getCurrentProjectSession(request); - session.unset("currentProjectId"); - return session; -} diff --git a/apps/webapp/app/services/dashboardPreferences.server.ts b/apps/webapp/app/services/dashboardPreferences.server.ts new file mode 100644 index 0000000000..3649704b81 --- /dev/null +++ b/apps/webapp/app/services/dashboardPreferences.server.ts @@ -0,0 +1,101 @@ +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { logger } from "./logger.server"; +import { type UserFromSession } from "./session.server"; + +const DashboardPreferences = z.object({ + version: z.literal("1"), + currentProjectId: z.string().optional(), + projects: z.record( + z.string(), + z.object({ + currentEnvironment: z.object({ id: z.string() }), + }) + ), +}); + +export type DashboardPreferences = z.infer; + +export function getDashboardPreferences(data?: any | null): DashboardPreferences { + if (!data) { + return { + version: "1", + projects: {}, + }; + } + + const result = DashboardPreferences.safeParse(data); + if (!result.success) { + logger.error("Failed to parse DashboardPreferences", { data, error: result.error }); + return { + version: "1", + projects: {}, + }; + } + + return result.data; +} + +export async function updateCurrentProjectEnvironmentId({ + user, + projectId, + environmentId, +}: { + user: UserFromSession; + projectId: string; + environmentId: string; +}) { + if (user.isImpersonating) { + return; + } + + //only update if the existing preferences are different + if ( + user.dashboardPreferences.currentProjectId === projectId && + user.dashboardPreferences.projects[projectId]?.currentEnvironment?.id === environmentId + ) { + return; + } + + //ok we need to update the preferences + const updatedPreferences: DashboardPreferences = { + ...user.dashboardPreferences, + currentProjectId: projectId, + projects: { + ...user.dashboardPreferences.projects, + [projectId]: { + ...user.dashboardPreferences.projects[projectId], + currentEnvironment: { id: environmentId }, + }, + }, + }; + + return prisma.user.update({ + where: { + id: user.id, + }, + data: { + dashboardPreferences: updatedPreferences, + }, + }); +} + +export async function clearCurrentProject({ user }: { user: UserFromSession }) { + if (user.isImpersonating) { + return; + } + + const updatedPreferences: DashboardPreferences = { + ...user.dashboardPreferences, + currentProjectId: undefined, + }; + + return prisma.user.update({ + where: { + id: user.id, + }, + data: { + dashboardPreferences: updatedPreferences, + }, + }); +} diff --git a/apps/webapp/app/services/impersonation.server.ts b/apps/webapp/app/services/impersonation.server.ts index 3f16857e30..78c771528b 100644 --- a/apps/webapp/app/services/impersonation.server.ts +++ b/apps/webapp/app/services/impersonation.server.ts @@ -1,5 +1,4 @@ -import type { Session } from "@remix-run/node"; -import { createCookieSessionStorage } from "@remix-run/node"; +import { createCookieSessionStorage, type Session } from "@remix-run/node"; import { env } from "~/env.server"; export const impersonationSessionStorage = createCookieSessionStorage({ diff --git a/apps/webapp/app/services/session.server.ts b/apps/webapp/app/services/session.server.ts index 8240ca2e7f..55cbd06b5a 100644 --- a/apps/webapp/app/services/session.server.ts +++ b/apps/webapp/app/services/session.server.ts @@ -34,11 +34,28 @@ export async function requireUserId(request: Request, redirectTo?: string) { return userId; } +export type UserFromSession = Awaited>; + export async function requireUser(request: Request) { const userId = await requireUserId(request); + const impersonationId = await getImpersonationId(request); const user = await getUserById(userId); - if (user) return user; + if (user) { + return { + id: user.id, + email: user.email, + name: user.name, + displayName: user.displayName, + avatarUrl: user.avatarUrl, + admin: user.admin, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + dashboardPreferences: user.dashboardPreferences, + confirmedBasicDetails: user.confirmedBasicDetails, + isImpersonating: !!impersonationId, + }; + } throw await logout(request); } diff --git a/apps/webapp/app/utils/environmentSort.ts b/apps/webapp/app/utils/environmentSort.ts index 64a681c1fc..4ed749bf64 100644 --- a/apps/webapp/app/utils/environmentSort.ts +++ b/apps/webapp/app/utils/environmentSort.ts @@ -1,4 +1,4 @@ -import { Prisma, RuntimeEnvironmentType } from "@trigger.dev/database"; +import { type RuntimeEnvironmentType } from "@trigger.dev/database"; const environmentSortOrder: RuntimeEnvironmentType[] = [ "DEVELOPMENT", diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index d1d2e177d8..0b9c679c46 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -1,12 +1,13 @@ -import type { TaskRun, WorkerDeployment } from "@trigger.dev/database"; +import type { RuntimeEnvironment, TaskRun, WorkerDeployment } from "@trigger.dev/database"; import { z } from "zod"; -import { TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; +import { type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters"; import type { Organization } from "~/models/organization.server"; import type { Project } from "~/models/project.server"; import { objectToSearchParams } from "./searchParams"; export type OrgForPath = Pick; export type ProjectForPath = Pick; +export type EnvironmentForPath = Pick; export type v3RunForPath = Pick; export type v3SpanForPath = Pick; export type DeploymentForPath = Pick; @@ -22,6 +23,10 @@ export const ProjectParamSchema = OrganizationParamsSchema.extend({ projectParam: z.string(), }); +export const EnvironmentParamSchema = ProjectParamSchema.extend({ + envParam: z.string(), +}); + //v3 export const v3TaskParamsSchema = ProjectParamSchema.extend({ taskParam: z.string(), @@ -123,9 +128,23 @@ function projectParam(project: ProjectForPath) { return project.slug; } +function environmentParam(environment: EnvironmentForPath) { + return environment.slug; +} + //v3 project export function v3ProjectPath(organization: OrgForPath, project: ProjectForPath) { - return `/orgs/${organizationParam(organization)}/projects/v3/${projectParam(project)}`; + return `/orgs/${organizationParam(organization)}/projects/${projectParam(project)}`; +} + +export function v3EnvironmentPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + return `/orgs/${organizationParam(organization)}/projects/${projectParam( + project + )}/env/${environmentParam(environment)}`; } export function v3TasksStreamingPath(organization: OrgForPath, project: ProjectForPath) { @@ -177,11 +196,11 @@ export function v3TestTaskPath( organization: OrgForPath, project: ProjectForPath, task: TaskForPath, - environmentSlug: string + environment: EnvironmentForPath ) { return `${v3TestPath(organization, project)}/tasks/${encodeURIComponent( task.taskIdentifier - )}?environment=${environmentSlug}`; + )}?environment=${environment.slug}`; } export function v3RunsPath( @@ -282,15 +301,15 @@ export function v3DeploymentPath( } export function v3BillingPath(organization: OrgForPath) { - return `${organizationPath(organization)}/v3/billing`; + return `${organizationPath(organization)}/billing`; } export function v3StripePortalPath(organization: OrgForPath) { - return `/resources/${organization.slug}/subscription/v3/portal`; + return `/resources/${organization.slug}/subscription/portal`; } export function v3UsagePath(organization: OrgForPath) { - return `${organizationPath(organization)}/v3/usage`; + return `${organizationPath(organization)}/usage`; } // Task diff --git a/internal-packages/database/prisma/migrations/20250310132020_added_dashboard_preferences_to_user/migration.sql b/internal-packages/database/prisma/migrations/20250310132020_added_dashboard_preferences_to_user/migration.sql new file mode 100644 index 0000000000..636a37b2a2 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250310132020_added_dashboard_preferences_to_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "dashboardPreferences" JSONB; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index af2c88485d..f9799cbb69 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -23,13 +23,19 @@ model User { name String? avatarUrl String? - admin Boolean @default(false) - isOnCloudWaitlist Boolean @default(false) + admin Boolean @default(false) + + /// Preferences for the dashboard + dashboardPreferences Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + /// @deprecated + isOnCloudWaitlist Boolean @default(false) + /// @deprecated featureCloud Boolean @default(false) + /// @deprecated isOnHostedRepoWaitlist Boolean @default(false) marketingEmails Boolean @default(true) @@ -39,7 +45,9 @@ model User { orgMemberships OrgMember[] sentInvites OrgMemberInvite[] - apiVotes ApiIntegrationVote[] + + /// @deprecated + apiVotes ApiIntegrationVote[] invitationCode InvitationCode? @relation(fields: [invitationCodeId], references: [id]) invitationCodeId String? diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c79c6bf79..a9f058da79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,31 +144,6 @@ importers: specifier: ^4.7.0 version: 4.7.1 - apps/proxy: - dependencies: - '@aws-sdk/client-sqs': - specifier: ^3.445.0 - version: 3.454.0 - '@trigger.dev/core': - specifier: workspace:* - version: link:../../packages/core - ulidx: - specifier: ^2.2.1 - version: 2.2.1 - zod: - specifier: 3.23.8 - version: 3.23.8 - zod-error: - specifier: 1.5.0 - version: 1.5.0 - devDependencies: - '@cloudflare/workers-types': - specifier: ^4.20240512.0 - version: 4.20240512.0 - wrangler: - specifier: ^3.57.1 - version: 3.57.1(@cloudflare/workers-types@4.20240512.0) - apps/supervisor: dependencies: '@kubernetes/client-node': @@ -5533,13 +5508,6 @@ packages: sisteransi: 1.0.5 dev: false - /@cloudflare/kv-asset-handler@0.3.2: - resolution: {integrity: sha512-EeEjMobfuJrwoctj7FA1y1KEbM0+Q1xSjobIEyie9k4haVEBB7vkDvsasw1pM3rO39mL2akxIAzLMUAtrMHZhA==} - engines: {node: '>=16.13'} - dependencies: - mime: 3.0.0 - dev: true - /@cloudflare/kv-asset-handler@0.3.4: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} @@ -5547,15 +5515,6 @@ packages: mime: 3.0.0 dev: false - /@cloudflare/workerd-darwin-64@1.20240512.0: - resolution: {integrity: sha512-VMp+CsSHFALQiBzPdQ5dDI4T1qwLu0mQ0aeKVNDosXjueN0f3zj/lf+mFil5/9jBbG3t4mG0y+6MMnalP9Lobw==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@cloudflare/workerd-darwin-64@1.20240806.0: resolution: {integrity: sha512-FqcVBBCO//I39K5F+HqE/v+UkqY1UrRnS653Jv+XsNNH9TpX5fTs7VCKG4kDSnmxlAaKttyIN5sMEt7lpuNExQ==} engines: {node: '>=16'} @@ -5565,15 +5524,6 @@ packages: dev: false optional: true - /@cloudflare/workerd-darwin-arm64@1.20240512.0: - resolution: {integrity: sha512-lZktXGmzMrB5rJqY9+PmnNfv1HKlj/YLZwMjPfF0WVKHUFdvQbAHsi7NlKv6mW9uIvlZnS+K4sIkWc0MDXcRnA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@cloudflare/workerd-darwin-arm64@1.20240806.0: resolution: {integrity: sha512-8c3KvmzYp/wg+82KHSOzDetJK+pThH4MTrU1OsjmsR2cUfedm5dk5Lah9/0Ld68+6A0umFACi4W2xJHs/RoBpA==} engines: {node: '>=16'} @@ -5583,15 +5533,6 @@ packages: dev: false optional: true - /@cloudflare/workerd-linux-64@1.20240512.0: - resolution: {integrity: sha512-wrHvqCZZqXz6Y3MUTn/9pQNsvaoNjbJpuA6vcXsXu8iCzJi911iVW2WUEBX+MpUWD+mBIP0oXni5tTlhkokOPw==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@cloudflare/workerd-linux-64@1.20240806.0: resolution: {integrity: sha512-/149Bpxw4e2p5QqnBc06g0mx+4sZYh9j0doilnt0wk/uqYkLp0DdXGMQVRB74sBLg2UD3wW8amn1w3KyFhK2tQ==} engines: {node: '>=16'} @@ -5601,15 +5542,6 @@ packages: dev: false optional: true - /@cloudflare/workerd-linux-arm64@1.20240512.0: - resolution: {integrity: sha512-YPezHMySL9J9tFdzxz390eBswQ//QJNYcZolz9Dgvb3FEfdpK345cE/bsWbMOqw5ws2f82l388epoenghtYvAg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@cloudflare/workerd-linux-arm64@1.20240806.0: resolution: {integrity: sha512-lacDWY3S1rKL/xT6iMtTQJEKmTTKrBavPczInEuBFXElmrS6IwVjZwv8hhVm32piyNt/AuFu9BYoJALi9D85/g==} engines: {node: '>=16'} @@ -5619,15 +5551,6 @@ packages: dev: false optional: true - /@cloudflare/workerd-windows-64@1.20240512.0: - resolution: {integrity: sha512-SxKapDrIYSscMR7lGIp/av0l6vokjH4xQ9ACxHgXh+OdOus9azppSmjaPyw4/ePvg7yqpkaNjf9o258IxWtvKQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@cloudflare/workerd-windows-64@1.20240806.0: resolution: {integrity: sha512-hC6JEfTSQK6//Lg+D54TLVn1ceTPY+fv4MXqDZIYlPP53iN+dL8Xd0utn2SG57UYdlL5FRAhm/EWHcATZg1RgA==} engines: {node: '>=16'} @@ -5641,10 +5564,6 @@ packages: resolution: {integrity: sha512-SyD4iw6jM4anZaG+ujgVETV4fulF2KHBOW31eavbVN7TNpk2l4aJgwY1YSPK00IKSWsoQuH2TigR446KuT5lqQ==} dev: false - /@cloudflare/workers-types@4.20240512.0: - resolution: {integrity: sha512-o2yTEWg+YK/I1t/Me+dA0oarO0aCbjibp6wSeaw52DSE9tDyKJ7S+Qdyw/XsMrKn4t8kF6f/YOba+9O4MJfW9w==} - dev: true - /@codemirror/autocomplete@6.4.0(@codemirror/language@6.3.2)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)(@lezer/common@1.0.2): resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==} peerDependencies: @@ -6016,6 +5935,7 @@ packages: esbuild: '*' dependencies: esbuild: 0.17.19 + dev: false /@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19): resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} @@ -6025,6 +5945,7 @@ packages: esbuild: 0.17.19 escape-string-regexp: 4.0.0 rollup-plugin-node-polyfills: 0.2.1 + dev: false /@esbuild/aix-ppc64@0.19.11: resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} @@ -6066,6 +5987,7 @@ packages: cpu: [arm64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-arm64@0.17.6: @@ -6135,6 +6057,7 @@ packages: cpu: [arm] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-arm@0.17.6: @@ -6195,6 +6118,7 @@ packages: cpu: [x64] os: [android] requiresBuild: true + dev: false optional: true /@esbuild/android-x64@0.17.6: @@ -6255,6 +6179,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/darwin-arm64@0.17.6: @@ -6315,6 +6240,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: false optional: true /@esbuild/darwin-x64@0.17.6: @@ -6375,6 +6301,7 @@ packages: cpu: [arm64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-arm64@0.17.6: @@ -6435,6 +6362,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: false optional: true /@esbuild/freebsd-x64@0.17.6: @@ -6495,6 +6423,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm64@0.17.6: @@ -6555,6 +6484,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-arm@0.17.6: @@ -6615,6 +6545,7 @@ packages: cpu: [ia32] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ia32@0.17.6: @@ -6684,6 +6615,7 @@ packages: cpu: [loong64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-loong64@0.17.6: @@ -6744,6 +6676,7 @@ packages: cpu: [mips64el] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-mips64el@0.17.6: @@ -6804,6 +6737,7 @@ packages: cpu: [ppc64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-ppc64@0.17.6: @@ -6864,6 +6798,7 @@ packages: cpu: [riscv64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-riscv64@0.17.6: @@ -6924,6 +6859,7 @@ packages: cpu: [s390x] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-s390x@0.17.6: @@ -6984,6 +6920,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: false optional: true /@esbuild/linux-x64@0.17.6: @@ -7044,6 +6981,7 @@ packages: cpu: [x64] os: [netbsd] requiresBuild: true + dev: false optional: true /@esbuild/netbsd-x64@0.17.6: @@ -7112,6 +7050,7 @@ packages: cpu: [x64] os: [openbsd] requiresBuild: true + dev: false optional: true /@esbuild/openbsd-x64@0.17.6: @@ -7172,6 +7111,7 @@ packages: cpu: [x64] os: [sunos] requiresBuild: true + dev: false optional: true /@esbuild/sunos-x64@0.17.6: @@ -7232,6 +7172,7 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-arm64@0.17.6: @@ -7292,6 +7233,7 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-ia32@0.17.6: @@ -7352,6 +7294,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: false optional: true /@esbuild/win32-x64@0.17.6: @@ -17189,6 +17132,7 @@ packages: resolution: {integrity: sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==} dependencies: '@types/node': 18.19.20 + dev: false /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -18806,6 +18750,7 @@ packages: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} dependencies: printable-characters: 1.0.42 + dev: false /asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -19196,6 +19141,7 @@ packages: /blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + dev: false /body-parser@1.20.1: resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} @@ -19483,6 +19429,7 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - supports-color + dev: false /case-anything@2.1.13: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} @@ -20445,6 +20392,7 @@ packages: /data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + dev: false /data-uri-to-buffer@3.0.1: resolution: {integrity: sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==} @@ -21543,6 +21491,7 @@ packages: '@esbuild/win32-arm64': 0.17.19 '@esbuild/win32-ia32': 0.17.19 '@esbuild/win32-x64': 0.17.19 + dev: false /esbuild@0.17.6: resolution: {integrity: sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==} @@ -22297,6 +22246,7 @@ packages: /estree-walker@0.6.1: resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: false /estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -23018,6 +22968,7 @@ packages: dependencies: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + dev: false /get-stream@4.1.0: resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} @@ -24820,6 +24771,7 @@ packages: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} dependencies: sourcemap-codec: 1.4.8 + dev: false /magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} @@ -25432,6 +25384,7 @@ packages: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true + dev: false /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -25455,29 +25408,6 @@ packages: hasBin: true dev: true - /miniflare@3.20240512.0: - resolution: {integrity: sha512-X0PlKR0AROKpxFoJNmRtCMIuJxj+ngEcyTOlEokj2rAQ0TBwUhB4/1uiPvdI6ofW5NugPOD1uomAv+gLjwsLDQ==} - engines: {node: '>=16.13'} - hasBin: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - acorn: 8.12.1 - acorn-walk: 8.3.2 - capnp-ts: 0.7.0 - exit-hook: 2.2.1 - glob-to-regexp: 0.4.1 - stoppable: 1.1.0 - undici: 5.28.4 - workerd: 1.20240512.0 - ws: 8.18.0 - youch: 3.3.3 - zod: 3.23.8 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - /miniflare@3.20240806.0: resolution: {integrity: sha512-jDsXBJOLUVpIQXHsluX3xV0piDxXolTCsxdje2Ex2LTC9PsSoBIkMwvCmnCxe9wpJJCq8rb0UMyeEn3KOF3LOw==} engines: {node: '>=16.13'} @@ -25760,6 +25690,7 @@ packages: /mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true + dev: false /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} @@ -26055,6 +25986,7 @@ packages: /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + dev: false /node-gyp@10.2.0: resolution: {integrity: sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==} @@ -26917,6 +26849,7 @@ packages: /path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + dev: false /path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -27635,6 +27568,7 @@ packages: /printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + dev: false /prism-react-renderer@2.1.0(react@18.3.1): resolution: {integrity: sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==} @@ -29157,16 +29091,19 @@ packages: estree-walker: 0.6.1 magic-string: 0.25.9 rollup-pluginutils: 2.8.2 + dev: false /rollup-plugin-node-polyfills@0.2.1: resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} dependencies: rollup-plugin-inject: 3.0.2 + dev: false /rollup-pluginutils@2.8.2: resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} dependencies: estree-walker: 0.6.1 + dev: false /rollup@3.10.0: resolution: {integrity: sha512-JmRYz44NjC1MjVF2VKxc0M1a97vn+cDxeqWmnwyAF4FvpjK8YFdHpaqvQB+3IxCvX05vJxKZkoMDU8TShhmJVA==} @@ -29346,6 +29283,7 @@ packages: dependencies: '@types/node-forge': 1.3.10 node-forge: 1.3.1 + dev: false /sembear@0.5.2: resolution: {integrity: sha512-Ij1vCAdFgWABd7zTg50Xw1/p0JgESNxuLlneEAsmBrKishA06ulTTL/SHGmNy2Zud7+rKrHTKNI6moJsn1ppAQ==} @@ -29768,6 +29706,7 @@ packages: /sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} deprecated: Please use @jridgewell/sourcemap-codec instead + dev: false /space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -29917,6 +29856,7 @@ packages: dependencies: as-table: 1.0.55 get-source: 2.0.12 + dev: false /standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -29936,6 +29876,7 @@ packages: /stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} + dev: false /stream-buffers@3.0.2: resolution: {integrity: sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==} @@ -32827,19 +32768,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /workerd@1.20240512.0: - resolution: {integrity: sha512-VUBmR1PscAPHEE0OF/G2K7/H1gnr9aDWWZzdkIgWfNKkv8dKFCT75H+GJtUHjfwqz3rYCzaNZmatSXOpLGpF8A==} - engines: {node: '>=16'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240512.0 - '@cloudflare/workerd-darwin-arm64': 1.20240512.0 - '@cloudflare/workerd-linux-64': 1.20240512.0 - '@cloudflare/workerd-linux-arm64': 1.20240512.0 - '@cloudflare/workerd-windows-64': 1.20240512.0 - dev: true - /workerd@1.20240806.0: resolution: {integrity: sha512-yyNtyzTMgVY0sgYijHBONqZFVXsOFGj2jDjS8MF/RbO2ZdGROvs4Hkc/9QnmqFWahE0STxXeJ1yW1yVotdF0UQ==} engines: {node: '>=16'} @@ -32853,39 +32781,6 @@ packages: '@cloudflare/workerd-windows-64': 1.20240806.0 dev: false - /wrangler@3.57.1(@cloudflare/workers-types@4.20240512.0): - resolution: {integrity: sha512-M8YnWUwdrb8AFiRePtVnzlDn02OX4osWvdl8oVh6eyZqqkqXYg7lwlYBr14Qj92pMN4JvMBmDZoukkYHvwpJRg==} - engines: {node: '>=16.17.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20240512.0 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - dependencies: - '@cloudflare/kv-asset-handler': 0.3.2 - '@cloudflare/workers-types': 4.20240512.0 - '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) - '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) - blake3-wasm: 2.1.5 - chokidar: 3.6.0 - esbuild: 0.17.19 - miniflare: 3.20240512.0 - nanoid: 3.3.7 - path-to-regexp: 6.2.1 - resolve: 1.22.8 - resolve.exports: 2.0.2 - selfsigned: 2.4.1 - source-map: 0.6.1 - xxhash-wasm: 1.0.2 - optionalDependencies: - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - /wrangler@3.70.0: resolution: {integrity: sha512-aMtCEXmH02SIxbxOFGGuJ8ZemmG9W+IcNRh5D4qIKgzSxqy0mt9mRoPNPSv1geGB2/8YAyeLGPf+tB4lxz+ssg==} engines: {node: '>=16.17.0'} @@ -33046,6 +32941,7 @@ packages: /xxhash-wasm@1.0.2: resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} + dev: false /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -33160,6 +33056,7 @@ packages: cookie: 0.5.0 mustache: 4.2.0 stacktracey: 2.1.8 + dev: false /yt-dlp-wrap@2.3.12: resolution: {integrity: sha512-P8fJ+6M1YjukyJENCTviNLiZ8mokxprR54ho3DsSKPWDcac489OjRiStGEARJr6un6ETS6goTn4CWl/b/rM3aA==} From 109e8ac64bd794e22432f94eb6d330074400853b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 10 Mar 2025 19:06:11 +0000 Subject: [PATCH 05/95] 2sm needed to be in the Tailwind merge list --- apps/webapp/app/utils/cn.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/webapp/app/utils/cn.ts b/apps/webapp/app/utils/cn.ts index 0c2e8411fa..dc64e862cf 100644 --- a/apps/webapp/app/utils/cn.ts +++ b/apps/webapp/app/utils/cn.ts @@ -9,6 +9,7 @@ const customTwMerge = extendTailwindMerge({ "xxs", "xs", "sm", + "2sm", "md", "lg", "xl", From eb410f9724de803833b34046a3698a5801b61f23 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 10:40:53 +0000 Subject: [PATCH 06/95] =?UTF-8?q?Move=20the=20task=20stream=20route=20(alt?= =?UTF-8?q?hough=20we=20don=E2=80=99t=20actually=20use=20the=20env=20for?= =?UTF-8?q?=20now)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/environments/EnvironmentLabel.tsx | 6 +++--- .../app/presenters/v3/TasksStreamPresenter.server.ts | 7 ++++--- .../route.tsx | 2 +- .../route.tsx | 12 +++++++++--- .../app/routes/_app.orgs.$organizationSlug/route.tsx | 2 -- apps/webapp/app/utils/pathBuilder.ts | 8 ++++++-- 6 files changed, 23 insertions(+), 14 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.tasks.stream => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.stream}/route.tsx (56%) diff --git a/apps/webapp/app/components/environments/EnvironmentLabel.tsx b/apps/webapp/app/components/environments/EnvironmentLabel.tsx index f5e6c8f376..46e7619467 100644 --- a/apps/webapp/app/components/environments/EnvironmentLabel.tsx +++ b/apps/webapp/app/components/environments/EnvironmentLabel.tsx @@ -164,10 +164,10 @@ export function FullEnvironmentCombo({ className?: string; }) { return ( -
+ -
{environmentFullTitle(environment)}
-
+ {environmentFullTitle(environment)} + ); } diff --git a/apps/webapp/app/presenters/v3/TasksStreamPresenter.server.ts b/apps/webapp/app/presenters/v3/TasksStreamPresenter.server.ts index 74bf5c957c..d690b3d083 100644 --- a/apps/webapp/app/presenters/v3/TasksStreamPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TasksStreamPresenter.server.ts @@ -1,8 +1,7 @@ -import { TaskRun, TaskRunAttempt } from "@trigger.dev/database"; +import { type TaskRunAttempt } from "@trigger.dev/database"; import { eventStream } from "remix-utils/sse/server"; -import { PrismaClient, prisma } from "~/db.server"; +import { type PrismaClient, prisma } from "~/db.server"; import { logger } from "~/services/logger.server"; -import { eventRepository } from "~/v3/eventRepository.server"; import { projectPubSub } from "~/v3/services/projectPubSub.server"; type RunWithAttempts = { @@ -26,11 +25,13 @@ export class TasksStreamPresenter { request, organizationSlug, projectSlug, + environmentSlug, userId, }: { request: Request; organizationSlug: string; projectSlug: string; + environmentSlug: string; userId: string; }) { const project = await this.#prismaClient.project.findFirst({ diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index c0a94cf014..a2f756402b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -188,7 +188,7 @@ export default function Page() { //live reload the page when the tasks change const revalidator = useRevalidator(); - const streamedEvents = useEventSource(v3TasksStreamingPath(organization, project), { + const streamedEvents = useEventSource(v3TasksStreamingPath(organization, project, environment), { event: "message", }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.tasks.stream/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.stream/route.tsx similarity index 56% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.tasks.stream/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.stream/route.tsx index e16ba2bac7..194dd0bec3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.tasks.stream/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.stream/route.tsx @@ -1,13 +1,19 @@ import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { TasksStreamPresenter } from "~/presenters/v3/TasksStreamPresenter.server"; import { requireUserId } from "~/services/session.server"; -import { ProjectParamSchema } from "~/utils/pathBuilder"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; export async function loader({ request, params }: LoaderFunctionArgs) { const userId = await requireUserId(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); const presenter = new TasksStreamPresenter(); - return presenter.call({ request, projectSlug: projectParam, organizationSlug, userId }); + return presenter.call({ + request, + projectSlug: projectParam, + environmentSlug: envParam, + organizationSlug, + userId, + }); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx index cd9da35342..9441044e8b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx @@ -111,8 +111,6 @@ export default function Organization() { useTypedLoaderData(); const user = useUser(); - console.log(project); - return ( <>
diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 0b9c679c46..acffce1261 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -147,8 +147,12 @@ export function v3EnvironmentPath( )}/env/${environmentParam(environment)}`; } -export function v3TasksStreamingPath(organization: OrgForPath, project: ProjectForPath) { - return `${v3ProjectPath(organization, project)}/tasks/stream`; +export function v3TasksStreamingPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + return `${v3EnvironmentPath(organization, project, environment)}/tasks/stream`; } export function v3ApiKeysPath(organization: OrgForPath, project: ProjectForPath) { From 692dfc47329da32c36dee8cdd8ae72f677b99747 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 10:50:43 +0000 Subject: [PATCH 07/95] Alerts moved from /v3 --- .../route.tsx | 0 .../route.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new => _app.orgs.$organizationSlug.projects.$projectParam.alerts.new}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts => _app.orgs.$organizationSlug.projects.$projectParam.alerts}/route.tsx (100%) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.alerts.new/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.alerts.new/route.tsx diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.alerts/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.alerts/route.tsx From 725c7722d075245f509b012c672e6a6c6ce1b345 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 10:51:13 +0000 Subject: [PATCH 08/95] API keys page moved from /v3 --- .../route.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.apikeys => _app.orgs.$organizationSlug.projects.$projectParam.apikeys}/route.tsx (100%) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.apikeys/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.apikeys/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.apikeys/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.apikeys/route.tsx From b5835d3244738705fb32c344c9ee3cf1cfef5dc4 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 12:41:01 +0000 Subject: [PATCH 09/95] Concurrency page moved from /v3 --- .../route.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.concurrency => _app.orgs.$organizationSlug.projects.$projectParam.concurrency}/route.tsx (100%) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.concurrency/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.concurrency/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.concurrency/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.concurrency/route.tsx From 1068e5c04ff6d05568118ef67b75cc41c3ca94ab Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 12:41:07 +0000 Subject: [PATCH 10/95] WIP on side menu sections --- .../app/assets/icons/ToggleArrowIcon.tsx | 10 ++ .../app/components/navigation/SideMenu.tsx | 114 ++++++++---------- .../components/navigation/SideMenuItem.tsx | 5 +- .../components/navigation/SideMenuSection.tsx | 98 +++++++++++++++ 4 files changed, 158 insertions(+), 69 deletions(-) create mode 100644 apps/webapp/app/assets/icons/ToggleArrowIcon.tsx create mode 100644 apps/webapp/app/components/navigation/SideMenuSection.tsx diff --git a/apps/webapp/app/assets/icons/ToggleArrowIcon.tsx b/apps/webapp/app/assets/icons/ToggleArrowIcon.tsx new file mode 100644 index 0000000000..7bcb261c4d --- /dev/null +++ b/apps/webapp/app/assets/icons/ToggleArrowIcon.tsx @@ -0,0 +1,10 @@ +export function ToggleArrowIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index ea0e6d5acd..8be9d28bf1 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -75,6 +75,7 @@ import { EnvironmentIcon, FullEnvironmentCombo, } from "../environments/EnvironmentLabel"; +import { SideMenuSection } from "./SideMenuSection"; type SideMenuUser = Pick & { isImpersonating: boolean }; type SideMenuProject = Pick; @@ -144,8 +145,7 @@ export function SideMenu({ environment={environment} /> - <> - + + + + + + + - - -
-
- - - - - - - - - + + +
diff --git a/apps/webapp/app/components/navigation/SideMenuItem.tsx b/apps/webapp/app/components/navigation/SideMenuItem.tsx index 036c325cdd..54c57b388c 100644 --- a/apps/webapp/app/components/navigation/SideMenuItem.tsx +++ b/apps/webapp/app/components/navigation/SideMenuItem.tsx @@ -13,7 +13,6 @@ export function SideMenuItem({ to, badge, target, - subItem = false, }: { icon?: React.ComponentType; activeIconColor?: string; @@ -24,14 +23,13 @@ export function SideMenuItem({ to: string; badge?: string; target?: AnchorHTMLAttributes["target"]; - subItem?: boolean; }) { const pathName = usePathName(); const isActive = pathName === to; return ( diff --git a/apps/webapp/app/components/navigation/SideMenuSection.tsx b/apps/webapp/app/components/navigation/SideMenuSection.tsx new file mode 100644 index 0000000000..c1eaa799f6 --- /dev/null +++ b/apps/webapp/app/components/navigation/SideMenuSection.tsx @@ -0,0 +1,98 @@ +import { AnimatePresence, motion } from "framer-motion"; +import React from "react"; +import { useCallback, useState } from "react"; +import { ToggleArrowIcon } from "~/assets/icons/ToggleArrowIcon"; + +type Props = { + title: string; + initialCollapsed?: boolean; + onCollapseToggle?: (isCollapsed: boolean) => void; + children: React.ReactNode; +}; + +/** A collapsible section for the side menu + * The collapsed state is passed in as a prop, and there's a callback when it's toggled so we can save the state. + */ +export function SideMenuSection({ + title, + initialCollapsed = false, + onCollapseToggle, + children, +}: Props) { + const [isCollapsed, setIsCollapsed] = useState(initialCollapsed); + + const childCount = React.Children.count(children); + + const handleToggle = useCallback(() => { + const newIsCollapsed = !isCollapsed; + setIsCollapsed(newIsCollapsed); + onCollapseToggle?.(newIsCollapsed); + }, [isCollapsed, onCollapseToggle]); + + return ( +
+
+

{title}

+ + + +
+ + + {React.Children.map(children, (child) => ( + + {child} + + ))} + + +
+ ); +} From 722acdfeab51751c217c2e7463dc8d3ef49cb917 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 13:03:18 +0000 Subject: [PATCH 11/95] Improved the accordion animation --- .../components/navigation/SideMenuSection.tsx | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenuSection.tsx b/apps/webapp/app/components/navigation/SideMenuSection.tsx index c1eaa799f6..013469f006 100644 --- a/apps/webapp/app/components/navigation/SideMenuSection.tsx +++ b/apps/webapp/app/components/navigation/SideMenuSection.tsx @@ -21,8 +21,6 @@ export function SideMenuSection({ }: Props) { const [isCollapsed, setIsCollapsed] = useState(initialCollapsed); - const childCount = React.Children.count(children); - const handleToggle = useCallback(() => { const newIsCollapsed = !isCollapsed; setIsCollapsed(newIsCollapsed); @@ -53,44 +51,34 @@ export function SideMenuSection({ expanded: { height: "auto", transition: { - height: { - duration: 0.3, - }, - opacity: { - duration: 0.3, - }, + height: { duration: 0.3, ease: "easeOut" }, }, }, collapsed: { height: 0, transition: { - height: { - duration: 0.2, - }, - opacity: { - duration: 0.2, - }, + height: { duration: 0.2, ease: "easeIn" }, }, }, }} style={{ overflow: "hidden" }} > - {React.Children.map(children, (child) => ( - - {child} - - ))} + + {children} +
From 651a6ae24e7cd7deaa07a6ec7575fe37138a9b91 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 13:06:00 +0000 Subject: [PATCH 12/95] Moved schedules from /v3 --- .../route.tsx | 0 .../route.tsx | 0 .../route.tsx | 22 +++++++++---------- 3 files changed, 11 insertions(+), 11 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules.$scheduleParam => _app.orgs.$organizationSlug.projects.$projectParam.schedules.$scheduleParam}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules.edit.$scheduleParam => _app.orgs.$organizationSlug.projects.$projectParam.schedules.edit.$scheduleParam}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules => _app.orgs.$organizationSlug.projects.$projectParam.schedules}/route.tsx (98%) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.schedules.$scheduleParam/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules.$scheduleParam/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.schedules.$scheduleParam/route.tsx diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules.edit.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.schedules.edit.$scheduleParam/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules.edit.$scheduleParam/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.schedules.edit.$scheduleParam/route.tsx diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.schedules/route.tsx similarity index 98% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.schedules/route.tsx index 97460665f4..9bdeb30c2c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.schedules/route.tsx @@ -1,7 +1,8 @@ -import { ClockIcon, LockOpenIcon, PlusIcon, RectangleGroupIcon } from "@heroicons/react/20/solid"; +import { ClockIcon, PlusIcon, RectangleGroupIcon } from "@heroicons/react/20/solid"; +import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { BookOpenIcon } from "@heroicons/react/24/solid"; -import { MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { type MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { Feedback } from "~/components/Feedback"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; @@ -10,11 +11,6 @@ import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; -import { - ScheduleTypeCombo, - ScheduleTypeIcon, - scheduleTypeName, -} from "~/components/runs/v3/ScheduleType"; import { Dialog, DialogContent, @@ -43,8 +39,14 @@ import { TableHeaderCell, TableRow, } from "~/components/primitives/Table"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { EnabledStatus } from "~/components/runs/v3/EnabledStatus"; import { ScheduleFilters, ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters"; +import { + ScheduleTypeCombo, + ScheduleTypeIcon, + scheduleTypeName, +} from "~/components/runs/v3/ScheduleType"; import { useOrganization } from "~/hooks/useOrganizations"; import { usePathName } from "~/hooks/usePathName"; import { useProject } from "~/hooks/useProject"; @@ -55,6 +57,7 @@ import { ScheduleListPresenter, } from "~/presenters/v3/ScheduleListPresenter.server"; import { requireUserId } from "~/services/session.server"; +import { cn } from "~/utils/cn"; import { ProjectParamSchema, docsPath, @@ -63,9 +66,6 @@ import { v3SchedulePath, } from "~/utils/pathBuilder"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; -import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; -import { SimpleTooltip } from "~/components/primitives/Tooltip"; -import { cn } from "~/utils/cn"; export const meta: MetaFunction = () => { return [ From 3e1c6c0e2f241f93cf5320ac479f4dc076de4739 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 13:45:11 +0000 Subject: [PATCH 13/95] More pages moved --- .../app/components/navigation/SideMenu.tsx | 8 +- .../components/navigation/SideMenuSection.tsx | 3 +- .../app/components/runs/v3/TaskRunsTable.tsx | 30 ++++---- .../route.tsx | 0 .../route.tsx | 3 +- .../route.tsx | 0 .../route.tsx | 41 ++++++---- .../route.tsx | 0 .../route.tsx | 0 .../projects.v3.$projectRef.runs.$runParam.ts | 17 ++++- apps/webapp/app/utils/pathBuilder.ts | 74 ++++++++----------- 11 files changed, 91 insertions(+), 85 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.v3.billing => _app.orgs.$organizationSlug.billing}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.projectParam._index => _app.orgs.$organizationSlug.projects.$projectParam._index}/route.tsx (55%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.batches => _app.orgs.$organizationSlug.projects.$projectParam.batches}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index}/route.tsx (94%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.runs => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.v3.usage => _app.orgs.$organizationSlug.usage}/route.tsx (100%) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 8be9d28bf1..bbd4bd06fd 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -150,21 +150,21 @@ export function SideMenu({ name="Tasks" icon={TaskIcon} activeIconColor="text-blue-500" - to={v3ProjectPath(organization, project)} + to={v3EnvironmentPath(organization, project, environment)} data-action="tasks" /> @@ -174,7 +174,7 @@ export function SideMenu({ name="Runs" icon={RunsIcon} activeIconColor="text-teal-500" - to={v3RunsPath(organization, project)} + to={v3RunsPath(organization, project, environment)} /> diff --git a/apps/webapp/app/components/navigation/SideMenuSection.tsx b/apps/webapp/app/components/navigation/SideMenuSection.tsx index 013469f006..9743a991e2 100644 --- a/apps/webapp/app/components/navigation/SideMenuSection.tsx +++ b/apps/webapp/app/components/navigation/SideMenuSection.tsx @@ -1,6 +1,5 @@ import { AnimatePresence, motion } from "framer-motion"; -import React from "react"; -import { useCallback, useState } from "react"; +import React, { useCallback, useState } from "react"; import { ToggleArrowIcon } from "~/assets/icons/ToggleArrowIcon"; type Props = { diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index e235a7ecf5..8c9a0f14de 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -23,10 +23,13 @@ import { useFeatures } from "~/hooks/useFeatures"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useUser } from "~/hooks/useUser"; -import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server"; +import { + type RunListAppliedFilters, + type RunListItem, +} from "~/presenters/v3/RunListPresenter.server"; import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter"; import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder"; -import { EnvironmentLabel } from "../../environments/EnvironmentLabel"; +import { EnvironmentLabel, FullEnvironmentCombo } from "../../environments/EnvironmentLabel"; import { DateTime } from "../../primitives/DateTime"; import { Paragraph } from "../../primitives/Paragraph"; import { Spinner } from "../../primitives/Spinner"; @@ -39,7 +42,7 @@ import { TableHeader, TableHeaderCell, TableRow, - TableVariant, + type TableVariant, } from "../../primitives/Table"; import { CancelRunDialog } from "./CancelRunDialog"; import { LiveTimer } from "./LiveTimer"; @@ -50,6 +53,7 @@ import { filterableTaskRunStatuses, TaskRunStatusCombo, } from "./TaskRunStatus"; +import { useEnvironment } from "~/hooks/useEnvironment"; type RunsTableProps = { total: number; @@ -74,6 +78,7 @@ export function TaskRunsTable({ const user = useUser(); const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const checkboxes = useRef<(HTMLInputElement | null)[]>([]); const { selectedItems, has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection); const { isManagedCloud } = useFeatures(); @@ -286,7 +291,9 @@ export function TaskRunsTable({ ) : ( runs.map((run, index) => { - const path = v3RunSpanPath(organization, project, run, { spanId: run.spanId }); + const path = v3RunSpanPath(organization, project, environment, run, { + spanId: run.spanId, + }); return ( {allowSelection && ( @@ -535,19 +542,17 @@ function NoRuns({ title }: { title: string }) { function BlankState({ isLoading, filters }: Pick) { const organization = useOrganization(); const project = useProject(); - const envs = useEnvironments(); + const environment = useEnvironment(); if (isLoading) return ; const { environments, tasks, from, to, ...otherFilters } = filters; if ( - filters.environments.length === 1 && filters.tasks.length === 1 && filters.from === undefined && filters.to === undefined && Object.values(otherFilters).every((filterArray) => filterArray.length === 0) ) { - const environment = envs?.find((env) => env.id === filters.environments[0]); return (
@@ -556,18 +561,13 @@ function BlankState({ isLoading, filters }: Pick {" "} - in{" "} - + in ) : null}
Run a test diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.v3.billing/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx similarity index 55% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.projectParam._index/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx index ef042b350d..0e8a393707 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx @@ -1,6 +1,5 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { requireUserId } from "~/services/session.server"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { - throw new Response("Not Implemented", { status: 501 }); + throw new Response("Not found", { status: 404, statusText: "Select an environment" }); }; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.batches/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.batches/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.batches/route.tsx diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx similarity index 94% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index 74638b6034..20b27d8436 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -1,7 +1,7 @@ import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid"; import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid"; -import { Form, MetaFunction, useNavigation } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { Form, type MetaFunction, useNavigation } from "@remix-run/react"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { IconCircleX } from "@tabler/icons-react"; import { AnimatePresence, motion } from "framer-motion"; import { ListChecks, ListX } from "lucide-react"; @@ -46,12 +46,15 @@ import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { docsPath, + EnvironmentParamSchema, ProjectParamSchema, v3ProjectPath, v3RunsPath, v3TestPath, } from "~/utils/pathBuilder"; import { ListPagination } from "../../components/ListPagination"; +import { prisma } from "~/db.server"; +import { useEnvironment } from "~/hooks/useEnvironment"; export const meta: MetaFunction = () => { return [ @@ -63,7 +66,7 @@ export const meta: MetaFunction = () => { export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { projectParam, organizationSlug } = ProjectParamSchema.parse(params); + const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params); const url = new URL(request.url); @@ -74,11 +77,26 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { rootOnlyValue = await getRootOnlyFilterPreference(request); } + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) { + throw new Error("Project not found"); + } + + const environment = await prisma.runtimeEnvironment.findFirst({ + where: { + projectId: project.id, + slug: envParam, + }, + }); + if (!environment) { + throw new Error("Environment not found"); + } + const s = { cursor: url.searchParams.get("cursor") ?? undefined, direction: url.searchParams.get("direction") ?? undefined, statuses: url.searchParams.getAll("statuses"), - environments: url.searchParams.getAll("environments"), + environments: [environment.id], tasks: url.searchParams.getAll("tasks"), period: url.searchParams.get("period") ?? undefined, bulkId: url.searchParams.get("bulkId") ?? undefined, @@ -108,12 +126,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { scheduleId, } = TaskRunListSearchFilters.parse(s); - const project = await findProjectBySlug(organizationSlug, projectParam, userId); - - if (!project) { - throw new Error("Project not found"); - } - const presenter = new RunListPresenter(); const list = presenter.call({ userId, @@ -313,7 +325,8 @@ function CancelRuns({ onOpen }: { onOpen: (open: boolean) => void }) { const organization = useOrganization(); const project = useProject(); - const failedRedirect = v3RunsPath(organization, project); + const environment = useEnvironment(); + const failedRedirect = v3RunsPath(organization, project, environment); const formAction = `/resources/taskruns/bulk/cancel`; @@ -370,7 +383,8 @@ function ReplayRuns({ onOpen }: { onOpen: (open: boolean) => void }) { const organization = useOrganization(); const project = useProject(); - const failedRedirect = v3RunsPath(organization, project); + const environment = useEnvironment(); + const failedRedirect = v3RunsPath(organization, project, environment); const formAction = `/resources/taskruns/bulk/replay`; @@ -448,6 +462,7 @@ function CreateFirstTaskInstructions() { function RunTaskInstructions() { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); return ( How to run your tasks @@ -458,7 +473,7 @@ function RunTaskInstructions() { page. Date: Tue, 11 Mar 2025 14:46:38 +0000 Subject: [PATCH 14/95] Move pages working --- .../app/components/navigation/SideMenu.tsx | 20 +++++++---- .../app/components/runs/v3/RunFilters.tsx | 25 +++---------- .../app/models/runtimeEnvironment.server.ts | 22 ++++++++++++ .../route.tsx | 14 ++++---- .../route.tsx | 35 +++++++++++-------- .../route.tsx | 10 ++---- .../route.tsx | 0 .../route.tsx | 22 ++++++++---- 8 files changed, 84 insertions(+), 64 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.batches => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches}/route.tsx (93%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.stream => _app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam.stream}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam => _app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam}/route.tsx (98%) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index bbd4bd06fd..87ede3976e 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -153,13 +153,6 @@ export function SideMenu({ to={v3EnvironmentPath(organization, project, environment)} data-action="tasks" /> - + @@ -176,6 +176,12 @@ export function SideMenu({ activeIconColor="text-teal-500" to={v3RunsPath(organization, project, environment)} /> + diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 8b8a3eecec..04b4c01cc2 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -9,16 +9,10 @@ import { TrashIcon, } from "@heroicons/react/20/solid"; import { Form, useFetcher } from "@remix-run/react"; -import type { - BulkActionType, - RuntimeEnvironment, - TaskRunStatus, - TaskTriggerSource, -} from "@trigger.dev/database"; +import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database"; import { ListChecks, ListFilterIcon } from "lucide-react"; import { matchSorter } from "match-sorter"; -import type { ReactNode } from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { z } from "zod"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; @@ -53,12 +47,10 @@ import { Button } from "../../primitives/Buttons"; import { BulkActionStatusCombo } from "./BulkAction"; import { AppliedCustomDateRangeFilter, - AppliedEnvironmentFilter, AppliedPeriodFilter, appliedSummary, CreatedAtDropdown, CustomDateRangeDropdown, - EnvironmentsDropdown, FilterMenuProvider, } from "./SharedFilters"; import { @@ -107,12 +99,7 @@ export const TaskRunListSearchFilters = z.object({ export type TaskRunListSearchFilters = z.infer; -type DisplayableEnvironment = Pick & { - userName?: string; -}; - type RunFiltersProps = { - possibleEnvironments: DisplayableEnvironment[]; possibleTasks: { slug: string; triggerSource: TaskTriggerSource }[]; bulkActions: { id: string; @@ -128,7 +115,6 @@ export function RunsFilters(props: RunFiltersProps) { const searchParams = new URLSearchParams(location.search); const hasFilters = searchParams.has("statuses") || - searchParams.has("environments") || searchParams.has("tasks") || searchParams.has("period") || searchParams.has("bulkId") || @@ -168,7 +154,6 @@ const filterTypes = [
), }, - { name: "environments", title: "Environment", icon: }, { name: "tasks", title: "Tasks", icon: }, { name: "tags", title: "Tags", icon: }, { name: "created", title: "Created", icon: }, @@ -217,11 +202,10 @@ function FilterMenu(props: RunFiltersProps) { ); } -function AppliedFilters({ possibleEnvironments, possibleTasks, bulkActions }: RunFiltersProps) { +function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) { return ( <> - @@ -248,8 +232,7 @@ function Menu(props: MenuProps) { return ; case "statuses": return props.setFilterType(undefined)} {...props} />; - case "environments": - return props.setFilterType(undefined)} {...props} />; + case "tasks": return props.setFilterType(undefined)} {...props} />; case "created": diff --git a/apps/webapp/app/models/runtimeEnvironment.server.ts b/apps/webapp/app/models/runtimeEnvironment.server.ts index 57ff83373f..c70d7fdb43 100644 --- a/apps/webapp/app/models/runtimeEnvironment.server.ts +++ b/apps/webapp/app/models/runtimeEnvironment.server.ts @@ -69,6 +69,28 @@ export async function findEnvironmentById(id: string): Promise {filteredItems.length > 0 ? ( filteredItems.map((task) => { - const path = v3RunsPath(organization, project, { + const path = v3RunsPath(organization, project, environment, { tasks: [task.slug], }); - const testPath = v3TestTaskPath( - organization, - project, - { taskIdentifier: task.slug }, - environment - ); + const testPath = v3TestTaskPath(organization, project, environment, { + taskIdentifier: task.slug, + }); return ( @@ -626,6 +623,7 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps) function HelpfulInfoHasTasks({ onClose }: { onClose: () => void }) { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const [isVideoDialogOpen, setIsVideoDialogOpen] = useState(false); return ( @@ -647,7 +645,7 @@ function HelpfulInfoHasTasks({ onClose }: { onClose: () => void }) {
} /> diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx similarity index 93% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.batches/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index 239747953e..f3728cacca 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -4,8 +4,8 @@ import { ExclamationCircleIcon, } from "@heroicons/react/20/solid"; import { BookOpenIcon } from "@heroicons/react/24/solid"; -import { MetaFunction, useLocation, useNavigation } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { type MetaFunction, useLocation, useNavigation } from "@remix-run/react"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { ListPagination } from "~/components/ListPagination"; @@ -38,17 +38,19 @@ import { } from "~/components/runs/v3/BatchStatus"; import { CheckBatchCompletionDialog } from "~/components/runs/v3/CheckBatchCompletionDialog"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithErrorMessage } from "~/models/message.server"; import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { - BatchList, - BatchListItem, + type BatchList, + type BatchListItem, BatchListPresenter, } from "~/presenters/v3/BatchListPresenter.server"; import { requireUserId } from "~/services/session.server"; -import { docsPath, ProjectParamSchema, v3BatchRunsPath } from "~/utils/pathBuilder"; +import { docsPath, EnvironmentParamSchema, v3BatchRunsPath } from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { return [ @@ -60,13 +62,23 @@ export const meta: MetaFunction = () => { export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { projectParam, organizationSlug } = ProjectParamSchema.parse(params); + const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) { + return redirectWithErrorMessage("/", request, "Project not found"); + } + + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) { + throw new Error("Environment not found"); + } const url = new URL(request.url); const s = { cursor: url.searchParams.get("cursor") ?? undefined, direction: url.searchParams.get("direction") ?? undefined, - environments: url.searchParams.getAll("environments"), + environments: [environment.id], statuses: url.searchParams.getAll("statuses"), period: url.searchParams.get("period") ?? undefined, from: url.searchParams.get("from") ?? undefined, @@ -75,12 +87,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; const filters = BatchListFilters.parse(s); - const project = await findProjectBySlug(organizationSlug, projectParam, userId); - - if (!project) { - return redirectWithErrorMessage("/", request, "Project not found"); - } - const presenter = new BatchListPresenter(); const list = await presenter.call({ userId, @@ -138,6 +144,7 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) { const isLoading = navigation.state !== "idle"; const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); return ( @@ -192,7 +199,7 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) { ) : ( batches.map((batch, index) => { - const path = v3BatchRunsPath(organization, project, batch); + const path = v3BatchRunsPath(organization, project, environment, batch); return ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index 20b27d8436..fbc2c00797 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -55,6 +55,7 @@ import { import { ListPagination } from "../../components/ListPagination"; import { prisma } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; export const meta: MetaFunction = () => { return [ @@ -82,12 +83,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { throw new Error("Project not found"); } - const environment = await prisma.runtimeEnvironment.findFirst({ - where: { - projectId: project.id, - slug: envParam, - }, - }); + const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Error("Environment not found"); } @@ -167,7 +163,6 @@ export default function Page() { const { data, rootOnlyDefault } = useTypedLoaderData(); const navigation = useNavigation(); const isLoading = navigation.state !== "idle"; - const project = useProject(); return ( <> @@ -222,7 +217,6 @@ export default function Page() { >
{ if (streamedEvents !== null) { revalidator.revalidate(); @@ -1049,6 +1057,7 @@ function ShowParentLink({ const [mouseOver, setMouseOver] = useState(false); const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const { spanParam } = useParams(); const span = spanId ? spanId : spanParam; @@ -1061,12 +1070,13 @@ function ShowParentLink({ ? v3RunSpanPath( organization, project, + environment, { friendlyId: runFriendlyId, }, { spanId: span } ) - : v3RunPath(organization, project, { + : v3RunPath(organization, project, environment, { friendlyId: runFriendlyId, }) } From dfa4443c995a41c58a0da57ee574ae03016732a4 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 15:07:03 +0000 Subject: [PATCH 15/95] Run page working --- .../route.tsx | 0 .../route.tsx | 16 +++--- .../route.tsx | 53 +++++++++++++------ apps/webapp/app/utils/pathBuilder.ts | 2 +- 4 files changed, 46 insertions(+), 25 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam.stream => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.stream}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam}/route.tsx (98%) rename apps/webapp/app/routes/{resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam => resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam}/route.tsx (95%) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam.stream/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.stream/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam.stream/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.stream/route.tsx diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx similarity index 98% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index ba10338dfd..0085f947c4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -12,14 +12,14 @@ import { StopCircleIcon, } from "@heroicons/react/20/solid"; import { useLoaderData, useParams, useRevalidator } from "@remix-run/react"; -import { LoaderFunctionArgs, SerializeFrom, json } from "@remix-run/server-runtime"; -import { Virtualizer } from "@tanstack/react-virtual"; +import { type LoaderFunctionArgs, type SerializeFrom, json } from "@remix-run/server-runtime"; +import { type Virtualizer } from "@tanstack/react-virtual"; import { formatDurationMilliseconds, millisecondsToNanoseconds, nanosecondsToMilliseconds, } from "@trigger.dev/core/v3"; -import { RuntimeEnvironmentType } from "@trigger.dev/database"; +import { type RuntimeEnvironmentType } from "@trigger.dev/database"; import { motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; @@ -64,14 +64,15 @@ import { import { TaskRunStatusIcon, runStatusClassNameColor } from "~/components/runs/v3/TaskRunStatus"; import { env } from "~/env.server"; import { useDebounce } from "~/hooks/useDebounce"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useEventSource } from "~/hooks/useEventSource"; import { useInitialDimensions } from "~/hooks/useInitialDimensions"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useReplaceSearchParams } from "~/hooks/useReplaceSearchParams"; -import { Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { type Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys"; import { useHasAdminAccess, useUser } from "~/hooks/useUser"; -import { Run, RunPresenter } from "~/presenters/v3/RunPresenter.server"; +import { RunPresenter } from "~/presenters/v3/RunPresenter.server"; import { getImpersonationId } from "~/services/impersonation.server"; import { getResizableSnapshot } from "~/services/resizablePanel.server"; import { requireUserId } from "~/services/session.server"; @@ -87,8 +88,7 @@ import { v3RunsPath, } from "~/utils/pathBuilder"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; -import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route"; -import { useEnvironment } from "~/hooks/useEnvironment"; +import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route"; const resizableSettings = { parent: { @@ -125,7 +125,7 @@ type TraceEvent = NonNullable["trace"]>["events"][0 export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const impersonationId = await getImpersonationId(request); - const { projectParam, organizationSlug, runParam } = v3RunParamsSchema.parse(params); + const { projectParam, organizationSlug, envParam, runParam } = v3RunParamsSchema.parse(params); const presenter = new RunPresenter(); const result = await presenter.call({ diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx similarity index 95% rename from apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx rename to apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index 805cf89b96..0fa2669bb8 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -5,10 +5,10 @@ import { QueueListIcon, } from "@heroicons/react/20/solid"; import { Link } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDurationMilliseconds, - TaskRunError, + type TaskRunError, taskRunErrorEnhancer, } from "@trigger.dev/core/v3"; import { useEffect } from "react"; @@ -16,7 +16,7 @@ import { typedjson, useTypedFetcher } from "remix-typedjson"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { AdminDebugRun } from "~/components/admin/debugRun"; import { CodeBlock } from "~/components/code/CodeBlock"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Feedback } from "~/components/Feedback"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; @@ -53,7 +53,7 @@ import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { useHasAdminAccess } from "~/hooks/useUser"; import { redirectWithErrorMessage } from "~/models/message.server"; -import { Span, SpanPresenter, SpanRun } from "~/presenters/v3/SpanPresenter.server"; +import { type Span, SpanPresenter, type SpanRun } from "~/presenters/v3/SpanPresenter.server"; import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; @@ -68,15 +68,16 @@ import { v3SchedulePath, v3SpanParamsSchema, } from "~/utils/pathBuilder"; -import { SpanLink } from "~/v3/eventRepository.server"; import { CompleteWaitpointForm, ForceTimeout, } from "../resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete/route"; +import { useEnvironment } from "~/hooks/useEnvironment"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { projectParam, organizationSlug, runParam, spanParam } = v3SpanParamsSchema.parse(params); + const { projectParam, organizationSlug, envParam, runParam, spanParam } = + v3SpanParamsSchema.parse(params); const presenter = new SpanPresenter(); @@ -99,7 +100,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { error, }); return redirectWithErrorMessage( - v3RunPath({ slug: organizationSlug }, { slug: projectParam }, { friendlyId: runParam }), + v3RunPath( + { slug: organizationSlug }, + { slug: projectParam }, + { slug: envParam }, + { friendlyId: runParam } + ), request, `Event not found.` ); @@ -117,14 +123,15 @@ export function SpanView({ }) { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const fetcher = useTypedFetcher(); useEffect(() => { if (spanId === undefined) return; fetcher.load( - `/resources/orgs/${organization.slug}/projects/v3/${project.slug}/runs/${runParam}/spans/${spanId}` + `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/runs/${runParam}/spans/${spanId}` ); - }, [organization.slug, project.slug, runParam, spanId]); + }, [organization.slug, project.slug, environment.slug, runParam, spanId]); if (spanId === undefined) { return null; @@ -178,6 +185,7 @@ function SpanBody({ }) { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const { value, replace } = useSearchParams(); let tab = value("tab"); @@ -257,7 +265,11 @@ function SpanBody({ + {span.taskSlug} } @@ -310,12 +322,11 @@ function RunBody({ }) { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const isAdmin = useHasAdminAccess(); const { value, replace } = useSearchParams(); const tab = value("tab"); - const environment = project.environments.find((e) => e.id === run.environmentId); - return (
@@ -403,7 +414,9 @@ function RunBody({ {run.taskIdentifier} @@ -421,6 +434,7 @@ function RunBody({ to={v3RunSpanPath( organization, project, + environment, { friendlyId: run.relationships.root.friendlyId, }, @@ -444,6 +458,7 @@ function RunBody({ to={v3RunSpanPath( organization, project, + environment, { friendlyId: run.relationships.root.friendlyId, }, @@ -466,6 +481,7 @@ function RunBody({ to={v3RunSpanPath( organization, project, + environment, { friendlyId: run.relationships.parent.friendlyId, }, @@ -490,7 +506,7 @@ function RunBody({ + {run.batch.friendlyId} } @@ -573,7 +589,7 @@ function RunBody({ Environment - + )} @@ -622,7 +638,9 @@ function RunBody({ + } @@ -720,6 +738,7 @@ function RunBody({ to={v3RunSpanPath( organization, project, + environment, { friendlyId: run.friendlyId }, { spanId: run.spanId } )} @@ -866,6 +885,7 @@ function SpanEntity({ span }: { span: Span }) { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); if (!span.entity) { //normal span @@ -927,6 +947,7 @@ function SpanEntity({ span }: { span: Span }) { const path = v3RunSpanPath( organization, project, + environment, { friendlyId: run.friendlyId }, { spanId: run.spanId } ); diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 3a740764d8..84711901cc 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -32,7 +32,7 @@ export const v3TaskParamsSchema = ProjectParamSchema.extend({ taskParam: z.string(), }); -export const v3RunParamsSchema = ProjectParamSchema.extend({ +export const v3RunParamsSchema = EnvironmentParamSchema.extend({ runParam: z.string(), }); From 804b8cdcb259efdc52dedd702f38cb4332d45d10 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Tue, 11 Mar 2025 18:18:00 +0000 Subject: [PATCH 16/95] Schedules working --- .../app/components/navigation/SideMenu.tsx | 45 ++++++++--------- .../components/runs/v3/ScheduleFilters.tsx | 49 +------------------ .../app/presenters/v3/TestPresenter.server.ts | 2 +- .../route.tsx | 0 .../route.tsx | 7 ++- .../route.tsx | 19 +++---- .../route.tsx | 17 +++++-- .../route.tsx | 13 +++-- .../route.tsx | 4 +- .../route.tsx | 37 ++++++++------ .../route.tsx | 0 .../route.tsx | 0 .../route.tsx | 0 .../route.tsx | 0 .../route.tsx | 0 ...rojectParam.alerts.new.connect-to-slack.ts | 8 +-- .../route.tsx | 20 +++++--- apps/webapp/app/utils/pathBuilder.ts | 49 ++++++++++++++----- 18 files changed, 134 insertions(+), 136 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments.$deploymentParam => _app.orgs.$organizationSlug.projects.$projectParam.deployments.$deploymentParam}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments => _app.orgs.$organizationSlug.projects.$projectParam.deployments}/route.tsx (98%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.schedules.$scheduleParam => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam}/route.tsx (96%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.schedules.edit.$scheduleParam => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam}/route.tsx (72%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.schedules.new => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new}/route.tsx (90%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.schedules => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules}/route.tsx (95%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.environment-variables.new => _app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.environment-variables => _app.orgs.$organizationSlug.projects.$projectParam.environment-variables}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.settings => _app.orgs.$organizationSlug.projects.$projectParam.settings}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.test.tasks.$taskParam => _app.orgs.$organizationSlug.projects.$projectParam.test.tasks.$taskParam}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.test => _app.orgs.$organizationSlug.projects.$projectParam.test}/route.tsx (100%) rename apps/webapp/app/routes/{resources.orgs.$organizationSlug.projects.$projectParam.schedules.new => resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new}/route.tsx (96%) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 87ede3976e..597f865627 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -17,7 +17,7 @@ import { Squares2X2Icon, } from "@heroicons/react/20/solid"; import { UserGroupIcon, UserPlusIcon } from "@heroicons/react/24/solid"; -import { useNavigation } from "@remix-run/react"; +import { useMatches, useNavigation } from "@remix-run/react"; import { Fragment, type ReactNode, useEffect, useRef, useState } from "react"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; @@ -167,6 +167,20 @@ export function SideMenu({ to={v3TestPath(organization, project, environment)} data-action="test" /> + + @@ -182,16 +196,16 @@ export function SideMenu({ statuses: ["COMPLETED_WITH_ERRORS"], })} /> + - - - + ; const All = "ALL"; -type DisplayableEnvironment = Pick & { - userName?: string; -}; - type ScheduleFiltersProps = { - possibleEnvironments: DisplayableEnvironment[]; possibleTasks: string[]; }; -export function ScheduleFilters({ possibleEnvironments, possibleTasks }: ScheduleFiltersProps) { +export function ScheduleFilters({ possibleTasks }: ScheduleFiltersProps) { const navigate = useNavigate(); const location = useOptimisticLocation(); const searchParams = new URLSearchParams(location.search); @@ -54,8 +48,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul Object.fromEntries(searchParams.entries()) ); - const hasFilters = - searchParams.has("tasks") || searchParams.has("environments") || searchParams.has("search"); + const hasFilters = searchParams.has("tasks") || searchParams.has("search"); const handleFilterChange = useCallback((filterType: string, value: string | undefined) => { if (value) { @@ -71,10 +64,6 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul handleFilterChange("tasks", value === "ALL" ? undefined : value); }, []); - const handleEnvironmentChange = useCallback((value: string | typeof All) => { - handleFilterChange("environments", value === "ALL" ? undefined : value); - }, []); - const handleTypeChange = useCallback((value: string | typeof All) => { handleFilterChange("type", value === "ALL" ? undefined : value); }, []); @@ -87,7 +76,6 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul searchParams.delete("page"); searchParams.delete("enabled"); searchParams.delete("tasks"); - searchParams.delete("environments"); searchParams.delete("search"); navigate(`${location.pathname}?${searchParams.toString()}`); }, []); @@ -127,39 +115,6 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul - - - -
- ) : ( + ) : environment.type === "DEVELOPMENT" ? ( - + + + ) : ( + + )}
@@ -428,45 +427,6 @@ export default function Page() { ); } -function CreateTaskInstructions() { - return ( - -
-
- Get setup in 3 minutes -
- - I'm stuck! - - } - defaultValue="help" - /> -
-
- - - - - You'll notice a new folder in your project called{" "} - trigger. We've added a very simple example task - in here to help you get started. - - - - - - - - - This page will automatically refresh. - -
-
- ); -} - function UserHasNoTasks() { const [open, setOpen] = useState(false); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 4ef306f1df..13d2494f18 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -24,6 +24,7 @@ import SegmentedControl from "~/components/primitives/SegmentedControl"; import { Select, SelectItem } from "~/components/primitives/Select"; import { InfoIconTooltip } from "~/components/primitives/Tooltip"; import { env } from "~/env.server"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithSuccessMessage } from "~/models/message.server"; @@ -31,7 +32,11 @@ import { findProjectBySlug } from "~/models/project.server"; import { NewAlertChannelPresenter } from "~/presenters/v3/NewAlertChannelPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import { ProjectParamSchema, v3ProjectAlertsPath } from "~/utils/pathBuilder"; +import { + EnvironmentParamSchema, + ProjectParamSchema, + v3ProjectAlertsPath, +} from "~/utils/pathBuilder"; import { type CreateAlertChannelOptions, CreateAlertChannelService, @@ -163,7 +168,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); if (request.method.toUpperCase() !== "POST") { return { status: 405, body: "Method Not Allowed" }; @@ -197,7 +202,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { } return redirectWithSuccessMessage( - v3ProjectAlertsPath({ slug: organizationSlug }, { slug: projectParam }), + v3ProjectAlertsPath({ slug: organizationSlug }, { slug: projectParam }, { slug: envParam }), request, `Created ${alertChannel.name} alert` ); @@ -211,6 +216,7 @@ export default function Page() { const navigate = useNavigate(); const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const [currentAlertChannel, setCurrentAlertChannel] = useState(option ?? "EMAIL"); const [selectedSlackChannelValue, setSelectedSlackChannelValue] = useState(); @@ -251,7 +257,7 @@ export default function Page() { open={isOpen} onOpenChange={(o) => { if (!o) { - navigate(v3ProjectAlertsPath(organization, project)); + navigate(v3ProjectAlertsPath(organization, project, environment)); } }} > @@ -436,7 +442,7 @@ export default function Page() { } cancelButton={ Cancel diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx index f033c3755a..4da9f53a07 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx @@ -48,6 +48,7 @@ import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { AlertChannelListPresenter, type AlertChannelListPresenterRecord, @@ -82,8 +83,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }); } + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) { + throw new Response(undefined, { + status: 404, + statusText: "Environment not found", + }); + } + const presenter = new AlertChannelListPresenter(); - const data = await presenter.call(project.id); + const data = await presenter.call(project.id, environment.type); return typedjson(data); }; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index f3728cacca..32d364eb19 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -8,10 +8,11 @@ import { type MetaFunction, useLocation, useNavigation } from "@remix-run/react" import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { BatchesNone } from "~/components/BlankStatePanels"; import { ListPagination } from "~/components/ListPagination"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; @@ -100,7 +101,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { export default function Page() { const { batches, hasFilters, filters, pagination } = useTypedLoaderData(); - const project = useProject(); return ( @@ -108,7 +108,6 @@ export default function Page() { - -
-
- -
- + {!hasFilters && batches.length === 0 ? ( + + + + ) : ( +
+
+ +
+ +
-
- -
+ +
+ )} ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index c223dc0e7a..aac0471133 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -68,6 +68,7 @@ import { import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { useEnvironment } from "~/hooks/useEnvironment"; +import { SchedulesNoneAttached, SchedulesNoPossibleTaskPanel } from "~/components/BlankStatePanels"; export const meta: MetaFunction = () => { return [ @@ -208,9 +209,13 @@ export default function Page() {
{possibleTasks.length === 0 ? ( - + + + ) : schedules.length === 0 && !hasFilters ? ( - + + + ) : ( <>
@@ -318,72 +323,6 @@ export default function Page() { ); } -function CreateScheduledTaskInstructions() { - return ( - - - - You have no scheduled tasks in your project. Before you can schedule a task you need to - create a schedules.task. - - - View the docs - - - - ); -} - -function AttachYourFirstScheduleInstructions() { - const organization = useOrganization(); - const project = useProject(); - const environment = useEnvironment(); - const location = useLocation(); - - return ( - - - - Scheduled tasks will only run automatically if you connect a schedule to them, you can do - this in the dashboard or using the SDK. - -
- - Use the dashboard - - - Use the SDK - -
-
-
- ); -} - function SchedulesTable({ schedules, hasFilters, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new/route.tsx index d5ec7d6681..fe530a4cc9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new/route.tsx @@ -16,6 +16,8 @@ import { z } from "zod"; import { environmentTextClassName, environmentTitle, + FullEnvironmentCombo, + FullEnvironmentLabel, } from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; @@ -217,16 +219,7 @@ export default function Page() { value={environment.id} name="environmentIds" type="radio" - label={ - - {environmentTitle(environment)} - - } + label={} variant="button" /> ))} @@ -239,9 +232,10 @@ export default function Page() { className="flex w-fit cursor-pointer items-center gap-2 rounded border border-dashed border-charcoal-600 py-3 pl-3 pr-4 transition hover:border-charcoal-500 hover:bg-charcoal-850" > - - Staging - + diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index 0fa2669bb8..ad81e26ce4 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -563,22 +563,6 @@ function RunBody({ )} - - Engine version - {run.engine} - - {isAdmin && ( - <> - - Primary master queue - {run.masterQueue} - - - Secondary master queue - {run.secondaryMasterQueue} - - - )} Test run @@ -695,6 +679,22 @@ function RunBody({ Internal ID {run.id} + {isAdmin && ( + <> + + Engine version + {run.engine} + + + Primary master queue + {run.masterQueue} + + + Secondary master queue + {run.secondaryMasterQueue ?? "–"} + + + )}
) : tab === "context" ? ( From f3c352529db04a38b8733da9889999eb77bc5e5d Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 14:18:29 +0000 Subject: [PATCH 21/95] Test page working --- .../app/components/BlankStatePanels.tsx | 29 ++- .../app/presenters/v3/TestPresenter.server.ts | 84 +------- .../presenters/v3/TestTaskPresenter.server.ts | 55 +----- .../route.tsx | 53 +++-- .../route.tsx | 183 ++++++------------ apps/webapp/app/utils/pathBuilder.ts | 2 +- 6 files changed, 138 insertions(+), 268 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.test.tasks.$taskParam => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam}/route.tsx (92%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.test => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test}/route.tsx (50%) diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index 160fd62938..fd5ea69ecd 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -1,4 +1,5 @@ import { + BeakerIcon, BookOpenIcon, ChatBubbleLeftRightIcon, ClockIcon, @@ -7,7 +8,7 @@ import { } from "@heroicons/react/20/solid"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { type MinimumEnvironment } from "~/presenters/SelectBestEnvironmentPresenter.server"; -import { docsPath, v3NewSchedulePath } from "~/utils/pathBuilder"; +import { docsPath, v3EnvironmentPath, v3NewSchedulePath } from "~/utils/pathBuilder"; import { InlineCode } from "./code/InlineCode"; import { environmentFullTitle } from "./environments/EnvironmentLabel"; import { Feedback } from "./Feedback"; @@ -22,6 +23,7 @@ import { useLocation } from "react-use"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; +import { RuntimeEnvironmentType } from "@trigger.dev/database"; export function HasNoTasksDev() { return ( @@ -163,3 +165,28 @@ export function BatchesNone() { ); } + +export function TestHasNoTasks() { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + return ( + + + You have no tasks in this environment. + + + Add tasks + + + ); +} diff --git a/apps/webapp/app/presenters/v3/TestPresenter.server.ts b/apps/webapp/app/presenters/v3/TestPresenter.server.ts index 9c72d0371e..a49d6b4269 100644 --- a/apps/webapp/app/presenters/v3/TestPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestPresenter.server.ts @@ -1,92 +1,24 @@ -import { TaskTriggerSource } from "@trigger.dev/database"; -import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server"; -import { TestSearchParams } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.test/route"; -import { sortEnvironments } from "~/utils/environmentSort"; -import { createSearchParams } from "~/utils/searchParams"; +import { type RuntimeEnvironmentType, type TaskTriggerSource } from "@trigger.dev/database"; +import { sqlDatabaseSchema } from "~/db.server"; import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server"; import { BasePresenter } from "./basePresenter.server"; type TaskListOptions = { userId: string; - projectSlug: string; - url: string; + projectId: string; + environmentId: string; + environmentType: RuntimeEnvironmentType; }; export type TaskList = Awaited>; export type TaskListItem = NonNullable[0]; -export type SelectedEnvironment = NonNullable; export class TestPresenter extends BasePresenter { - public async call({ userId, projectSlug, url }: TaskListOptions) { - // Find the project scoped to the organization - const project = await this._replica.project.findFirstOrThrow({ - select: { - id: true, - environments: { - select: { - id: true, - type: true, - slug: true, - }, - where: { - OR: [ - { - type: { - in: ["PREVIEW", "STAGING", "PRODUCTION"], - }, - }, - { - type: "DEVELOPMENT", - orgMember: { - userId, - }, - }, - ], - }, - }, - }, - where: { - slug: projectSlug, - }, - }); - - const environments = sortEnvironments( - project.environments.map((environment) => ({ - id: environment.id, - type: environment.type, - slug: environment.slug, - })) - ); - - const searchParams = createSearchParams(url, TestSearchParams); - - //no environmentId - 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 === environment); - if (!matchingEnvironment) { - return { - hasSelectedEnvironment: false as const, - environments, - }; - } - - const isDev = matchingEnvironment.type === "DEVELOPMENT"; - const tasks = await this.#getTasks(matchingEnvironment.id, isDev); + public async call({ userId, projectId, environmentId, environmentType }: TaskListOptions) { + const isDev = environmentType === "DEVELOPMENT"; + const tasks = await this.#getTasks(environmentId, isDev); return { - hasSelectedEnvironment: true as const, - environments, - selectedEnvironment: matchingEnvironment, tasks: tasks.map((task) => { return { id: task.id, diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 46e2c1a37b..c384781768 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -1,17 +1,19 @@ import { ScheduledTaskPayload, parsePacket, prettyPrintPacket } from "@trigger.dev/core/v3"; -import { BackgroundWorkerTask, RuntimeEnvironmentType, TaskRunStatus } from "@trigger.dev/database"; -import { PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server"; +import { type RuntimeEnvironmentType, type TaskRunStatus } from "@trigger.dev/database"; +import { type PrismaClient, prisma, sqlDatabaseSchema } from "~/db.server"; import { getTimezones } from "~/utils/timezones.server"; -import { getUsername } from "~/utils/username"; import { - BackgroundWorkerTaskSlim, + type BackgroundWorkerTaskSlim, findCurrentWorkerDeployment, } from "~/v3/models/workerDeployment.server"; type TestTaskOptions = { userId: string; - projectSlug: string; - environmentSlug: string; + projectId: string; + environment: { + id: string; + type: RuntimeEnvironmentType; + }; taskIdentifier: string; }; @@ -21,12 +23,6 @@ type Task = { filePath: string; exportName: string; friendlyId: string; - environment: { - id: string; - type: RuntimeEnvironmentType; - userId?: string; - userName?: string; - }; }; export type TestTask = @@ -87,35 +83,10 @@ export class TestTaskPresenter { public async call({ userId, - projectSlug, - environmentSlug, + projectId, + environment, taskIdentifier, }: TestTaskOptions): Promise { - const environment = await this.#prismaClient.runtimeEnvironment.findFirstOrThrow({ - where: { - slug: environmentSlug, - project: { - slug: projectSlug, - }, - orgMember: environmentSlug === "dev" ? { userId } : undefined, - }, - select: { - id: true, - type: true, - orgMember: { - select: { - user: { - select: { - id: true, - name: true, - displayName: true, - }, - }, - }, - }, - }, - }); - let task: BackgroundWorkerTaskSlim | null = null; if (environment.type !== "DEVELOPMENT") { const deployment = await findCurrentWorkerDeployment(environment.id); @@ -182,12 +153,6 @@ export class TestTaskPresenter { filePath: task.filePath, exportName: task.exportName, friendlyId: task.friendlyId, - environment: { - id: environment.id, - type: environment.type, - userId: environment.orgMember?.user.id, - userName: getUsername(environment.orgMember?.user), - }, }; switch (task.triggerSource) { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx similarity index 92% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.test.tasks.$taskParam/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 5869e6bc74..4b1ffa5050 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -2,12 +2,12 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { BeakerIcon } from "@heroicons/react/20/solid"; import { Form, useActionData, useSubmit } from "@remix-run/react"; -import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { TaskRunStatus } from "@trigger.dev/database"; +import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { type TaskRunStatus } from "@trigger.dev/database"; import { useCallback, useEffect, useRef, useState } from "react"; -import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { JSONEditor } from "~/components/code/JSONEditor"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { FullEnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { Button } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { DateField } from "~/components/primitives/DateField"; @@ -31,16 +31,19 @@ import { TabButton, TabContainer } from "~/components/primitives/Tabs"; import { TextLink } from "~/components/primitives/TextLink"; import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; import { TimezoneList } from "~/components/scheduled/timezones"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useSearchParams } from "~/hooks/useSearchParam"; import { redirectBackWithErrorMessage, redirectWithErrorMessage, redirectWithSuccessMessage, } from "~/models/message.server"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { - ScheduledRun, - StandardRun, - TestTask, + type ScheduledRun, + type StandardRun, + type TestTask, TestTaskPresenter, } from "~/presenters/v3/TestTaskPresenter.server"; import { logger } from "~/services/logger.server"; @@ -53,22 +56,31 @@ import { TestTaskData } from "~/v3/testTask"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { projectParam, organizationSlug, taskParam } = v3TaskParamsSchema.parse(params); + const { projectParam, organizationSlug, envParam, taskParam } = v3TaskParamsSchema.parse(params); - //need an environment - const searchParams = new URL(request.url).searchParams; - const environment = searchParams.get("environment"); + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { - return redirect(v3TestPath({ slug: organizationSlug }, { slug: projectParam })); + throw new Response(undefined, { + status: 404, + statusText: "Environment not found", + }); } const presenter = new TestTaskPresenter(); try { const result = await presenter.call({ userId, - projectSlug: projectParam, + projectId: project.id, taskIdentifier: taskParam, - environmentSlug: environment, + environment: environment, }); return typedjson(result); @@ -83,7 +95,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { export const action: ActionFunction = async ({ request, params }) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam, taskParam } = v3TaskParamsSchema.parse(params); + const { organizationSlug, projectParam, envParam, taskParam } = v3TaskParamsSchema.parse(params); const formData = await request.formData(); const submission = parse(formData, { schema: TestTaskData }); @@ -107,6 +119,7 @@ export const action: ActionFunction = async ({ request, params }) => { v3RunSpanPath( { slug: organizationSlug }, { slug: projectParam }, + { slug: envParam }, { friendlyId: run.friendlyId }, { spanId: run.spanId } ), @@ -156,6 +169,7 @@ export default function Page() { const startingJson = "{\n\n}"; function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: StandardRun[] }) { + const environment = useEnvironment(); const { value, replace } = useSearchParams(); const tab = value("tab"); @@ -195,7 +209,7 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa payload: currentPayloadJson.current, metadata: currentMetadataJson.current, taskIdentifier: task.taskIdentifier, - environmentId: task.environment.id, + environmentId: environment.id, }, { action: "", @@ -312,7 +326,7 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa This test will run in - +
{hasTaskInEnvironment === false && (
- - There is no task {activeTaskIdentifier} in the selected environment. + + There is no "{activeTaskIdentifier}" task in the selected environment.
)} @@ -234,9 +176,7 @@ function TaskSelector({ {filteredItems.length > 0 ? ( - filteredItems.map((t) => ( - - )) + filteredItems.map((t) => ) ) : ( @@ -250,21 +190,12 @@ function TaskSelector({ ); } -function NoTaskInstructions({ environment }: { environment?: SelectedEnvironment }) { - return ( -
- - You have no tasks {environment ? `in ${environmentTitle(environment)}` : ""}. - -
- ); -} - -function TaskRow({ task, environmentSlug }: { task: TaskListItem; environmentSlug: string }) { +function TaskRow({ task }: { task: TaskListItem }) { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); - const path = v3TestTaskPath(organization, project, task, environmentSlug); + const path = v3TestTaskPath(organization, project, environment, task); const { isActive, isPending } = useLinkStatus(path); return ( diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 3d17d534c2..88235a1d14 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -28,7 +28,7 @@ export const EnvironmentParamSchema = ProjectParamSchema.extend({ }); //v3 -export const v3TaskParamsSchema = ProjectParamSchema.extend({ +export const v3TaskParamsSchema = EnvironmentParamSchema.extend({ taskParam: z.string(), }); From 79379334c7e5574e19b82d76838a45320d5cc056 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 14:51:23 +0000 Subject: [PATCH 22/95] =?UTF-8?q?Removed=20=E2=80=9CSelect=20task=E2=80=9D?= =?UTF-8?q?=20from=20the=20test=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../route.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index 49c6b00c1c..6c770c63ee 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -107,14 +107,7 @@ export default function Page() {
-
-
-
- Select a task -
- -
-
+
From e8c8aa2dc30efa348c43ff897878877225b60fd0 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 15:56:44 +0000 Subject: [PATCH 23/95] Some work on deployment page --- apps/webapp/app/assets/icons/DropdownIcon.tsx | 20 ++++ .../app/components/BlankStatePanels.tsx | 111 +++++++++++++++++- .../navigation/EnvironmentSelector.tsx | 57 +++++++++ .../app/components/navigation/SideMenu.tsx | 68 ++--------- .../components/navigation/SideMenuHeader.tsx | 8 +- .../components/navigation/SideMenuSection.tsx | 2 +- .../app/components/primitives/Popover.tsx | 11 +- .../app/hooks/useEnvironmentSwitcher.ts | 7 ++ .../route.tsx | 54 ++------- 9 files changed, 219 insertions(+), 119 deletions(-) create mode 100644 apps/webapp/app/assets/icons/DropdownIcon.tsx create mode 100644 apps/webapp/app/components/navigation/EnvironmentSelector.tsx diff --git a/apps/webapp/app/assets/icons/DropdownIcon.tsx b/apps/webapp/app/assets/icons/DropdownIcon.tsx new file mode 100644 index 0000000000..4988448bd6 --- /dev/null +++ b/apps/webapp/app/assets/icons/DropdownIcon.tsx @@ -0,0 +1,20 @@ +export function DropdownIcon({ className }: { className?: string }) { + return ( + + + + + ); +} diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index fd5ea69ecd..6c23991b18 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -4,11 +4,17 @@ import { ChatBubbleLeftRightIcon, ClockIcon, RectangleGroupIcon, + ServerStackIcon, Squares2X2Icon, } from "@heroicons/react/20/solid"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { type MinimumEnvironment } from "~/presenters/SelectBestEnvironmentPresenter.server"; -import { docsPath, v3EnvironmentPath, v3NewSchedulePath } from "~/utils/pathBuilder"; +import { + docsPath, + v3EnvironmentPath, + v3EnvironmentVariablesPath, + v3NewSchedulePath, +} from "~/utils/pathBuilder"; import { InlineCode } from "./code/InlineCode"; import { environmentFullTitle } from "./environments/EnvironmentLabel"; import { Feedback } from "./Feedback"; @@ -24,6 +30,8 @@ import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { RuntimeEnvironmentType } from "@trigger.dev/database"; +import { TextLink } from "./primitives/TextLink"; +import { EnvironmentSelector } from "./navigation/EnvironmentSelector"; export function HasNoTasksDev() { return ( @@ -190,3 +198,104 @@ export function TestHasNoTasks() { ); } + +export function DeploymentsNone() { + const organization = useOrganization(); + const project = useProject(); + + return ( + + + There are several ways to deploy your tasks. You can use the CLI, Continuous Integration + (like GitHub Actions), or an integration with a service like Netlify or Vercel. Make sure + you{" "} + + set your environment variables + {" "} + first. + +
+ + Deploy with the CLI + + + Deploy with GitHub actions + +
+
+ ); +} + +export function DeploymentsNoneDev() { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + return ( +
+ + + This is the Development environment. When you're ready to deploy your tasks, switch to a + different environment. + + + There are several ways to deploy your tasks. You can use the CLI, Continuous Integration + (like GitHub Actions), or an integration with a service like Netlify or Vercel. Make sure + you{" "} + + set your environment variables + {" "} + first. + +
+ + Deploy with the CLI + + + Deploy with GitHub actions + +
+
+
+ + Switch to a deployed environment + + +
+
+ ); +} diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx new file mode 100644 index 0000000000..5c01f0024b --- /dev/null +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -0,0 +1,57 @@ +import { useNavigation } from "@remix-run/react"; +import { useEffect, useState } from "react"; +import { useEnvironmentSwitcher } from "~/hooks/useEnvironmentSwitcher"; +import { type MatchedOrganization } from "~/hooks/useOrganizations"; +import { FullEnvironmentCombo } from "../environments/EnvironmentLabel"; +import { + Popover, + PopoverArrowTrigger, + PopoverContent, + PopoverMenuItem, +} from "../primitives/Popover"; +import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu"; +import { cn } from "~/utils/cn"; + +export function EnvironmentSelector({ + project, + environment, + className, +}: { + project: SideMenuProject; + environment: SideMenuEnvironment; + className?: string; +}) { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const navigation = useNavigation(); + const { urlForEnvironment } = useEnvironmentSwitcher(); + + useEffect(() => { + setIsMenuOpen(false); + }, [navigation.location?.pathname]); + + return ( + setIsMenuOpen(open)} open={isMenuOpen}> + + + + + {project.environments.map((env) => ( + } + isSelected={env.id === environment.id} + /> + ))} + + + ); +} diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 85b30d2060..3ed9acf235 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -67,13 +67,15 @@ import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; import { type MatchedEnvironment } from "~/hooks/useEnvironment"; -import { FullEnvironmentCombo } from "../environments/EnvironmentLabel"; import { SideMenuSection } from "./SideMenuSection"; -import { useEnvironmentSwitcher } from "~/hooks/useEnvironmentSwitcher"; +import { EnvironmentSelector } from "./EnvironmentSelector"; type SideMenuUser = Pick & { isImpersonating: boolean }; -type SideMenuProject = Pick; -type SideMenuEnvironment = MatchedEnvironment; +export type SideMenuProject = Pick< + MatchedProject, + "id" | "name" | "slug" | "version" | "environments" +>; +export type SideMenuEnvironment = MatchedEnvironment; type SideMenuProps = { user: SideMenuUser; @@ -133,11 +135,9 @@ export function SideMenu({ >
- +
+ +
- ); } - -function EnvironmentSelector({ - organization, - project, - environment, -}: { - organization: MatchedOrganization; - project: SideMenuProject; - environment: SideMenuEnvironment; -}) { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const navigation = useNavigation(); - const { urlForEnvironment } = useEnvironmentSwitcher(); - - useEffect(() => { - setIsMenuOpen(false); - }, [navigation.location?.pathname]); - - return ( - setIsMenuOpen(open)} open={isMenuOpen}> - - - - - {project.environments.map((env) => ( - } - isSelected={env.id === environment.id} - /> - ))} - - - ); -} diff --git a/apps/webapp/app/components/navigation/SideMenuHeader.tsx b/apps/webapp/app/components/navigation/SideMenuHeader.tsx index 8502ea0e35..83741a6c7a 100644 --- a/apps/webapp/app/components/navigation/SideMenuHeader.tsx +++ b/apps/webapp/app/components/navigation/SideMenuHeader.tsx @@ -1,6 +1,5 @@ import { useNavigation } from "@remix-run/react"; import { useEffect, useState } from "react"; -import { Paragraph } from "../primitives/Paragraph"; import { Popover, PopoverContent, PopoverCustomTrigger } from "../primitives/Popover"; import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid"; @@ -14,12 +13,7 @@ export function SideMenuHeader({ title, children }: { title: string; children?: return (
- - {title} - +

{title}

{children !== undefined ? ( setHeaderMenuOpen(open)} open={isHeaderMenuOpen}> diff --git a/apps/webapp/app/components/navigation/SideMenuSection.tsx b/apps/webapp/app/components/navigation/SideMenuSection.tsx index 9743a991e2..e6d4e47593 100644 --- a/apps/webapp/app/components/navigation/SideMenuSection.tsx +++ b/apps/webapp/app/components/navigation/SideMenuSection.tsx @@ -32,7 +32,7 @@ export function SideMenuSection({ className="flex cursor-pointer items-center gap-1 rounded-sm py-1 pl-1 text-text-dimmed hover:bg-charcoal-750 hover:text-text-bright" onClick={handleToggle} > -

{title}

+

{title}

{children} - diff --git a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts index 2cc840ae32..821bc471f0 100644 --- a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts +++ b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts @@ -44,6 +44,13 @@ export function routeForEnvironmentSwitch({ matchId: string; environmentSlug: string; }) { + switch (matchId) { + case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam": { + } + default: { + } + } + //replace the /env// in the path so it's /env/ const newPath = location.pathname.replace(/env\/([a-z0-9-]+)/, `env/${environmentSlug}`); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index e01fcc3982..4722ab788d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -9,6 +9,7 @@ import { type MetaFunction, Outlet, useLocation, useParams } from "@remix-run/re import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; +import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels"; import { UserAvatar } from "~/components/UserProfilePhoto"; import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; @@ -109,7 +110,6 @@ export default function Page() { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); - const user = useUser(); const { deployments, currentPage, totalPages } = useTypedLoaderData(); const hasDeployments = totalPages > 0; @@ -259,8 +259,14 @@ export default function Page() {
)}
+ ) : environment.type === "DEVELOPMENT" ? ( + + + ) : ( - + + + )}
@@ -278,50 +284,6 @@ export default function Page() { ); } -function CreateDeploymentInstructions() { - const organization = useOrganization(); - const project = useProject(); - - return ( - - - - There are several ways to deploy your tasks. You can use the CLI, Continuous Integration - (like GitHub Actions), or an integration with a service like Netlify or Vercel. Make sure - you{" "} - - set your environment variables - {" "} - first. - -
- - Deploy with the CLI - - - Deploy with GitHub actions - -
-
-
- ); -} - function DeploymentActionsCell({ deployment, path, From 197bc1d45ed2613b34f745c036ec697107394f85 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:02:43 +0000 Subject: [PATCH 24/95] Style tweaks --- apps/webapp/app/components/BlankStatePanels.tsx | 2 +- apps/webapp/app/components/navigation/EnvironmentSelector.tsx | 2 +- apps/webapp/app/components/primitives/Popover.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index 6c23991b18..788dd80b9e 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -293,7 +293,7 @@ export function DeploymentsNoneDev() {
diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 5c01f0024b..e02a1d221e 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -39,7 +39,7 @@ export function EnvironmentSelector({ diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 495c51d46a..784bd03ed4 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -176,7 +176,7 @@ function PopoverArrowTrigger({ From 9749ebce7fd2334795b74c32bd1ee2a2b02af154 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:13:52 +0000 Subject: [PATCH 25/95] Redirect from project root to approriate env --- .../SelectBestEnvironmentPresenter.server.ts | 2 +- .../route.tsx | 42 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts b/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts index ac60b46686..0bce257ca5 100644 --- a/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts +++ b/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts @@ -31,7 +31,7 @@ export class SelectBestEnvironmentPresenter { const projectId = user.dashboardPreferences.currentProjectId; if (projectId) { - const project = await this.#prismaClient.project.findUnique({ + const project = await this.#prismaClient.project.findFirst({ where: { id: projectId, deletedAt: null, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx index 0e8a393707..5788ce9734 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam._index/route.tsx @@ -1,5 +1,43 @@ -import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { prisma } from "~/db.server"; +import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server"; +import { requireUser } from "~/services/session.server"; +import { ProjectParamSchema, v3EnvironmentPath } from "~/utils/pathBuilder"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { - throw new Response("Not found", { status: 404, statusText: "Select an environment" }); + const user = await requireUser(request); + const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + deletedAt: null, + organization: { slug: organizationSlug, members: { some: { userId: user.id } } }, + }, + include: { + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const selector = new SelectBestEnvironmentPresenter(); + const environment = await selector.selectBestEnvironment(project.id, user, project.environments); + + return redirect(v3EnvironmentPath({ slug: organizationSlug }, project, environment)); }; From 126faeec308164ae136e6cbf5b35c16cb562dd91 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:14:04 +0000 Subject: [PATCH 26/95] Improved env selector styling --- .../webapp/app/components/navigation/EnvironmentSelector.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index e02a1d221e..74d5eae8f4 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -34,9 +34,10 @@ export function EnvironmentSelector({ - + Date: Wed, 12 Mar 2025 16:26:20 +0000 Subject: [PATCH 27/95] Fix for jsx errors --- apps/webapp/app/assets/icons/DropdownIcon.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/assets/icons/DropdownIcon.tsx b/apps/webapp/app/assets/icons/DropdownIcon.tsx index 4988448bd6..a22a11fbba 100644 --- a/apps/webapp/app/assets/icons/DropdownIcon.tsx +++ b/apps/webapp/app/assets/icons/DropdownIcon.tsx @@ -4,16 +4,16 @@ export function DropdownIcon({ className }: { className?: string }) { ); From a7cd227b3715d76b6c7c6ad61894b42485ddd29e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:26:27 +0000 Subject: [PATCH 28/95] Better min width on env selector --- apps/webapp/app/components/navigation/EnvironmentSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 74d5eae8f4..0020744bef 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -40,7 +40,7 @@ export function EnvironmentSelector({ From dc456fdd8262f4f4b45fe6e9a5fb4ff77f2b1025 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:26:35 +0000 Subject: [PATCH 29/95] Improved the env switching logic --- .../app/hooks/useEnvironmentSwitcher.ts | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts index 821bc471f0..6c4a0cf15c 100644 --- a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts +++ b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts @@ -1,4 +1,4 @@ -import { useMatch, useMatches, type Location } from "@remix-run/react"; +import { Path, useMatch, useMatches, type Location } from "@remix-run/react"; import { type MinimumEnvironment } from "~/presenters/SelectBestEnvironmentPresenter.server"; import { useEnvironment } from "./useEnvironment"; import { useEnvironments } from "./useEnvironments"; @@ -40,24 +40,41 @@ export function routeForEnvironmentSwitch({ matchId, environmentSlug, }: { - location: Location; + location: Path; matchId: string; environmentSlug: string; }) { switch (matchId) { + // Run page case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam": { + const newLocation: Path = { + pathname: replaceEnvInPath(location.pathname, environmentSlug).replace( + /\/runs\/.*/, + "/runs" + ), + search: "", + hash: "", + }; + return fullPath(newLocation); } default: { + const newLocation: Path = { + pathname: replaceEnvInPath(location.pathname, environmentSlug), + search: location.search, + hash: location.hash, + }; + return fullPath(newLocation); } } +} - //replace the /env// in the path so it's /env/ - const newPath = location.pathname.replace(/env\/([a-z0-9-]+)/, `env/${environmentSlug}`); - - console.log({ - oldPath: location.pathname, - newPath, - }); +/** + * Replace the /env// in the path so it's /env/ + */ +function replaceEnvInPath(path: string, environmentSlug: string) { + return path.replace(/env\/([a-z0-9-]+)/, `env/${environmentSlug}`); +} - return `${newPath}${location.search}${location.hash}`; +function fullPath(location: Path) { + return `${location.pathname}${location.search}${location.hash}`; } From e63b231323edc24d95a8512eecaecdec835d9603 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:28:11 +0000 Subject: [PATCH 30/95] Added deployments to env routing --- apps/webapp/app/hooks/useEnvironmentSwitcher.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts index 6c4a0cf15c..5dbfc6aeb3 100644 --- a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts +++ b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts @@ -57,6 +57,17 @@ export function routeForEnvironmentSwitch({ }; return fullPath(newLocation); } + case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam": { + const newLocation: Path = { + pathname: replaceEnvInPath(location.pathname, environmentSlug).replace( + /\/deployments\/.*/, + "/deployments" + ), + search: "", + hash: "", + }; + return fullPath(newLocation); + } default: { const newLocation: Path = { pathname: replaceEnvInPath(location.pathname, environmentSlug), From fad7e2a0cc7fdc9ff63985f3edc3ae8c3d47708b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:34:27 +0000 Subject: [PATCH 31/95] Redirect deployments to the correct env --- ...ojectParam.deployments.$deploymentParam.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.deployments.$deploymentParam.ts diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.deployments.$deploymentParam.ts b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.deployments.$deploymentParam.ts new file mode 100644 index 0000000000..021d468844 --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.deployments.$deploymentParam.ts @@ -0,0 +1,43 @@ +import { redirect } from "@remix-run/router"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { requireUserId } from "~/services/session.server"; +import { ProjectParamSchema, v3DeploymentPath } from "~/utils/pathBuilder"; + +const ParamSchema = ProjectParamSchema.extend({ + deploymentParam: z.string(), +}); + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + await requireUserId(request); + const { organizationSlug, projectParam, deploymentParam } = ParamSchema.parse(params); + + const deployment = await prisma.workerDeployment.findFirst({ + where: { + shortCode: deploymentParam, + project: { + slug: projectParam, + }, + }, + select: { + environment: true, + }, + }); + + if (!deployment) { + throw new Response("Not Found", { status: 404 }); + } + + return redirect( + v3DeploymentPath( + { + slug: organizationSlug, + }, + { slug: projectParam }, + { slug: deployment.environment.slug }, + { shortCode: deploymentParam }, + 0 + ) + ); +}; From 9cc8598c7dbdb76021a8e17c397b5cf672af7940 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 16:39:46 +0000 Subject: [PATCH 32/95] Redirect run from proj to env --- ...g.projects.$projectParam.runs.$runParam.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.runs.$runParam.ts diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.runs.$runParam.ts b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.runs.$runParam.ts new file mode 100644 index 0000000000..29aa557797 --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.runs.$runParam.ts @@ -0,0 +1,42 @@ +import { redirect } from "@remix-run/router"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { requireUserId } from "~/services/session.server"; +import { ProjectParamSchema, v3DeploymentPath, v3RunPath } from "~/utils/pathBuilder"; + +const ParamSchema = ProjectParamSchema.extend({ + runParam: z.string(), +}); + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + await requireUserId(request); + const { organizationSlug, projectParam, runParam } = ParamSchema.parse(params); + + const run = await prisma.taskRun.findFirst({ + where: { + friendlyId: runParam, + project: { + slug: projectParam, + }, + }, + select: { + runtimeEnvironment: true, + }, + }); + + if (!run) { + throw new Response("Not Found", { status: 404 }); + } + + return redirect( + v3RunPath( + { + slug: organizationSlug, + }, + { slug: projectParam }, + { slug: run.runtimeEnvironment.slug }, + { friendlyId: runParam } + ) + ); +}; From 517c2aa97d806d9ac18a9d01471b2339089bd4b1 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 18:31:58 +0000 Subject: [PATCH 33/95] JSX icon fix --- apps/webapp/app/assets/icons/DropdownIcon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/assets/icons/DropdownIcon.tsx b/apps/webapp/app/assets/icons/DropdownIcon.tsx index a22a11fbba..4a869ec8f6 100644 --- a/apps/webapp/app/assets/icons/DropdownIcon.tsx +++ b/apps/webapp/app/assets/icons/DropdownIcon.tsx @@ -6,7 +6,7 @@ export function DropdownIcon({ className }: { className?: string }) { stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" - stroke-linejoin="round" + strokeLinejoin="round" /> Date: Wed, 12 Mar 2025 18:33:03 +0000 Subject: [PATCH 34/95] Only allow single env schedules from now on --- .../app/components/BlankStatePanels.tsx | 138 +++++++- .../environments/EnvironmentLabel.tsx | 2 +- .../app/hooks/useEnvironmentSwitcher.ts | 12 + .../route.tsx | 22 +- .../route.tsx | 301 +++++++++--------- .../route.tsx | 13 +- .../route.tsx | 11 +- .../route.tsx | 63 ++-- .../v3/services/upsertTaskSchedule.server.ts | 4 +- 9 files changed, 360 insertions(+), 206 deletions(-) diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index 788dd80b9e..13fe257a2a 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -1,8 +1,10 @@ import { BeakerIcon, + BellAlertIcon, BookOpenIcon, ChatBubbleLeftRightIcon, ClockIcon, + PlusIcon, RectangleGroupIcon, ServerStackIcon, Squares2X2Icon, @@ -13,6 +15,7 @@ import { docsPath, v3EnvironmentPath, v3EnvironmentVariablesPath, + v3NewProjectAlertPath, v3NewSchedulePath, } from "~/utils/pathBuilder"; import { InlineCode } from "./code/InlineCode"; @@ -29,9 +32,9 @@ import { useLocation } from "react-use"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { RuntimeEnvironmentType } from "@trigger.dev/database"; import { TextLink } from "./primitives/TextLink"; import { EnvironmentSelector } from "./navigation/EnvironmentSelector"; +import { Pi } from "lucide-react"; export function HasNoTasksDev() { return ( @@ -244,7 +247,6 @@ export function DeploymentsNone() { export function DeploymentsNoneDev() { const organization = useOrganization(); const project = useProject(); - const environment = useEnvironment(); return (
@@ -286,16 +288,130 @@ export function DeploymentsNoneDev() {
-
- - Switch to a deployed environment + +
+ ); +} + +export function AlertsNoneDev() { + return ( +
+ + + You can get alerted when deployed runs fail. - -
+ + We don't support alerts in the Development environment. Switch to a deployed environment + to setup alerts. + +
+ + How to setup alerts + +
+ + + + ); +} + +export function AlertsNoneDeployed() { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + return ( +
+ + + You can get alerted when deployed runs fail. We currently support sending Slack, Email, + and webhooks. + + +
+ + New alert + + + Alert docs + +
+
+
+ ); +} + +function AlertsNoneProd() { + return ( +
+ + + You can get alerted when deployed runs fail. + + + We don't support alerts in the Development environment. Switch to a deployed environment + to setup alerts. + +
+ + How to setup alerts + +
+
+ +
+ ); +} + +function SwitcherPanel() { + const project = useProject(); + const environment = useEnvironment(); + + return ( +
+ + Switch to a deployed environment + +
); } diff --git a/apps/webapp/app/components/environments/EnvironmentLabel.tsx b/apps/webapp/app/components/environments/EnvironmentLabel.tsx index 13e73c269f..e9154a522c 100644 --- a/apps/webapp/app/components/environments/EnvironmentLabel.tsx +++ b/apps/webapp/app/components/environments/EnvironmentLabel.tsx @@ -164,7 +164,7 @@ export function FullEnvironmentCombo({ className?: string; }) { return ( - + diff --git a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts index 5dbfc6aeb3..0429026452 100644 --- a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts +++ b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts @@ -68,6 +68,18 @@ export function routeForEnvironmentSwitch({ }; return fullPath(newLocation); } + case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam": + case "routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam": { + const newLocation: Path = { + pathname: replaceEnvInPath(location.pathname, environmentSlug).replace( + /\/schedules\/.*/, + "/schedules" + ), + search: "", + hash: "", + }; + return fullPath(newLocation); + } default: { const newLocation: Path = { pathname: replaceEnvInPath(location.pathname, environmentSlug), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 13d2494f18..4d8b0482a7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -9,6 +9,7 @@ import { useEffect, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; +import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout, variantClasses } from "~/components/primitives/Callout"; import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; @@ -413,24 +414,9 @@ export default function Page() { {alertTypes.error} - - - - + + + {environmentTypes.error} {form.error} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx index 4da9f53a07..a620cc31a1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx @@ -17,8 +17,12 @@ import { type ProjectAlertChannelType, type ProjectAlertType } from "@trigger.de import assertNever from "assert-never"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; -import { EnvironmentTypeLabel } from "~/components/environments/EnvironmentLabel"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { AlertsNoneDev, AlertsNoneDeployed } from "~/components/BlankStatePanels"; +import { + EnvironmentTypeLabel, + FullEnvironmentCombo, +} from "~/components/environments/EnvironmentLabel"; +import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; import { DetailCell } from "~/components/primitives/DetailCell"; @@ -187,155 +191,166 @@ export default function Page() { -
-
- Project alerts - {alertChannels.length > 0 && !requiresUpgrade && ( - - New alert - - )} -
- - - - Name - Alert types - Environments - Channel - Enabled - Actions - - - - {alertChannels.length > 0 ? ( - alertChannels.map((alertChannel) => ( - - - {alertChannel.name} - - - {alertChannel.alertTypes.map((type) => alertTypeTitle(type)).join(", ")} - - - {alertChannel.environmentTypes.map((environmentType) => ( - 0 ? ( +
+
+ Project alerts + {alertChannels.length > 0 && !requiresUpgrade && ( + + New alert + + )} +
+
+ + + Name + Alert types + Channel + Enabled + Environments + Actions + + + + {alertChannels.length > 0 ? ( + alertChannels.map((alertChannel) => ( + + + {alertChannel.name} + + + {alertChannel.alertTypes.map((type) => alertTypeTitle(type)).join(", ")} + + + + + + - ))} - - - - - - + +
+ {alertChannel.environmentTypes.map((environmentType) => ( + + ))} +
+
+ + {alertChannel.enabled ? ( + + ) : ( + + )} + + + } + className={ + alertChannel.enabled ? "" : "group-hover/table-row:bg-charcoal-800/50" + } /> +
+ )) + ) : ( + + +
+ + You haven't created any project alerts yet + + + Get alerted when runs or deployments fail, or when deployments succeed in + both Prod and Staging environments. + + + New alert + +
- - {alertChannel.enabled ? ( - - ) : ( - - )} - - - } - className={ - alertChannel.enabled ? "" : "group-hover/table-row:bg-charcoal-800/50" - } - />
- )) - ) : ( - - -
- - You haven't created any project alerts yet - - - Get alerted when runs or deployments fail, or when deployments succeed in - both Prod and Staging environments. - - - New alert - -
-
-
- )} -
-
-
-
-
- - - - - -
- } - content={`${Math.round((limits.used / limits.limit) * 100)}%`} - /> -
- {requiresUpgrade ? ( - - You've used all {limits.limit} of your available alerts. Upgrade your plan to - enable more. - - ) : ( - - You've used {limits.used}/{limits.limit} of your alerts. - - )} - - - Upgrade - + )} + + +
+
+
+ + + + + +
+ } + content={`${Math.round((limits.used / limits.limit) * 100)}%`} + /> +
+ {requiresUpgrade ? ( + + You've used all {limits.limit} of your available alerts. Upgrade your plan + to enable more. + + ) : ( + + You've used {limits.used}/{limits.limit} of your alerts. + + )} + + + Upgrade + +
-
+ ) : environment.type === "DEVELOPMENT" ? ( + + + + ) : ( + + + + )} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx index de5002de8e..1a56af2b0c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx @@ -13,7 +13,10 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { InlineCode } from "~/components/code/InlineCode"; -import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel"; +import { + EnvironmentLabels, + FullEnvironmentCombo, +} from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; import { @@ -262,9 +265,13 @@ export default function Page() { {schedule.timezone} - Environments + Environment - +
+ {schedule.environments.map((env) => ( + + ))} +
{isImperative && ( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index aac0471133..21841c2541 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -7,7 +7,10 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { Feedback } from "~/components/Feedback"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { InlineCode } from "~/components/code/InlineCode"; -import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel"; +import { + EnvironmentLabels, + FullEnvironmentCombo, +} from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; @@ -453,7 +456,11 @@ function SchedulesTable({ : "N/A"} - +
+ {schedule.environments.map((env) => ( + + ))} +
{schedule.type === "IMPERATIVE" ? ( diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx index 6bbf30886e..30ed8eca36 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx @@ -10,6 +10,7 @@ import { useRef, useState } from "react"; import { environmentTextClassName, environmentTitle, + FullEnvironmentCombo, } from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; @@ -315,34 +316,44 @@ export function UpsertScheduleForm({
)} - +
- {possibleEnvironments.map((environment) => ( - - {environmentTitle(environment, environment.userName)} - - } - defaultChecked={ - schedule?.instances.find((i) => i.environmentId === environment.id) !== - undefined - } - variant="button" - /> - ))} + {/* This first condition supports old schedules where we let you have multiple environments */} + {schedule && schedule?.environments.length > 1 ? ( + possibleEnvironments.map((environment) => ( + + {environmentTitle(environment, environment.userName)} + + } + defaultChecked={ + schedule?.instances.find((i) => i.environmentId === environment.id) !== + undefined + } + variant="button" + /> + )) + ) : ( + <> + + + + )}
- - Select all the environments where you want this schedule to run. Note that scheduled - tasks in dev environments will only run while you are connected with the dev CLI - + {environment.type === "DEVELOPMENT" && ( + + Note that scheduled tasks in dev environments will only run while you are + connected with the dev CLI. + + )} {environments.error}
diff --git a/apps/webapp/app/v3/services/upsertTaskSchedule.server.ts b/apps/webapp/app/v3/services/upsertTaskSchedule.server.ts index 70aa09c2b0..4e62f9cc61 100644 --- a/apps/webapp/app/v3/services/upsertTaskSchedule.server.ts +++ b/apps/webapp/app/v3/services/upsertTaskSchedule.server.ts @@ -1,9 +1,9 @@ -import { Prisma, TaskSchedule } from "@trigger.dev/database"; +import { type Prisma, type TaskSchedule } from "@trigger.dev/database"; import cronstrue from "cronstrue"; import { nanoid } from "nanoid"; import { $transaction } from "~/db.server"; import { generateFriendlyId } from "../friendlyIdentifiers"; -import { UpsertSchedule } from "../schedules"; +import { type UpsertSchedule } from "../schedules"; import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server"; import { BaseService, ServiceValidationError } from "./baseService.server"; import { CheckScheduleService } from "./checkSchedule.server"; From 5ff1f1ef77b30288c7d88b9345e727ba545dd484 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 18:33:31 +0000 Subject: [PATCH 35/95] Remove env var count from the API keys page --- .../app/presenters/v3/ApiKeysPresenter.server.ts | 12 +++--------- .../route.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/presenters/v3/ApiKeysPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiKeysPresenter.server.ts index eeb7b9f2e4..e269f3fc2c 100644 --- a/apps/webapp/app/presenters/v3/ApiKeysPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiKeysPresenter.server.ts @@ -1,6 +1,6 @@ -import { PrismaClient, prisma } from "~/db.server"; -import { Project } from "~/models/project.server"; -import { User } from "~/models/user.server"; +import { type PrismaClient, prisma } from "~/db.server"; +import { type Project } from "~/models/project.server"; +import { type User } from "~/models/user.server"; import { sortEnvironments } from "~/utils/environmentSort"; export class ApiKeysPresenter { @@ -33,11 +33,6 @@ export class ApiKeysPresenter { version: "desc", }, }, - _count: { - select: { - environmentVariableValues: true, - }, - }, }, where: { project: { @@ -71,7 +66,6 @@ export class ApiKeysPresenter { slug: environment.slug, updatedAt: environment.updatedAt, latestVersion: environment.backgroundWorkers.at(0)?.version, - environmentVariableCount: environment._count.environmentVariableValues, })) ), hasStaging: environments.some((environment) => environment.type === "STAGING"), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.apikeys/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.apikeys/route.tsx index 6608fb69b3..a1eb8e1a37 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.apikeys/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.apikeys/route.tsx @@ -4,7 +4,11 @@ import { MetaFunction } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; -import { EnvironmentLabel, environmentTitle } from "~/components/environments/EnvironmentLabel"; +import { + EnvironmentLabel, + environmentTitle, + FullEnvironmentCombo, +} from "~/components/environments/EnvironmentLabel"; import { RegenerateApiKeyModal } from "~/components/environments/RegenerateApiKeyModal"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { LinkButton } from "~/components/primitives/Buttons"; @@ -99,7 +103,6 @@ export default function Page() { Secret key Key generated Latest version - Env vars Actions @@ -107,7 +110,7 @@ export default function Page() { {environments.map((environment) => ( - + {environment.latestVersion ?? "–"} - {environment.environmentVariableCount} Date: Wed, 12 Mar 2025 19:33:16 +0000 Subject: [PATCH 36/95] Move improvements and redirects --- .../app/components/navigation/SideMenu.tsx | 34 ++++++------- .../route.tsx | 47 +++++++++--------- .../route.tsx | 2 +- .../route.tsx | 23 +++++++-- .../route.tsx | 48 +++++++++---------- ...tionSlug.projects.$projectParam.apikeys.ts | 43 +++++++++++++++++ ...Slug.projects.$projectParam.concurrency.ts | 43 +++++++++++++++++ ...cts.$projectParam.environment-variables.ts | 43 +++++++++++++++++ apps/webapp/app/utils/pathBuilder.ts | 32 +++++++++---- 9 files changed, 235 insertions(+), 80 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.apikeys => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys}/route.tsx (84%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.concurrency => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency}/route.tsx (98%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new}/route.tsx (96%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.environment-variables => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables}/route.tsx (92%) create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.apikeys.ts create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.concurrency.ts create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.environment-variables.ts diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 3ed9acf235..0c3dc6387d 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -133,13 +133,15 @@ export function SideMenu({ className="h-full overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" ref={borderRef} > -
- -
- +
+
+ +
+ +
- +
- - + +
@@ -213,7 +215,7 @@ export function SideMenu({ name="Concurrency limits" icon={RectangleStackIcon} activeIconColor="text-indigo-500" - to={v3ConcurrencyPath(organization, project)} + to={v3ConcurrencyPath(organization, project, environment)} data-action="concurrency" /> { return [ @@ -135,6 +131,25 @@ export default function Page() { > ))} + {!hasStaging && ( + + + + + + + Upgrade to get staging environment + + + + + + + )} @@ -146,22 +161,6 @@ export default function Page() { backend. - - {!hasStaging && ( -
- - - Upgrade to add a Staging environment - - - Upgrade - -
- )}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.concurrency/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx similarity index 98% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.concurrency/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx index 08798ff4de..3ea3c4b0b0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.concurrency/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx @@ -4,7 +4,7 @@ import { ChatBubbleLeftEllipsisIcon, } from "@heroicons/react/20/solid"; import { LockOpenIcon } from "@heroicons/react/24/solid"; -import { Await, MetaFunction } from "@remix-run/react"; +import { Await, type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense } from "react"; import { typeddefer, useTypedLoaderData } from "remix-typedjson"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx similarity index 96% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index fe530a4cc9..64a1d73a34 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -37,7 +37,12 @@ import { useProject } from "~/hooks/useProject"; import { EnvironmentVariablesPresenter } from "~/presenters/v3/EnvironmentVariablesPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; -import { ProjectParamSchema, v3BillingPath, v3EnvironmentVariablesPath } from "~/utils/pathBuilder"; +import { + EnvironmentParamSchema, + ProjectParamSchema, + v3BillingPath, + v3EnvironmentVariablesPath, +} from "~/utils/pathBuilder"; import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; import { EnvironmentVariableKey } from "~/v3/environmentVariables/repository"; import dotenv from "dotenv"; @@ -49,6 +54,7 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/primitives/Tooltip"; +import { useEnvironment } from "~/hooks/useEnvironment"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); @@ -110,7 +116,7 @@ const schema = z.object({ export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); if (request.method.toUpperCase() !== "POST") { return { status: 405, body: "Method Not Allowed" }; @@ -162,7 +168,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { return json(submission); } - return redirect(v3EnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam })); + return redirect( + v3EnvironmentVariablesPath( + { slug: organizationSlug }, + { slug: projectParam }, + { slug: envParam } + ) + ); }; export default function Page() { @@ -173,6 +185,7 @@ export default function Page() { const navigate = useNavigate(); const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const isLoading = navigation.state !== "idle" && navigation.formMethod === "post"; @@ -197,7 +210,7 @@ export default function Page() { open={isOpen} onOpenChange={(o) => { if (!o) { - navigate(v3EnvironmentVariablesPath(organization, project)); + navigate(v3EnvironmentVariablesPath(organization, project, environment)); } }} > @@ -299,7 +312,7 @@ export default function Page() { } cancelButton={ Cancel diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx similarity index 92% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index 07fb7b00b3..2e3e8060bf 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -21,7 +21,7 @@ import { Fragment, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; @@ -47,6 +47,7 @@ import { TableRow, } from "~/components/primitives/Table"; import { prisma } from "~/db.server"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithSuccessMessage } from "~/models/message.server"; @@ -57,6 +58,7 @@ import { import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { + EnvironmentParamSchema, ProjectParamSchema, docsPath, v3BillingPath, @@ -109,7 +111,7 @@ const schema = z.discriminatedUnion("action", [ export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); if (request.method.toUpperCase() !== "POST") { return { status: 405, body: "Method Not Allowed" }; @@ -154,7 +156,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { //use redirectDocument because it reloads the page return redirectDocument( - v3EnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam }), + v3EnvironmentVariablesPath( + { slug: organizationSlug }, + { slug: projectParam }, + { slug: envParam } + ), { headers: { refresh: "true", @@ -172,7 +178,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { } return redirectWithSuccessMessage( - v3EnvironmentVariablesPath({ slug: organizationSlug }, { slug: projectParam }), + v3EnvironmentVariablesPath( + { slug: organizationSlug }, + { slug: projectParam }, + { slug: envParam } + ), request, `Deleted ${submission.value.key} environment variable` ); @@ -183,8 +193,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { export default function Page() { const [revealAll, setRevealAll] = useState(false); const { environmentVariables, environments, hasStaging } = useTypedLoaderData(); - const project = useProject(); const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); return ( @@ -211,7 +222,7 @@ export default function Page() { onCheckedChange={(e) => setRevealAll(e.valueOf())} /> Key {environments.map((environment) => ( - - + + ))} Actions @@ -275,7 +286,7 @@ export default function Page() {
You haven't set any environment variables yet. -
+
Dev environment variables specified here will be overridden by ones in your .env file when running locally. - {!hasStaging && ( -
- - - Upgrade to add a Staging environment - - - Upgrade - -
- )}
@@ -409,7 +405,7 @@ function EditEnvironmentVariablePanel({ className="flex items-center justify-end" htmlFor={`values[${index}].value`} > - + { + const user = await requireUser(request); + const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + deletedAt: null, + organization: { slug: organizationSlug, members: { some: { userId: user.id } } }, + }, + include: { + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const selector = new SelectBestEnvironmentPresenter(); + const environment = await selector.selectBestEnvironment(project.id, user, project.environments); + + return redirect(v3ApiKeysPath({ slug: organizationSlug }, project, environment)); +}; diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.concurrency.ts b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.concurrency.ts new file mode 100644 index 0000000000..5a977cd5cc --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.concurrency.ts @@ -0,0 +1,43 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { prisma } from "~/db.server"; +import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server"; +import { requireUser } from "~/services/session.server"; +import { ProjectParamSchema, v3ApiKeysPath, v3ConcurrencyPath } from "~/utils/pathBuilder"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + deletedAt: null, + organization: { slug: organizationSlug, members: { some: { userId: user.id } } }, + }, + include: { + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const selector = new SelectBestEnvironmentPresenter(); + const environment = await selector.selectBestEnvironment(project.id, user, project.environments); + + return redirect(v3ConcurrencyPath({ slug: organizationSlug }, project, environment)); +}; diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.environment-variables.ts b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.environment-variables.ts new file mode 100644 index 0000000000..5f99c4a953 --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.environment-variables.ts @@ -0,0 +1,43 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { prisma } from "~/db.server"; +import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server"; +import { requireUser } from "~/services/session.server"; +import { ProjectParamSchema, v3ApiKeysPath, v3EnvironmentVariablesPath } from "~/utils/pathBuilder"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + deletedAt: null, + organization: { slug: organizationSlug, members: { some: { userId: user.id } } }, + }, + include: { + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const selector = new SelectBestEnvironmentPresenter(); + const environment = await selector.selectBestEnvironment(project.id, user, project.environments); + + return redirect(v3EnvironmentVariablesPath({ slug: organizationSlug }, project, environment)); +}; diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 88235a1d14..df45f50f89 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -155,20 +155,36 @@ export function v3TasksStreamingPath( return `${v3EnvironmentPath(organization, project, environment)}/tasks/stream`; } -export function v3ApiKeysPath(organization: OrgForPath, project: ProjectForPath) { - return `${v3ProjectPath(organization, project)}/apikeys`; +export function v3ApiKeysPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + return `${v3EnvironmentPath(organization, project, environment)}/apikeys`; } -export function v3EnvironmentVariablesPath(organization: OrgForPath, project: ProjectForPath) { - return `${v3ProjectPath(organization, project)}/environment-variables`; +export function v3EnvironmentVariablesPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + return `${v3EnvironmentPath(organization, project, environment)}/environment-variables`; } -export function v3ConcurrencyPath(organization: OrgForPath, project: ProjectForPath) { - return `${v3ProjectPath(organization, project)}/concurrency`; +export function v3ConcurrencyPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + return `${v3EnvironmentPath(organization, project, environment)}/concurrency`; } -export function v3NewEnvironmentVariablesPath(organization: OrgForPath, project: ProjectForPath) { - return `${v3EnvironmentVariablesPath(organization, project)}/new`; +export function v3NewEnvironmentVariablesPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + return `${v3EnvironmentVariablesPath(organization, project, environment)}/new`; } export function v3ProjectAlertsPath( From ab3e8809f55f996e0ba05a6d94b3228d3810796c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 19:55:36 +0000 Subject: [PATCH 37/95] Project settings moved --- .../app/components/navigation/SideMenu.tsx | 2 +- .../route.tsx | 6 +-- ...ionSlug.projects.$projectParam.settings.ts | 43 +++++++++++++++++++ apps/webapp/app/utils/pathBuilder.ts | 8 +++- 4 files changed, 53 insertions(+), 6 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.$projectParam.settings => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings}/route.tsx (96%) create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.settings.ts diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 0c3dc6387d..34cb5059b3 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -222,7 +222,7 @@ export function SideMenu({ name="Project settings" icon={Cog8ToothIcon} activeIconColor="text-teal-500" - to={v3ProjectSettingsPath(organization, project)} + to={v3ProjectSettingsPath(organization, project, environment)} data-action="project-settings" /> - + This goes in your{" "} trigger.config file. diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.settings.ts b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.settings.ts new file mode 100644 index 0000000000..0948caec68 --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.settings.ts @@ -0,0 +1,43 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { prisma } from "~/db.server"; +import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server"; +import { requireUser } from "~/services/session.server"; +import { ProjectParamSchema, v3ProjectSettingsPath } from "~/utils/pathBuilder"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + + const project = await prisma.project.findFirst({ + where: { + slug: projectParam, + deletedAt: null, + organization: { slug: organizationSlug, members: { some: { userId: user.id } } }, + }, + include: { + environments: { + select: { + id: true, + type: true, + slug: true, + orgMember: { + select: { + userId: true, + }, + }, + }, + }, + }, + }); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const selector = new SelectBestEnvironmentPresenter(); + const environment = await selector.selectBestEnvironment(project.id, user, project.environments); + + return redirect(v3ProjectSettingsPath({ slug: organizationSlug }, project, environment)); +}; diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index df45f50f89..db4e9c9183 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -337,8 +337,12 @@ export function v3BatchRunsPath( return `${v3RunsPath(organization, project, environment, { batchId: batch.friendlyId })}`; } -export function v3ProjectSettingsPath(organization: OrgForPath, project: ProjectForPath) { - return `${v3ProjectPath(organization, project)}/settings`; +export function v3ProjectSettingsPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + return `${v3EnvironmentPath(organization, project, environment)}/settings`; } export function v3DeploymentsPath( From 894b37312f647bc857553900a3688574be525040 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 19:55:43 +0000 Subject: [PATCH 38/95] Fix for scroll area on test page --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index 6c770c63ee..649fdd9124 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -136,7 +136,7 @@ function TaskSelector({ return (
From 7f757cc99b98fd5baac716b0851e50b79ed0874c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 20:09:00 +0000 Subject: [PATCH 39/95] Tweaked the test design --- .../app/components/primitives/RadioButton.tsx | 2 +- .../route.tsx | 34 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/components/primitives/RadioButton.tsx b/apps/webapp/app/components/primitives/RadioButton.tsx index 537b1715ce..928f587afd 100644 --- a/apps/webapp/app/components/primitives/RadioButton.tsx +++ b/apps/webapp/app/components/primitives/RadioButton.tsx @@ -70,7 +70,7 @@ export function RadioButtonCircle({ return (
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index 649fdd9124..0985c29a50 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -161,10 +161,9 @@ function TaskSelector({ - - Task - - File path + + Task ID + Task @@ -198,7 +197,7 @@ function TaskRow({ task }: { task: TaskListItem }) { > @@ -206,21 +205,14 @@ function TaskRow({ task }: { task: TaskListItem }) { -
- -
- - - {task.taskIdentifier} - -
+
+ + + {task.taskIdentifier} +
@@ -229,7 +221,11 @@ function TaskRow({ task }: { task: TaskListItem }) { actionClassName="px-2 py-1" className={cn((isActive || isPending) && "group-hover/table-row:bg-indigo-500/5")} > - {task.filePath} + ); From 9c817f4189e9d9e56a60f413d31e5a803429fe1f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Wed, 12 Mar 2025 20:11:19 +0000 Subject: [PATCH 40/95] Made recent payloads column narrower --- .../route.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 4b1ffa5050..69cb1bde57 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -410,7 +410,7 @@ function ScheduledTaskForm({ value={environment.id} /> - +
From d9556d47a17808eca32c37f1fc5c04a444a3afba Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 11:36:23 +0000 Subject: [PATCH 41/95] Improved the test layout some more --- .../route.tsx | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index 0985c29a50..0ad556c07f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -106,7 +106,7 @@ export default function Page() { ) : (
- + @@ -159,18 +159,17 @@ function TaskSelector({
)}
- + - - Task ID - Task + + Task {filteredItems.length > 0 ? ( filteredItems.map((t) => ) ) : ( - + No tasks match "{filterText}" @@ -197,8 +196,8 @@ function TaskRow({ task }: { task: TaskListItem }) { > @@ -215,18 +214,6 @@ function TaskRow({ task }: { task: TaskListItem }) { - - - - ); } From 14d88e4492b9a50c152e1108e266d13ef1d4cc15 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 15:52:57 +0000 Subject: [PATCH 42/95] Added org icon, new project selector menu --- .../app/components/navigation/SideMenu.tsx | 167 ++++++++++++++---- .../app/components/primitives/Avatar.tsx | 124 +++++++++++++ .../app/components/primitives/Popover.tsx | 11 +- .../OrganizationsPresenter.server.ts | 10 +- .../app/routes/storybook.avatar/route.tsx | 34 ++++ apps/webapp/app/routes/storybook/route.tsx | 6 +- apps/webapp/app/utils/tablerIcons.ts | 2 + .../migration.sql | 2 + .../database/prisma/schema.prisma | 8 +- 9 files changed, 322 insertions(+), 42 deletions(-) create mode 100644 apps/webapp/app/components/primitives/Avatar.tsx create mode 100644 apps/webapp/app/routes/storybook.avatar/route.tsx create mode 100644 internal-packages/database/prisma/migrations/20250313114927_organization_added_avatar/migration.sql diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 34cb5059b3..e8a111f2b2 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -6,8 +6,10 @@ import { ChartBarIcon, ClockIcon, Cog8ToothIcon, + CogIcon, CreditCardIcon, FolderIcon, + FolderOpenIcon, IdentificationIcon, KeyIcon, PlusIcon, @@ -18,9 +20,12 @@ import { } from "@heroicons/react/20/solid"; import { UserGroupIcon } from "@heroicons/react/24/solid"; import { useNavigation } from "@remix-run/react"; -import { Fragment, type ReactNode, useEffect, useRef, useState } from "react"; +import { Fragment, useEffect, useRef, useState, type ReactNode } from "react"; +import simplur from "simplur"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; +import { Avatar } from "~/components/primitives/Avatar"; +import { type MatchedEnvironment } from "~/hooks/useEnvironment"; import { useFeatures } from "~/hooks/useFeatures"; import { type MatchedOrganization } from "~/hooks/useOrganizations"; import { type MatchedProject } from "~/hooks/useProject"; @@ -52,9 +57,9 @@ import { v3UsagePath, } from "~/utils/pathBuilder"; import { ImpersonationBanner } from "../ImpersonationBanner"; -import { LogoIcon } from "../LogoIcon"; import { UserProfilePhoto } from "../UserProfilePhoto"; import { FreePlanUsage } from "../billing/FreePlanUsage"; +import { Paragraph } from "../primitives/Paragraph"; import { Popover, PopoverArrowTrigger, @@ -63,12 +68,13 @@ import { PopoverMenuItem, PopoverSectionHeader, } from "../primitives/Popover"; +import { EnvironmentSelector } from "./EnvironmentSelector"; import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; -import { type MatchedEnvironment } from "~/hooks/useEnvironment"; import { SideMenuSection } from "./SideMenuSection"; -import { EnvironmentSelector } from "./EnvironmentSelector"; +import { LinkButton } from "../primitives/Buttons"; +import { useUser } from "~/hooks/useUser"; type SideMenuUser = Pick & { isImpersonating: boolean }; export type SideMenuProject = Pick< @@ -126,8 +132,12 @@ export function SideMenu({ showHeaderDivider ? " border-grid-bright" : "border-transparent" )} > - - +
{ setOrgMenuOpen(false); }, [navigation.location?.pathname]); @@ -296,53 +318,130 @@ function ProjectSelector({ overflowHidden className="h-7 w-full justify-between overflow-hidden py-1 pl-2" > - - {project.name ?? "Select a project"} +
+ + + + {project.name ?? "Select a project"} + +
+
+
+
+ +
+
+ {organization.title} +
+ {plan && {plan}} + {simplur`${organization.membersCount} member[|s]`} +
+
+
+
+ + + Settings + + + + Usage + +
+
+ {organizations.map((organization) => ( -
- {organization.projects.length > 0 ? ( - organization.projects.map((p) => { - const isSelected = p.id === project.id; - return ( - - {p.name} -
- } - isSelected={isSelected} - icon={FolderIcon} - /> - ); - }) - ) : ( - - )} + {organization.projects.map((p) => { + const isSelected = p.id === project.id; + return ( + + {p.name} +
+ } + isSelected={isSelected} + icon={isSelected ? FolderOpenIcon : FolderIcon} + leadingIconClassName="text-indigo-500" + /> + ); + })} + ))}
- + +
+
+ + {user.isImpersonating && } +
+
+
); } +function SelectorDivider() { + return ( + + + + ); +} + function UserMenu({ user }: { user: SideMenuUser }) { const [isProfileMenuOpen, setProfileMenuOpen] = useState(false); const navigation = useNavigation(); diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx new file mode 100644 index 0000000000..b5c3ba9a9f --- /dev/null +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -0,0 +1,124 @@ +import { + AcademicCapIcon, + BuildingOffice2Icon, + CodeBracketSquareIcon, + CpuChipIcon, + CubeIcon, + FaceSmileIcon, + FireIcon, + HeartIcon, + RocketLaunchIcon, + StarIcon, +} from "@heroicons/react/24/solid"; +import { type Prisma } from "@trigger.dev/database"; +import { z } from "zod"; +import { logger } from "~/services/logger.server"; +import { cn } from "~/utils/cn"; + +const AvatarData = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("icon"), + name: z.string(), + hex: z.string(), + }), + z.object({ + type: z.literal("image"), + url: z.string().url(), + }), +]); + +export type Avatar = z.infer; +export type IconAvatar = Extract; +export type ImageAvatar = Extract; + +export function parseAvatar(json: Prisma.JsonValue, defaultAvatar: Avatar): Avatar { + if (!json || typeof json !== "object") { + return defaultAvatar; + } + + const parsed = AvatarData.safeParse(json); + + if (!parsed.success) { + logger.error("Invalid org avatar", { json, error: parsed.error }); + return defaultAvatar; + } + + return parsed.data; +} + +export function Avatar({ + avatar, + className, + includePadding, +}: { + avatar: Avatar; + className?: string; + includePadding?: boolean; +}) { + switch (avatar.type) { + case "icon": + return ; + case "image": + return ; + } +} + +export const avatarIcons: Record>> = { + "hero:rocket-launch": RocketLaunchIcon, + "hero:cube": CubeIcon, + "hero:academic-cap": AcademicCapIcon, + "hero:code-bracket-square": CodeBracketSquareIcon, + "hero:fire": FireIcon, + "hero:star": StarIcon, + "hero:face-smile": FaceSmileIcon, + "hero:heart": HeartIcon, + "hero:cpu-chip": CpuChipIcon, + "hero:building-office-2": BuildingOffice2Icon, +}; + +export const defaultAvatarColors = [ + "#2563EB", + "#4F46E5", + "#9333EA", + "#DB2777", + "#E11D48", + "#EA580C", + "#EAB308", + "#16A34A", +]; + +export const defaultAvatarIcon: IconAvatar = { + type: "icon", + name: "hero:building-office-2", + hex: defaultAvatarColors[0], +}; + +function AvatarIcon({ + avatar, + className, + includePadding, +}: { + avatar: IconAvatar; + className?: string; + includePadding?: boolean; +}) { + const classes = cn("aspect-square", className); + const style = { + color: avatar.hex, + }; + + const IconComponent = avatarIcons[avatar.name] || defaultAvatarIcon.name; + return ( +
+ +
+ ); +} + +function AvatarImage({ avatar, className }: { avatar: ImageAvatar; className?: string }) { + return ( +
+ Organization avatar +
+ ); +} diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 784bd03ed4..8d2cc4bbbb 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -5,7 +5,7 @@ import { EllipsisVerticalIcon } from "@heroicons/react/24/solid"; import * as PopoverPrimitive from "@radix-ui/react-popover"; import * as React from "react"; import { DropdownIcon } from "~/assets/icons/DropdownIcon"; -import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; +import * as useShortcutKeys from "~/hooks/useShortcutKeys"; import { cn } from "~/utils/cn"; import { type ButtonContentPropsType, LinkButton } from "./Buttons"; import { Paragraph, type ParagraphVariant } from "./Paragraph"; @@ -110,11 +110,12 @@ function PopoverSideMenuTrigger({ className, shortcut, ...props -}: { isOpen?: boolean; shortcut?: ShortcutDefinition } & React.ComponentPropsWithoutRef< - typeof PopoverTrigger ->) { +}: { + isOpen?: boolean; + shortcut?: useShortcutKeys.ShortcutDefinition; +} & React.ComponentPropsWithoutRef) { const ref = React.useRef(null); - useShortcutKeys({ + useShortcutKeys.useShortcutKeys({ shortcut: shortcut, action: (e) => { e.preventDefault(); diff --git a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts index 6b6164a309..dff53b4115 100644 --- a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts +++ b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts @@ -1,12 +1,12 @@ import { type PrismaClient } from "@trigger.dev/database"; import { redirect } from "remix-typedjson"; import { prisma } from "~/db.server"; -import { type UserWithDashboardPreferences } from "~/models/user.server"; import { logger } from "~/services/logger.server"; import { type UserFromSession } from "~/services/session.server"; import { newOrganizationPath, newProjectPath } from "~/utils/pathBuilder"; import { type MinimumEnvironment } from "./SelectBestEnvironmentPresenter.server"; import { sortEnvironments } from "~/utils/environmentSort"; +import { defaultAvatarIcon, parseAvatar } from "~/components/primitives/Avatar"; export class OrganizationsPresenter { #prismaClient: PrismaClient; @@ -122,6 +122,7 @@ export class OrganizationsPresenter { id: true, slug: true, title: true, + avatar: true, projects: { where: { deletedAt: null, version: "V3" }, select: { @@ -132,6 +133,11 @@ export class OrganizationsPresenter { }, orderBy: { name: "asc" }, }, + _count: { + select: { + members: true, + }, + }, }, }); @@ -140,12 +146,14 @@ export class OrganizationsPresenter { id: org.id, slug: org.slug, title: org.title, + avatar: parseAvatar(org.avatar, defaultAvatarIcon), projects: org.projects.map((project) => ({ id: project.id, slug: project.slug, name: project.name, updatedAt: project.updatedAt, })), + membersCount: org._count.members, }; }); } diff --git a/apps/webapp/app/routes/storybook.avatar/route.tsx b/apps/webapp/app/routes/storybook.avatar/route.tsx new file mode 100644 index 0000000000..a5331ac4ed --- /dev/null +++ b/apps/webapp/app/routes/storybook.avatar/route.tsx @@ -0,0 +1,34 @@ +import { Avatar, avatarIcons, defaultAvatarColors } from "~/components/primitives/Avatar"; + +// Map tablerIcons Set to Avatar array with cycling colors +const avatars: Avatar[] = Object.entries(avatarIcons).map(([iconName], index) => ({ + type: "icon", + name: iconName, + hex: defaultAvatarColors[index % defaultAvatarColors.length], // Cycle through colors +})); + +export default function Story() { + return ( +
+ {/* Left grid - size-8 */} +
+

Size 8

+
+ {avatars.map((avatar, index) => ( + + ))} +
+
+ + {/* Right grid - size-12 */} +
+

Size 12

+
+ {avatars.map((avatar, index) => ( + + ))} +
+
+
+ ); +} diff --git a/apps/webapp/app/routes/storybook/route.tsx b/apps/webapp/app/routes/storybook/route.tsx index aea6f80e91..bd451f6147 100644 --- a/apps/webapp/app/routes/storybook/route.tsx +++ b/apps/webapp/app/routes/storybook/route.tsx @@ -1,5 +1,5 @@ import { NavLink, Outlet } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Fragment } from "react"; import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; import { AppContainer } from "~/components/layout/AppLayout"; @@ -8,6 +8,10 @@ import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; const stories: Story[] = [ + { + name: "Avatar", + slug: "avatar", + }, { name: "Badges", slug: "badges", diff --git a/apps/webapp/app/utils/tablerIcons.ts b/apps/webapp/app/utils/tablerIcons.ts index 559f08356a..e188e779bf 100644 --- a/apps/webapp/app/utils/tablerIcons.ts +++ b/apps/webapp/app/utils/tablerIcons.ts @@ -4820,3 +4820,5 @@ const tablerIconNames = [ ]; export const tablerIcons = new Set(tablerIconNames); + +export const tablerIconsFilled = new Set(tablerIconNames.filter((i) => i.endsWith("-filled"))); diff --git a/internal-packages/database/prisma/migrations/20250313114927_organization_added_avatar/migration.sql b/internal-packages/database/prisma/migrations/20250313114927_organization_added_avatar/migration.sql new file mode 100644 index 0000000000..f1dcc4e07c --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250313114927_organization_added_avatar/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Organization" ADD COLUMN "avatar" JSONB; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index f9799cbb69..0398103546 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -131,11 +131,17 @@ model Organization { companySize String? + avatar Json? + runsEnabled Boolean @default(true) - v3Enabled Boolean @default(false) + v3Enabled Boolean @default(false) + + /// @deprecated v2Enabled Boolean @default(false) + /// @deprecated v2MarqsEnabled Boolean @default(false) + /// @deprecated hasRequestedV3 Boolean @default(false) environments RuntimeEnvironment[] From 9f7b783c5d75de9ad1312c6d596cdceddeaa3799 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 16:08:14 +0000 Subject: [PATCH 43/95] WIP on org switching menu --- .../app/components/navigation/SideMenu.tsx | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index e8a111f2b2..c3b8aa96cc 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -1,9 +1,11 @@ import { AcademicCapIcon, + ArrowPathRoundedSquareIcon, ArrowRightOnRectangleIcon, BeakerIcon, BellAlertIcon, ChartBarIcon, + ChevronRightIcon, ClockIcon, Cog8ToothIcon, CogIcon, @@ -38,6 +40,7 @@ import { logoutPath, newOrganizationPath, newProjectPath, + organizationPath, organizationSettingsPath, organizationTeamPath, personalAccessTokensPath, @@ -67,13 +70,14 @@ import { PopoverCustomTrigger, PopoverMenuItem, PopoverSectionHeader, + PopoverTrigger, } from "../primitives/Popover"; import { EnvironmentSelector } from "./EnvironmentSelector"; import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; import { SideMenuSection } from "./SideMenuSection"; -import { LinkButton } from "../primitives/Buttons"; +import { ButtonContent, LinkButton } from "../primitives/Buttons"; import { useUser } from "~/hooks/useUser"; type SideMenuUser = Pick & { isImpersonating: boolean }; @@ -395,12 +399,7 @@ function ProjectSelector({ ))}
- +
{ + setMenuOpen(false); + }, [navigation.location?.pathname]); + + return ( + setMenuOpen(open)} open={isMenuOpen}> + + + Switch organization + + + + {organizations.map((org) => ( + + ))} + + + + ); +} + function SelectorDivider() { return ( From b2957721d0f31d86736864c7a663e2ba20841e5f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 16:22:54 +0000 Subject: [PATCH 44/95] Org switching is working --- .../app/components/navigation/SideMenu.tsx | 201 ++++++++---------- 1 file changed, 83 insertions(+), 118 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index c3b8aa96cc..c4e6682856 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -369,35 +369,26 @@ function ProjectSelector({
- - {organizations.map((organization) => ( - -
- {organization.projects.map((p) => { - const isSelected = p.id === project.id; - return ( - - {p.name} -
- } - isSelected={isSelected} - icon={isSelected ? FolderOpenIcon : FolderIcon} - leadingIconClassName="text-indigo-500" - /> - ); - })} +
+ {organization.projects.map((p) => { + const isSelected = p.id === project.id; + return ( + {p.name} +
+ } + isSelected={isSelected} + icon={isSelected ? FolderOpenIcon : FolderIcon} + leadingIconClassName="text-indigo-500" /> - -
- ))} + ); + })} + +
@@ -435,43 +426,79 @@ function SwitchOrganizations({ }) { const navigation = useNavigation(); const [isMenuOpen, setMenuOpen] = useState(false); + const timeoutRef = useRef(null); + + // Clear timeout on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); useEffect(() => { setMenuOpen(false); }, [navigation.location?.pathname]); + const handleMouseEnter = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + setMenuOpen(true); + }; + + const handleMouseLeave = () => { + // Small delay before closing to allow moving to the content + timeoutRef.current = setTimeout(() => { + setMenuOpen(false); + }, 150); + }; + return ( setMenuOpen(open)} open={isMenuOpen}> - - - Switch organization - - - - {organizations.map((org) => ( - + + - ))} - - + TrailingIcon={ChevronRightIcon} + trailingIconClassName="text-text-dimmed" + textAlignLeft + fullWidth + > + Switch organization + + + +
+ {organizations.map((org) => ( + + ))} +
+
+ +
+
+
); } @@ -490,65 +517,3 @@ function SelectorDivider() { ); } - -function UserMenu({ user }: { user: SideMenuUser }) { - const [isProfileMenuOpen, setProfileMenuOpen] = useState(false); - const navigation = useNavigation(); - const { v3Enabled } = useFeatures(); - - useEffect(() => { - setProfileMenuOpen(false); - }, [navigation.location?.pathname]); - - return ( - setProfileMenuOpen(open)}> - - - - - - -
- {user.isImpersonating && } - {user.admin && ( - - )} - - {v3Enabled && ( - - )} - -
-
-
-
- ); -} From 8ea3cadade1a6bad2a9c36b74aa251a3dc38849a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 16:41:27 +0000 Subject: [PATCH 45/95] New menu working well, removed old side menu items --- .../app/components/navigation/SideMenu.tsx | 59 +++++++------------ .../app/components/primitives/Avatar.tsx | 8 +-- .../app/components/primitives/TextLink.tsx | 4 +- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index c4e6682856..62fa2f967a 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -79,6 +79,7 @@ import { SideMenuItem } from "./SideMenuItem"; import { SideMenuSection } from "./SideMenuSection"; import { ButtonContent, LinkButton } from "../primitives/Buttons"; import { useUser } from "~/hooks/useUser"; +import { TextLink } from "../primitives/TextLink"; type SideMenuUser = Pick & { isImpersonating: boolean }; export type SideMenuProject = Pick< @@ -239,39 +240,6 @@ export function SideMenu({ to={v3ProjectSettingsPath(organization, project, environment)} data-action="project-settings" /> - - - - @@ -320,15 +288,15 @@ function ProjectSelector({ -
+ {project.name ?? "Select a project"} -
+
{organization.title}
- {plan && {plan}} - {simplur`${organization.membersCount} member[|s]`} + {plan && ( + + + {plan} + + + )} + + {simplur`${organization.membersCount} member[|s]`} +
@@ -461,6 +440,7 @@ function SwitchOrganizations({ @@ -486,6 +468,7 @@ function SwitchOrganizations({ to={organizationPath(org)} title={org.title} leadingIconClassName="text-text-dimmed" + isSelected={org.id === organization.id} /> ))} diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index b5c3ba9a9f..7182ac86bc 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -109,16 +109,16 @@ function AvatarIcon({ const IconComponent = avatarIcons[avatar.name] || defaultAvatarIcon.name; return ( -
+ -
+ ); } function AvatarImage({ avatar, className }: { avatar: ImageAvatar; className?: string }) { return ( -
+ Organization avatar -
+ ); } diff --git a/apps/webapp/app/components/primitives/TextLink.tsx b/apps/webapp/app/components/primitives/TextLink.tsx index e4314d4b0f..38fd1525c5 100644 --- a/apps/webapp/app/components/primitives/TextLink.tsx +++ b/apps/webapp/app/components/primitives/TextLink.tsx @@ -1,12 +1,12 @@ import { Link } from "@remix-run/react"; import { cn } from "~/utils/cn"; -import { Icon, RenderIcon } from "./Icon"; +import { Icon, type RenderIcon } from "./Icon"; const variations = { primary: "text-indigo-500 transition hover:text-indigo-400 inline-flex gap-0.5 items-center group focus-visible:focus-custom", secondary: - "text-text-dimmed transition underline underline-offset-2 decoration-dimmed/50 hover:decoration-dimmed inline-flex gap-0.5 items-center group focus-visible:focus-custom", + "text-text-dimmed transition hover:text-text-bright inline-flex gap-0.5 items-center group focus-visible:focus-custom", } as const; type TextLinkProps = { From 5d747d0548855a0cbf7557e3ca857ee065dade5a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 16:55:08 +0000 Subject: [PATCH 46/95] Buttons can now have a component name or an actual component for their icons --- .../app/components/navigation/SideMenu.tsx | 3 ++- .../app/components/primitives/Buttons.tsx | 17 ++++++++++------- apps/webapp/app/components/primitives/Icon.tsx | 2 +- .../app/components/primitives/Popover.tsx | 3 ++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 62fa2f967a..c0a5fcc2f9 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -290,7 +290,7 @@ function ProjectSelector({ overflowHidden className="h-8 w-full justify-between overflow-hidden py-1 pl-2" > - + @@ -467,6 +467,7 @@ function SwitchOrganizations({ key={org.id} to={organizationPath(org)} title={org.title} + icon={} leadingIconClassName="text-text-dimmed" isSelected={org.id === organization.id} /> diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 9697b77699..c5244171f8 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -1,9 +1,10 @@ -import { Link, LinkProps, NavLink, NavLinkProps } from "@remix-run/react"; -import React, { forwardRef, ReactNode, useImperativeHandle, useRef } from "react"; -import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { Link, type LinkProps, NavLink, type NavLinkProps } from "@remix-run/react"; +import React, { forwardRef, type ReactNode, useImperativeHandle, useRef } from "react"; +import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys"; import { cn } from "~/utils/cn"; import { ShortcutKey } from "./ShortcutKey"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip"; +import { Icon, type RenderIcon } from "./Icon"; const sizes = { small: { @@ -164,8 +165,8 @@ const allVariants = { export type ButtonContentPropsType = { children?: React.ReactNode; - LeadingIcon?: React.ComponentType; - TrailingIcon?: React.ComponentType; + LeadingIcon?: RenderIcon; + TrailingIcon?: RenderIcon; trailingIconClassName?: string; leadingIconClassName?: string; fullWidth?: boolean; @@ -220,7 +221,8 @@ export function ButtonContent(props: ButtonContentPropsType) { )} > {LeadingIcon && ( - | React.ReactNode; diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 8d2cc4bbbb..145a58d90b 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -10,6 +10,7 @@ import { cn } from "~/utils/cn"; import { type ButtonContentPropsType, LinkButton } from "./Buttons"; import { Paragraph, type ParagraphVariant } from "./Paragraph"; import { ShortcutKey } from "./ShortcutKey"; +import { RenderIcon } from "./Icon"; const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; @@ -60,7 +61,7 @@ function PopoverMenuItem({ leadingIconClassName, }: { to: string; - icon?: React.ComponentType; + icon?: RenderIcon; title: React.ReactNode; isSelected?: boolean; variant?: ButtonContentPropsType; From 2a18f1ea6d4355e6f44884be1ad5ea422c1f39ef Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 17:06:25 +0000 Subject: [PATCH 47/95] Removed the Projects page, instead redirect appropriately --- .../OrganizationsPresenter.server.ts | 46 +------ .../SelectBestEnvironmentPresenter.server.ts | 39 ++++++ .../route.tsx | 114 ++++++++---------- 3 files changed, 94 insertions(+), 105 deletions(-) diff --git a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts index dff53b4115..5377d8532a 100644 --- a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts +++ b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts @@ -4,7 +4,10 @@ import { prisma } from "~/db.server"; import { logger } from "~/services/logger.server"; import { type UserFromSession } from "~/services/session.server"; import { newOrganizationPath, newProjectPath } from "~/utils/pathBuilder"; -import { type MinimumEnvironment } from "./SelectBestEnvironmentPresenter.server"; +import { + SelectBestEnvironmentPresenter, + type MinimumEnvironment, +} from "./SelectBestEnvironmentPresenter.server"; import { sortEnvironments } from "~/utils/environmentSort"; import { defaultAvatarIcon, parseAvatar } from "~/components/primitives/Avatar"; @@ -47,7 +50,8 @@ export class OrganizationsPresenter { throw new Response("Organization not Found", { status: 404 }); } - const bestProject = this.#getProject({ + const selector = new SelectBestEnvironmentPresenter(); + const bestProject = selector.selectBestProjectFromProjects({ user, projectSlug, projects: organization.projects, @@ -158,44 +162,6 @@ export class OrganizationsPresenter { }); } - #getProject({ - user, - projectSlug, - projects, - }: { - user: UserFromSession; - projectSlug: string | undefined; - projects: { - id: string; - slug: string; - name: string; - updatedAt: Date; - }[]; - }) { - if (projectSlug) { - const proj = projects.find((p) => p.slug === projectSlug); - if (proj) { - return proj; - } - - if (!proj) { - logger.info("Not Found: project", { - projectSlug, - projects, - }); - } - } - - const currentProjectId = user.dashboardPreferences.currentProjectId; - const project = projects.find((p) => p.id === currentProjectId); - if (project) { - return project; - } - - //most recently updated - return projects.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()).at(0); - } - #getEnvironment({ user, projectId, diff --git a/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts b/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts index 0bce257ca5..5ca3458b99 100644 --- a/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts +++ b/apps/webapp/app/presenters/SelectBestEnvironmentPresenter.server.ts @@ -1,5 +1,6 @@ import { type RuntimeEnvironment, type PrismaClient } from "@trigger.dev/database"; import { prisma } from "~/db.server"; +import { logger } from "~/services/logger.server"; import { type UserFromSession } from "~/services/session.server"; export type MinimumEnvironment = Pick & { @@ -94,6 +95,44 @@ export class SelectBestEnvironmentPresenter { return { project: projects[0], organization: projects[0].organization }; } + async selectBestProjectFromProjects({ + user, + projectSlug, + projects, + }: { + user: UserFromSession; + projectSlug: string | undefined; + projects: { + id: string; + slug: string; + name: string; + updatedAt: Date; + }[]; + }) { + if (projectSlug) { + const proj = projects.find((p) => p.slug === projectSlug); + if (proj) { + return proj; + } + + if (!proj) { + logger.info("Not Found: project", { + projectSlug, + projects, + }); + } + } + + const currentProjectId = user.dashboardPreferences.currentProjectId; + const project = projects.find((p) => p.id === currentProjectId); + if (project) { + return project; + } + + //most recently updated + return projects.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()).at(0); + } + async selectBestEnvironment( projectId: string, user: UserFromSession, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx index f669b0db18..c8b725e835 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug._index/route.tsx @@ -1,69 +1,53 @@ -import { FolderIcon } from "@heroicons/react/20/solid"; -import { Link, type MetaFunction } from "@remix-run/react"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { Badge } from "~/components/primitives/Badge"; -import { LinkButton } from "~/components/primitives/Buttons"; -import { Header3 } from "~/components/primitives/Headers"; -import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { newProjectPath, v3ProjectPath } from "~/utils/pathBuilder"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { prisma } from "~/db.server"; +import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server"; +import { logger } from "~/services/logger.server"; +import { requireUser } from "~/services/session.server"; +import { + newOrganizationPath, + newProjectPath, + OrganizationParamsSchema, + v3ProjectPath, +} from "~/utils/pathBuilder"; -export const meta: MetaFunction = () => { - return [ - { - title: "Projects | Trigger.dev", +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const { organizationSlug } = OrganizationParamsSchema.parse(params); + + const org = await prisma.organization.findFirst({ + where: { slug: organizationSlug, members: { some: { userId: user.id } }, deletedAt: null }, + orderBy: { createdAt: "desc" }, + select: { + projects: { + where: { deletedAt: null, version: "V3" }, + select: { + id: true, + slug: true, + name: true, + updatedAt: true, + }, + orderBy: { name: "asc" }, + }, }, - ]; -}; + }); -export default function Page() { - const organization = useOrganization(); + if (!org) { + throw redirect(newOrganizationPath()); + } - return ( - - - - - - Org UID: {organization.id} - - - Create a new project - - - - -
    - {organization.projects.length > 0 ? ( - organization.projects.map((project) => { - return ( -
  • - - -
    - {project.name} -
    - -
  • - ); - }) - ) : ( -
  • - - Create a Project - -
  • - )} -
-
-
- ); -} + const selector = new SelectBestEnvironmentPresenter(); + const bestProject = await selector.selectBestProjectFromProjects({ + user, + projectSlug: undefined, + projects: org.projects, + }); + if (!bestProject) { + logger.info("Not Found: project", { + request, + project: bestProject, + }); + throw redirect(newProjectPath({ slug: organizationSlug })); + } + + return redirect(v3ProjectPath({ slug: organizationSlug }, bestProject)); +}; From 4660699ca7c3428f3d9ecd3aa461316f0bb38d11 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:15:47 +0000 Subject: [PATCH 48/95] Fix for broken blank states --- apps/webapp/app/components/BlankStatePanels.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index 13fe257a2a..d272ca3e4c 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -205,6 +205,7 @@ export function TestHasNoTasks() { export function DeploymentsNone() { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); return ( + set your environment variables {" "} first. @@ -247,6 +248,7 @@ export function DeploymentsNone() { export function DeploymentsNoneDev() { const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); return (
@@ -264,7 +266,7 @@ export function DeploymentsNoneDev() { There are several ways to deploy your tasks. You can use the CLI, Continuous Integration (like GitHub Actions), or an integration with a service like Netlify or Vercel. Make sure you{" "} - + set your environment variables {" "} first. From 1a4c3d355ebfedbef1d44ecef50ff713ea0e9e7e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:15:56 +0000 Subject: [PATCH 49/95] Minor run table improvements --- .../app/components/runs/v3/TaskRunsTable.tsx | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 8c9a0f14de..f948a7e3e9 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -18,7 +18,6 @@ import { Header3 } from "~/components/primitives/Headers"; import { PopoverMenuItem } from "~/components/primitives/Popover"; import { useSelectedItems } from "~/components/primitives/SelectedItemsProvider"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; -import { useEnvironments } from "~/hooks/useEnvironments"; import { useFeatures } from "~/hooks/useFeatures"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; @@ -284,7 +283,7 @@ export function TaskRunsTable({ {total === 0 && !hasFilters ? ( - + {!isLoading && } ) : runs.length === 0 ? ( @@ -543,7 +542,7 @@ function BlankState({ isLoading, filters }: Pick; + if (isLoading) return ; const { environments, tasks, from, to, ...otherFilters } = filters; @@ -554,43 +553,35 @@ function BlankState({ isLoading, filters }: Pick filterArray.length === 0) ) { return ( - -
- - There are no runs for {filters.tasks[0]} - {environment ? ( - <> - {" "} - in - - ) : null} - -
- - Create a test run - - or - - Triggering a task docs - -
+ + + There are no runs for {filters.tasks[0]} + +
+ + Create a test run + + or + + Triggering a task docs +
); } return ( - +
No runs match your filters. Try refreshing, modifying your filters or run a test. From f70d8ae0c71d229e0881351fab09081c7453011f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:16:05 +0000 Subject: [PATCH 50/95] Removed unused switcher log and logic --- apps/webapp/app/hooks/useEnvironmentSwitcher.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts index 0429026452..9c0d02a7b6 100644 --- a/apps/webapp/app/hooks/useEnvironmentSwitcher.ts +++ b/apps/webapp/app/hooks/useEnvironmentSwitcher.ts @@ -1,4 +1,4 @@ -import { Path, useMatch, useMatches, type Location } from "@remix-run/react"; +import { type Path, useMatches } from "@remix-run/react"; import { type MinimumEnvironment } from "~/presenters/SelectBestEnvironmentPresenter.server"; import { useEnvironment } from "./useEnvironment"; import { useEnvironments } from "./useEnvironments"; @@ -9,18 +9,9 @@ import { useOptimisticLocation } from "./useOptimisticLocation"; * @returns */ export function useEnvironmentSwitcher() { - const environments = useEnvironments(); - const existingEnvironment = useEnvironment(); const matches = useMatches(); const location = useOptimisticLocation(); - console.log({ - environments, - existingEnvironment, - matches, - location, - }); - const urlForEnvironment = (newEnvironment: MinimumEnvironment) => { return routeForEnvironmentSwitch({ location, From bb14d6ad034d6103a68e09811af7ee64a5e7ce2a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:16:21 +0000 Subject: [PATCH 51/95] Concurrency page fix for invalid html, improved layout --- .../route.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx index 3ea3c4b0b0..47826c609f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx @@ -95,7 +95,17 @@ export default function Page() { - }> + + +
+ +
+
+ + } + > Error loading environments

}> {(environments) => }
From edd762916fa372a590675f30d6e43d3cde9ba829 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:16:45 +0000 Subject: [PATCH 52/95] Minor improvements --- .../components/navigation/AccountSideMenu.tsx | 78 +++--- .../app/components/navigation/SideMenu.tsx | 241 +++++++++--------- .../OrganizationsPresenter.server.ts | 2 +- .../route.tsx | 14 +- 4 files changed, 151 insertions(+), 184 deletions(-) diff --git a/apps/webapp/app/components/navigation/AccountSideMenu.tsx b/apps/webapp/app/components/navigation/AccountSideMenu.tsx index 4955d380f9..1026a80c30 100644 --- a/apps/webapp/app/components/navigation/AccountSideMenu.tsx +++ b/apps/webapp/app/components/navigation/AccountSideMenu.tsx @@ -1,7 +1,6 @@ import { ShieldCheckIcon, UserCircleIcon } from "@heroicons/react/20/solid"; import { ArrowLeftIcon } from "@heroicons/react/24/solid"; -import { User } from "@trigger.dev/database"; -import { useFeatures } from "~/hooks/useFeatures"; +import { type User } from "@trigger.dev/database"; import { cn } from "~/utils/cn"; import { accountPath, personalAccessTokensPath, rootPath } from "~/utils/pathBuilder"; import { LinkButton } from "../primitives/Buttons"; @@ -10,54 +9,43 @@ import { SideMenuItem } from "./SideMenuItem"; import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; export function AccountSideMenu({ user }: { user: User }) { - const { v3Enabled } = useFeatures(); - return (
-
-
- - Account - -
-
-
- - - -
- {v3Enabled && ( -
- - -
- )} -
-
- -
+
+ + Back to app + +
+
+ + + +
+
+
); diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index c0a5fcc2f9..da97b5a73c 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -1,5 +1,4 @@ import { - AcademicCapIcon, ArrowPathRoundedSquareIcon, ArrowRightOnRectangleIcon, BeakerIcon, @@ -9,7 +8,6 @@ import { ClockIcon, Cog8ToothIcon, CogIcon, - CreditCardIcon, FolderIcon, FolderOpenIcon, IdentificationIcon, @@ -17,18 +15,15 @@ import { PlusIcon, RectangleStackIcon, ServerStackIcon, - ShieldCheckIcon, Squares2X2Icon, } from "@heroicons/react/20/solid"; -import { UserGroupIcon } from "@heroicons/react/24/solid"; import { useNavigation } from "@remix-run/react"; -import { Fragment, useEffect, useRef, useState, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; import simplur from "simplur"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { Avatar } from "~/components/primitives/Avatar"; import { type MatchedEnvironment } from "~/hooks/useEnvironment"; -import { useFeatures } from "~/hooks/useFeatures"; import { type MatchedOrganization } from "~/hooks/useOrganizations"; import { type MatchedProject } from "~/hooks/useProject"; import { type User } from "~/models/user.server"; @@ -43,7 +38,6 @@ import { organizationPath, organizationSettingsPath, organizationTeamPath, - personalAccessTokensPath, v3ApiKeysPath, v3BatchesPath, v3BillingPath, @@ -67,9 +61,7 @@ import { Popover, PopoverArrowTrigger, PopoverContent, - PopoverCustomTrigger, PopoverMenuItem, - PopoverSectionHeader, PopoverTrigger, } from "../primitives/Popover"; import { EnvironmentSelector } from "./EnvironmentSelector"; @@ -78,7 +70,6 @@ import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; import { SideMenuSection } from "./SideMenuSection"; import { ButtonContent, LinkButton } from "../primitives/Buttons"; -import { useUser } from "~/hooks/useUser"; import { TextLink } from "../primitives/TextLink"; type SideMenuUser = Pick & { isImpersonating: boolean }; @@ -127,132 +118,130 @@ export function SideMenu({ return (
-
-
- -
-
-
-
- -
- -
+
+ +
+
+
+
+ +
+
+
-
- - - - - -
+
+ + + + + +
- - - - + + + + - - - + + + - - - -
-
-
- - {isFreeUser && ( - + - )} +
+
+ + {isFreeUser && ( + + )} +
); } diff --git a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts index 5377d8532a..b6cea9afe9 100644 --- a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts +++ b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts @@ -51,7 +51,7 @@ export class OrganizationsPresenter { } const selector = new SelectBestEnvironmentPresenter(); - const bestProject = selector.selectBestProjectFromProjects({ + const bestProject = await selector.selectBestProjectFromProjects({ user, projectSlug, projects: organization.projects, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index 4722ab788d..5e98d58187 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -3,7 +3,6 @@ import { ArrowUturnLeftIcon, ArrowUturnRightIcon, BookOpenIcon, - ServerStackIcon, } from "@heroicons/react/20/solid"; import { type MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; @@ -11,13 +10,12 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels"; import { UserAvatar } from "~/components/UserProfilePhoto"; -import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; -import { InfoPanel } from "~/components/primitives/InfoPanel"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; @@ -36,7 +34,6 @@ import { TableHeaderCell, TableRow, } from "~/components/primitives/Table"; -import { TextLink } from "~/components/primitives/TextLink"; import { DeploymentStatus, deploymentStatusDescription, @@ -50,19 +47,12 @@ import { import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { useUser } from "~/hooks/useUser"; import { type DeploymentListItem, DeploymentListPresenter, } from "~/presenters/v3/DeploymentListPresenter.server"; import { requireUserId } from "~/services/session.server"; -import { - EnvironmentParamSchema, - ProjectParamSchema, - docsPath, - v3DeploymentPath, - v3EnvironmentVariablesPath, -} from "~/utils/pathBuilder"; +import { EnvironmentParamSchema, docsPath, v3DeploymentPath } from "~/utils/pathBuilder"; import { createSearchParams } from "~/utils/searchParams"; import { deploymentIndexingIsRetryable } from "~/v3/deploymentStatus"; import { compareDeploymentVersions } from "~/v3/utils/deploymentVersions"; From 482d4eac1779d5a6a9fdfd682672e4a5134ec7be Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:17:00 +0000 Subject: [PATCH 53/95] Moved the side menu to the project level --- .../route.tsx | 26 +++++++++++++++++-- .../_app.orgs.$organizationSlug/route.tsx | 21 +-------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx index 938af52804..07311cc5de 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx @@ -1,7 +1,11 @@ import { Outlet } from "@remix-run/react"; import { RouteErrorDisplay } from "~/components/ErrorDisplay"; -import { useOrganization } from "~/hooks/useOrganizations"; +import { MainBody } from "~/components/layout/AppLayout"; +import { SideMenu } from "~/components/navigation/SideMenu"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useIsImpersonating, useOrganization, useOrganizations } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; +import { useUser } from "~/hooks/useUser"; import { type Handle } from "~/utils/handle"; import { v3ProjectPath } from "~/utils/pathBuilder"; @@ -15,9 +19,27 @@ export const handle: Handle = { }; export default function Project() { + const organizations = useOrganizations(); + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const user = useUser(); + const isImpersonating = useIsImpersonating(); + return ( <> - +
+ + + + +
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx index 9441044e8b..8c15309cd9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx @@ -107,26 +107,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; export default function Organization() { - const { organization, project, organizations, environment, isImpersonating } = - useTypedLoaderData(); - const user = useUser(); - - return ( - <> -
- - - - -
- - ); + return ; } export function ErrorBoundary() { From b96d371f290f1121f5c9009897f1348618eae1f6 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:24:39 +0000 Subject: [PATCH 54/95] Improved account styling --- .../components/navigation/AccountSideMenu.tsx | 2 +- .../app/routes/account._index/route.tsx | 100 +++++++++--------- 2 files changed, 52 insertions(+), 50 deletions(-) diff --git a/apps/webapp/app/components/navigation/AccountSideMenu.tsx b/apps/webapp/app/components/navigation/AccountSideMenu.tsx index 1026a80c30..4971a41fbc 100644 --- a/apps/webapp/app/components/navigation/AccountSideMenu.tsx +++ b/apps/webapp/app/components/navigation/AccountSideMenu.tsx @@ -12,7 +12,7 @@ export function AccountSideMenu({ user }: { user: User }) { return (
diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx index 96d95a9aee..247fa4124c 100644 --- a/apps/webapp/app/routes/account._index/route.tsx +++ b/apps/webapp/app/routes/account._index/route.tsx @@ -1,11 +1,11 @@ import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { EnvelopeIcon, UserCircleIcon } from "@heroicons/react/20/solid"; -import { Form, MetaFunction, useActionData } from "@remix-run/react"; -import { ActionFunction, json } from "@remix-run/server-runtime"; +import { Form, type MetaFunction, useActionData } from "@remix-run/react"; +import { type ActionFunction, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { UserProfilePhoto } from "~/components/UserProfilePhoto"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button } from "~/components/primitives/Buttons"; import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; import { Fieldset } from "~/components/primitives/Fieldset"; @@ -138,54 +138,56 @@ export default function Page() { -
- - - - -
- - - - Your teammates will see this - {name.error} - - - - - {email.error} +
+ + + + - - - + + + + Your teammates will see this + {name.error} + + + + + {email.error} + + + + + {marketingEmails.error} + + + + Update + + } /> - {marketingEmails.error} - - - - Update - - } - /> -
- + + +
); From 65e87292ae865e98d450fc4c5009463f765204ae Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 20:41:45 +0000 Subject: [PATCH 55/95] Moved org settings pages (with redirects) --- .../OrganizationSettingsSideMenu.tsx | 86 ++++++ .../route.tsx | 272 +++++++++++++++++ .../route.tsx | 6 +- .../route.tsx | 0 .../route.tsx | 8 +- .../route.tsx | 275 +----------------- .../routes/orgs.$organizationSlug.billing.ts | 7 + .../app/routes/orgs.$organizationSlug.team.ts | 7 + .../routes/orgs.$organizationSlug.usage.ts | 7 + apps/webapp/app/utils/pathBuilder.ts | 6 +- 10 files changed, 400 insertions(+), 274 deletions(-) create mode 100644 apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.billing => _app.orgs.$organizationSlug.settings.billing}/route.tsx (96%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.team => _app.orgs.$organizationSlug.settings.team}/route.tsx (100%) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.usage => _app.orgs.$organizationSlug.settings.usage}/route.tsx (98%) create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.billing.ts create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.team.ts create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.usage.ts diff --git a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx new file mode 100644 index 0000000000..7917e03e04 --- /dev/null +++ b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx @@ -0,0 +1,86 @@ +import { + ChartBarIcon, + Cog8ToothIcon, + CreditCardIcon, + UserGroupIcon, +} from "@heroicons/react/20/solid"; +import { ArrowLeftIcon } from "@heroicons/react/24/solid"; +import { useFeatures } from "~/hooks/useFeatures"; +import { type MatchedOrganization } from "~/hooks/useOrganizations"; +import { cn } from "~/utils/cn"; +import { + organizationSettingsPath, + organizationTeamPath, + rootPath, + v3BillingPath, + v3UsagePath, +} from "~/utils/pathBuilder"; +import { LinkButton } from "../primitives/Buttons"; +import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; +import { SideMenuHeader } from "./SideMenuHeader"; +import { SideMenuItem } from "./SideMenuItem"; + +export function OrganizationSettingsSideMenu({ + organization, +}: { + organization: MatchedOrganization; +}) { + const { isManagedCloud } = useFeatures(); + + return ( +
+
+ + Back to app + +
+
+ + + {isManagedCloud && ( + + )} + + +
+
+ +
+
+ ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx new file mode 100644 index 0000000000..89b4782b6d --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -0,0 +1,272 @@ +import { conform, useForm } from "@conform-to/react"; +import { parse } from "@conform-to/zod"; +import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid"; +import { Form, type MetaFunction, useActionData, useNavigation } from "@remix-run/react"; +import { type ActionFunction, json } from "@remix-run/server-runtime"; +import { redirect } from "remix-typedjson"; +import { z } from "zod"; +import { InlineCode } from "~/components/code/InlineCode"; +import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { Button } from "~/components/primitives/Buttons"; +import { Fieldset } from "~/components/primitives/Fieldset"; +import { FormButtons } from "~/components/primitives/FormButtons"; +import { FormError } from "~/components/primitives/FormError"; +import { Header2 } from "~/components/primitives/Headers"; +import { Hint } from "~/components/primitives/Hint"; +import { Input } from "~/components/primitives/Input"; +import { InputGroup } from "~/components/primitives/InputGroup"; +import { Label } from "~/components/primitives/Label"; +import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; +import { SpinnerWhite } from "~/components/primitives/Spinner"; +import { prisma } from "~/db.server"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; +import { clearCurrentProject } from "~/services/dashboardPreferences.server"; +import { DeleteOrganizationService } from "~/services/deleteOrganization.server"; +import { logger } from "~/services/logger.server"; +import { requireUser, requireUserId } from "~/services/session.server"; +import { organizationPath, organizationSettingsPath, rootPath } from "~/utils/pathBuilder"; + +export const meta: MetaFunction = () => { + return [ + { + title: `Organization settings | Trigger.dev`, + }, + ]; +}; + +export function createSchema( + constraints: { + getSlugMatch?: (slug: string) => { isMatch: boolean; organizationSlug: string }; + } = {} +) { + return z.discriminatedUnion("action", [ + z.object({ + action: z.literal("rename"), + organizationName: z + .string() + .min(3, "Organization name must have at least 3 characters") + .max(50), + }), + z.object({ + action: z.literal("delete"), + organizationSlug: z.string().superRefine((slug, ctx) => { + if (constraints.getSlugMatch === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: conform.VALIDATION_UNDEFINED, + }); + } else { + const { isMatch, organizationSlug } = constraints.getSlugMatch(slug); + if (isMatch) { + return; + } + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The slug must match ${organizationSlug}`, + }); + } + }), + }), + ]); +} + +export const action: ActionFunction = async ({ request, params }) => { + const user = await requireUser(request); + const { organizationSlug } = params; + if (!organizationSlug) { + return json({ errors: { body: "organizationSlug is required" } }, { status: 400 }); + } + + const formData = await request.formData(); + const schema = createSchema({ + getSlugMatch: (slug) => { + return { isMatch: slug === organizationSlug, organizationSlug }; + }, + }); + const submission = parse(formData, { schema }); + + if (!submission.value || submission.intent !== "submit") { + return json(submission); + } + + try { + switch (submission.value.action) { + case "rename": { + await prisma.organization.update({ + where: { + slug: organizationSlug, + members: { + some: { + userId: user.id, + }, + }, + }, + data: { + title: submission.value.organizationName, + }, + }); + + return redirectWithSuccessMessage( + organizationPath({ slug: organizationSlug }), + request, + `Organization renamed to ${submission.value.organizationName}` + ); + } + case "delete": { + const deleteOrganizationService = new DeleteOrganizationService(); + try { + await deleteOrganizationService.call({ organizationSlug, userId: user.id, request }); + + //we need to clear the project from the session + await clearCurrentProject({ + user, + }); + return redirect(rootPath()); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : JSON.stringify(error); + logger.error("Organization could not be deleted", { + error: errorMessage, + }); + return redirectWithErrorMessage( + organizationSettingsPath({ slug: organizationSlug }), + request, + errorMessage + ); + } + } + } + } catch (error: any) { + return json({ errors: { body: error.message } }, { status: 400 }); + } +}; + +export default function Page() { + const organization = useOrganization(); + const lastSubmission = useActionData(); + const navigation = useNavigation(); + + const [renameForm, { organizationName }] = useForm({ + id: "rename-organization", + // TODO: type this + lastSubmission: lastSubmission as any, + shouldRevalidate: "onSubmit", + onValidate({ formData }) { + return parse(formData, { + schema: createSchema(), + }); + }, + }); + + const [deleteForm, { organizationSlug }] = useForm({ + id: "delete-organization", + // TODO: type this + lastSubmission: lastSubmission as any, + shouldValidate: "onInput", + shouldRevalidate: "onSubmit", + onValidate({ formData }) { + return parse(formData, { + schema: createSchema({ + getSlugMatch: (slug) => ({ + isMatch: slug === organization.slug, + organizationSlug: organization.slug, + }), + }), + }); + }, + }); + + const isRenameLoading = + navigation.formData?.get("action") === "rename" && + (navigation.state === "submitting" || navigation.state === "loading"); + + const isDeleteLoading = + navigation.formData?.get("action") === "delete" && + (navigation.state === "submitting" || navigation.state === "loading"); + + return ( + + + + + + +
+
+
+ +
+ + + + {organizationName.error} + + + Rename organization + + } + /> +
+ +
+ +
+ Danger zone +
+ +
+ + + + {organizationSlug.error} + {deleteForm.error} + + This change is irreversible, so please be certain. Type in the Organization slug{" "} + {organization.slug} and then + press Delete. + + + + Delete organization + + } + /> +
+ +
+
+
+
+ ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx similarity index 96% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx index 29b599fe5e..b3b661f4b6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx @@ -1,6 +1,6 @@ import { CalendarDaysIcon, StarIcon } from "@heroicons/react/20/solid"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { PlanDefinition } from "@trigger.dev/platform/v3"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { type PlanDefinition } from "@trigger.dev/platform/v3"; import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { LinkButton } from "~/components/primitives/Buttons"; @@ -16,7 +16,7 @@ import { v3StripePortalPath, } from "~/utils/pathBuilder"; import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan"; -import { MetaFunction } from "@remix-run/react"; +import { type MetaFunction } from "@remix-run/react"; export const meta: MetaFunction = () => { return [ diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx similarity index 100% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.team/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.usage/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx similarity index 98% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.usage/route.tsx rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx index 28df91d1e1..9e92c27f2b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.usage/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx @@ -1,6 +1,6 @@ import { InformationCircleIcon } from "@heroicons/react/20/solid"; -import { Await, MetaFunction } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { Await, type MetaFunction } from "@remix-run/react"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDurationMilliseconds } from "@trigger.dev/core/v3"; import { Suspense } from "react"; import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; @@ -9,7 +9,7 @@ import { URL } from "url"; import { UsageBar } from "~/components/billing/UsageBar"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { - ChartConfig, + type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, @@ -31,7 +31,7 @@ import { import { prisma } from "~/db.server"; import { featuresForRequest } from "~/features.server"; import { useSearchParams } from "~/hooks/useSearchParam"; -import { UsagePresenter, UsageSeriesData } from "~/presenters/v3/UsagePresenter.server"; +import { UsagePresenter, type UsageSeriesData } from "~/presenters/v3/UsagePresenter.server"; import { requireUserId } from "~/services/session.server"; import { formatCurrency, formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter"; import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx index 89b4782b6d..32f77ef904 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx @@ -1,272 +1,19 @@ -import { conform, useForm } from "@conform-to/react"; -import { parse } from "@conform-to/zod"; -import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, useActionData, useNavigation } from "@remix-run/react"; -import { type ActionFunction, json } from "@remix-run/server-runtime"; -import { redirect } from "remix-typedjson"; -import { z } from "zod"; -import { InlineCode } from "~/components/code/InlineCode"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; -import { Button } from "~/components/primitives/Buttons"; -import { Fieldset } from "~/components/primitives/Fieldset"; -import { FormButtons } from "~/components/primitives/FormButtons"; -import { FormError } from "~/components/primitives/FormError"; -import { Header2 } from "~/components/primitives/Headers"; -import { Hint } from "~/components/primitives/Hint"; -import { Input } from "~/components/primitives/Input"; -import { InputGroup } from "~/components/primitives/InputGroup"; -import { Label } from "~/components/primitives/Label"; -import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; -import { SpinnerWhite } from "~/components/primitives/Spinner"; -import { prisma } from "~/db.server"; +import { Outlet } from "@remix-run/react"; +import { AppContainer, MainBody } from "~/components/layout/AppLayout"; +import { OrganizationSettingsSideMenu } from "~/components/navigation/OrganizationSettingsSideMenu"; import { useOrganization } from "~/hooks/useOrganizations"; -import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; -import { clearCurrentProject } from "~/services/dashboardPreferences.server"; -import { DeleteOrganizationService } from "~/services/deleteOrganization.server"; -import { logger } from "~/services/logger.server"; -import { requireUser, requireUserId } from "~/services/session.server"; -import { organizationPath, organizationSettingsPath, rootPath } from "~/utils/pathBuilder"; - -export const meta: MetaFunction = () => { - return [ - { - title: `Organization settings | Trigger.dev`, - }, - ]; -}; - -export function createSchema( - constraints: { - getSlugMatch?: (slug: string) => { isMatch: boolean; organizationSlug: string }; - } = {} -) { - return z.discriminatedUnion("action", [ - z.object({ - action: z.literal("rename"), - organizationName: z - .string() - .min(3, "Organization name must have at least 3 characters") - .max(50), - }), - z.object({ - action: z.literal("delete"), - organizationSlug: z.string().superRefine((slug, ctx) => { - if (constraints.getSlugMatch === undefined) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: conform.VALIDATION_UNDEFINED, - }); - } else { - const { isMatch, organizationSlug } = constraints.getSlugMatch(slug); - if (isMatch) { - return; - } - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The slug must match ${organizationSlug}`, - }); - } - }), - }), - ]); -} - -export const action: ActionFunction = async ({ request, params }) => { - const user = await requireUser(request); - const { organizationSlug } = params; - if (!organizationSlug) { - return json({ errors: { body: "organizationSlug is required" } }, { status: 400 }); - } - - const formData = await request.formData(); - const schema = createSchema({ - getSlugMatch: (slug) => { - return { isMatch: slug === organizationSlug, organizationSlug }; - }, - }); - const submission = parse(formData, { schema }); - - if (!submission.value || submission.intent !== "submit") { - return json(submission); - } - - try { - switch (submission.value.action) { - case "rename": { - await prisma.organization.update({ - where: { - slug: organizationSlug, - members: { - some: { - userId: user.id, - }, - }, - }, - data: { - title: submission.value.organizationName, - }, - }); - - return redirectWithSuccessMessage( - organizationPath({ slug: organizationSlug }), - request, - `Organization renamed to ${submission.value.organizationName}` - ); - } - case "delete": { - const deleteOrganizationService = new DeleteOrganizationService(); - try { - await deleteOrganizationService.call({ organizationSlug, userId: user.id, request }); - - //we need to clear the project from the session - await clearCurrentProject({ - user, - }); - return redirect(rootPath()); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : JSON.stringify(error); - logger.error("Organization could not be deleted", { - error: errorMessage, - }); - return redirectWithErrorMessage( - organizationSettingsPath({ slug: organizationSlug }), - request, - errorMessage - ); - } - } - } - } catch (error: any) { - return json({ errors: { body: error.message } }, { status: 400 }); - } -}; export default function Page() { const organization = useOrganization(); - const lastSubmission = useActionData(); - const navigation = useNavigation(); - - const [renameForm, { organizationName }] = useForm({ - id: "rename-organization", - // TODO: type this - lastSubmission: lastSubmission as any, - shouldRevalidate: "onSubmit", - onValidate({ formData }) { - return parse(formData, { - schema: createSchema(), - }); - }, - }); - - const [deleteForm, { organizationSlug }] = useForm({ - id: "delete-organization", - // TODO: type this - lastSubmission: lastSubmission as any, - shouldValidate: "onInput", - shouldRevalidate: "onSubmit", - onValidate({ formData }) { - return parse(formData, { - schema: createSchema({ - getSlugMatch: (slug) => ({ - isMatch: slug === organization.slug, - organizationSlug: organization.slug, - }), - }), - }); - }, - }); - - const isRenameLoading = - navigation.formData?.get("action") === "rename" && - (navigation.state === "submitting" || navigation.state === "loading"); - - const isDeleteLoading = - navigation.formData?.get("action") === "delete" && - (navigation.state === "submitting" || navigation.state === "loading"); return ( - - - - - - -
-
-
- -
- - - - {organizationName.error} - - - Rename organization - - } - /> -
- -
- -
- Danger zone -
- -
- - - - {organizationSlug.error} - {deleteForm.error} - - This change is irreversible, so please be certain. Type in the Organization slug{" "} - {organization.slug} and then - press Delete. - - - - Delete organization - - } - /> -
- -
-
-
-
+ +
+ + + + +
+
); } diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.billing.ts b/apps/webapp/app/routes/orgs.$organizationSlug.billing.ts new file mode 100644 index 0000000000..6aeec6dd17 --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.billing.ts @@ -0,0 +1,7 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { OrganizationParamsSchema, v3BillingPath, v3UsagePath } from "~/utils/pathBuilder"; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const { organizationSlug } = OrganizationParamsSchema.parse(params); + return redirect(v3BillingPath({ slug: organizationSlug })); +}; diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.team.ts b/apps/webapp/app/routes/orgs.$organizationSlug.team.ts new file mode 100644 index 0000000000..38833438dc --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.team.ts @@ -0,0 +1,7 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { OrganizationParamsSchema, organizationTeamPath, v3UsagePath } from "~/utils/pathBuilder"; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const { organizationSlug } = OrganizationParamsSchema.parse(params); + return redirect(organizationTeamPath({ slug: organizationSlug })); +}; diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.usage.ts b/apps/webapp/app/routes/orgs.$organizationSlug.usage.ts new file mode 100644 index 0000000000..b054ef6a1f --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.usage.ts @@ -0,0 +1,7 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { OrganizationParamsSchema, v3UsagePath } from "~/utils/pathBuilder"; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const { organizationSlug } = OrganizationParamsSchema.parse(params); + return redirect(v3UsagePath({ slug: organizationSlug })); +}; diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index db4e9c9183..0a6bef0d0b 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -98,7 +98,7 @@ export function selectPlanPath(organization: OrgForPath) { } export function organizationTeamPath(organization: OrgForPath) { - return `${organizationPath(organization)}/team`; + return `${organizationPath(organization)}/settings/team`; } export function inviteTeamMemberPath(organization: OrgForPath) { @@ -365,7 +365,7 @@ export function v3DeploymentPath( } export function v3BillingPath(organization: OrgForPath) { - return `${organizationPath(organization)}/billing`; + return `${organizationPath(organization)}/settings/billing`; } export function v3StripePortalPath(organization: OrgForPath) { @@ -373,7 +373,7 @@ export function v3StripePortalPath(organization: OrgForPath) { } export function v3UsagePath(organization: OrgForPath) { - return `${organizationPath(organization)}/usage`; + return `${organizationPath(organization)}/settings/usage`; } // Docs From 0f1efaf829081ae033104ac81db07ef303fa71e7 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 13 Mar 2025 21:05:09 +0000 Subject: [PATCH 56/95] Add current plan to billing side menu link --- .../components/navigation/OrganizationSettingsSideMenu.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx index 7917e03e04..90504c9dfa 100644 --- a/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx +++ b/apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx @@ -19,6 +19,7 @@ import { LinkButton } from "../primitives/Buttons"; import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; +import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route"; export function OrganizationSettingsSideMenu({ organization, @@ -26,6 +27,7 @@ export function OrganizationSettingsSideMenu({ organization: MatchedOrganization; }) { const { isManagedCloud } = useFeatures(); + const currentPlan = useCurrentPlan(); return (
)} Date: Fri, 14 Mar 2025 11:09:22 +0000 Subject: [PATCH 57/95] Upgrade to get staging from env dropdown --- .../navigation/EnvironmentSelector.tsx | 48 +++++++++++++++---- .../app/components/navigation/SideMenu.tsx | 6 ++- .../app/components/primitives/Popover.tsx | 2 +- .../route.tsx | 22 ++++++++- apps/webapp/app/utils/pathBuilder.ts | 6 ++- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index 0020744bef..c9ebd295e4 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -8,19 +8,26 @@ import { PopoverArrowTrigger, PopoverContent, PopoverMenuItem, + PopoverSectionHeader, } from "../primitives/Popover"; import { type SideMenuEnvironment, type SideMenuProject } from "./SideMenu"; import { cn } from "~/utils/cn"; +import { useFeatures } from "~/hooks/useFeatures"; +import { v3BillingPath } from "~/utils/pathBuilder"; +import { TextLink } from "../primitives/TextLink"; export function EnvironmentSelector({ + organization, project, environment, className, }: { + organization: MatchedOrganization; project: SideMenuProject; environment: SideMenuEnvironment; className?: string; }) { + const { isManagedCloud } = useFeatures(); const [isMenuOpen, setIsMenuOpen] = useState(false); const navigation = useNavigation(); const { urlForEnvironment } = useEnvironmentSwitcher(); @@ -29,6 +36,8 @@ export function EnvironmentSelector({ setIsMenuOpen(false); }, [navigation.location?.pathname]); + const hasStaging = project.environments.some((env) => env.type === "STAGING"); + return ( setIsMenuOpen(open)} open={isMenuOpen}> - {project.environments.map((env) => ( - } - isSelected={env.id === environment.id} - /> - ))} +
+ {project.environments.map((env) => ( + } + isSelected={env.id === environment.id} + /> + ))} +
+ {!hasStaging && isManagedCloud && ( + <> + +
+ + + Upgrade +
+ } + isSelected={false} + /> +
+ + )} ); diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index da97b5a73c..bdbc6acc7d 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -142,7 +142,11 @@ export function SideMenu({
- +
diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 145a58d90b..7e202f6c14 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -40,7 +40,7 @@ PopoverContent.displayName = PopoverPrimitive.Content.displayName; function PopoverSectionHeader({ title, - variant = "extra-extra-small/dimmed/caps", + variant = "extra-small", }: { title: string; variant?: ParagraphVariant; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx index b3b661f4b6..f42c77ad50 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx @@ -17,6 +17,7 @@ import { } from "~/utils/pathBuilder"; import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan"; import { type MetaFunction } from "@remix-run/react"; +import { Callout } from "~/components/primitives/Callout"; export const meta: MetaFunction = () => { return [ @@ -64,6 +65,10 @@ export async function loader({ params, request }: LoaderFunctionArgs) { (periodEnd.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24) ); + // Extract 'message' from search params + const url = new URL(request.url); + const message = url.searchParams.get("message"); + return typedjson({ ...plans, ...currentPlan, @@ -71,12 +76,20 @@ export async function loader({ params, request }: LoaderFunctionArgs) { periodStart, periodEnd, daysRemaining, + message, }); } export default function ChoosePlanPage() { - const { plans, v3Subscription, organizationSlug, periodStart, periodEnd, daysRemaining } = - useTypedLoaderData(); + const { + plans, + v3Subscription, + organizationSlug, + periodStart, + periodEnd, + daysRemaining, + message, + } = useTypedLoaderData(); return ( @@ -102,6 +115,11 @@ export default function ChoosePlanPage() {
+ {message && ( + + {message} + + )}
diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 0a6bef0d0b..332ed0aa0f 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -364,8 +364,10 @@ export function v3DeploymentPath( return `${v3DeploymentsPath(organization, project, environment)}/${deployment.shortCode}${query}`; } -export function v3BillingPath(organization: OrgForPath) { - return `${organizationPath(organization)}/settings/billing`; +export function v3BillingPath(organization: OrgForPath, message?: string) { + return `${organizationPath(organization)}/settings/billing${ + message ? `?message=${encodeURIComponent(message)}` : "" + }`; } export function v3StripePortalPath(organization: OrgForPath) { From b3609501477a167373c7d107f9372517fe4466e8 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 13:27:36 +0000 Subject: [PATCH 58/95] New env badge on concurrency limits page --- .../route.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx index 47826c609f..d8c7188578 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx @@ -9,7 +9,7 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense } from "react"; import { typeddefer, useTypedLoaderData } from "remix-typedjson"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Feedback } from "~/components/Feedback"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; @@ -155,7 +155,7 @@ function EnvironmentsTable({ environments }: { environments: Environment[] }) { {environments.map((environment) => ( - + {environment.queued} {environment.concurrency} From 43fd8d0b8e2dea4850cef574a0df2645218d3e0d Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 13:27:49 +0000 Subject: [PATCH 59/95] Show Run Engine version in span presenter --- .../route.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index ad81e26ce4..4d0707c146 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -679,12 +679,12 @@ function RunBody({ Internal ID {run.id} + + Run Engine + {run.engine} + {isAdmin && ( <> - - Engine version - {run.engine} - Primary master queue {run.masterQueue} From 13cae62a99d49fb01bfa58a59c0e7a2900d6bb64 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 14:57:29 +0000 Subject: [PATCH 60/95] New promote icon --- apps/webapp/app/assets/icons/PromoteIcon.tsx | 24 +++++++++++++++++++ .../route.tsx | 10 +++----- 2 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 apps/webapp/app/assets/icons/PromoteIcon.tsx diff --git a/apps/webapp/app/assets/icons/PromoteIcon.tsx b/apps/webapp/app/assets/icons/PromoteIcon.tsx new file mode 100644 index 0000000000..be70388877 --- /dev/null +++ b/apps/webapp/app/assets/icons/PromoteIcon.tsx @@ -0,0 +1,24 @@ +export function PromoteIcon({ className }: { className?: string }) { + return ( + + + + + + + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index 5e98d58187..cb2ee45e09 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -1,13 +1,9 @@ -import { - ArrowPathIcon, - ArrowUturnLeftIcon, - ArrowUturnRightIcon, - BookOpenIcon, -} from "@heroicons/react/20/solid"; +import { ArrowPathIcon, ArrowUturnLeftIcon, BookOpenIcon } from "@heroicons/react/20/solid"; import { type MetaFunction, Outlet, useLocation, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; +import { PromoteIcon } from "~/assets/icons/PromoteIcon"; import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels"; import { UserAvatar } from "~/components/UserProfilePhoto"; import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; @@ -335,7 +331,7 @@ function DeploymentActionsCell({ + + ))} +
+ + + ); +} From c9cfb3df0cf774324c70f33aa6d0c50327351e32 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 18:10:13 +0000 Subject: [PATCH 65/95] Avatar setting is working --- apps/webapp/app/assets/images/color-wheel.png | Bin 0 -> 5216 bytes .../app/components/billing/FreePlanUsage.tsx | 2 +- .../app/components/navigation/SideMenu.tsx | 2 +- .../app/components/primitives/Avatar.tsx | 68 ++++++-- .../OrganizationsPresenter.server.ts | 4 +- .../route.tsx | 160 +++++++++++++++--- apps/webapp/tailwind.config.js | 1 + 7 files changed, 195 insertions(+), 42 deletions(-) create mode 100644 apps/webapp/app/assets/images/color-wheel.png diff --git a/apps/webapp/app/assets/images/color-wheel.png b/apps/webapp/app/assets/images/color-wheel.png new file mode 100644 index 0000000000000000000000000000000000000000..af76136e82d6a4817dbab132e3942f8d2d2fc2e4 GIT binary patch literal 5216 zcmV-m6rbyfP) zCD&Cx=hk~Y<6pQfS*#+ty|Lt##}Ob|Af+dW1yUq>fMaN9u>Prs^rIOqG$y{~3G6VETNQd0G; zS5>dx{myrO-)fPcz)j^N-rm|dxw&z8xFst&wwT7Pg-lPBYMTq2HdU+WAP2T62Rhi> zWw3o2hW)26UD}h6aQjF=|MRC_++K|1CkC0eOPzeII@C!9tG-XNh|xa>Xjy0g7s*}y zvlSC9wx>(o8#cPxNC%k$|q<8uo-YL9G?rX;oK15Aq*jSIs5dVUb-N6vE3Eu z^Pkyu$Fw?M+-8SRU9lT%Hh{ZY2WXob;1Ga@S_f#qrW&^E;0(4bo$(TOO|@Tx4ncrz z|LoUYAaQ-)?dSN@e{^T87rDdjuYdjQ^Q&6F>pX3{f(G2+%eiz~d|q)b6jyK#TuSbW z)2_k(5eF&p7>(B?2Ezr#>@-Ke$=tyOaOD?E{Df?|;J*8BU*0(>cd*?N=)d^PADnT1 zE|`?ZYRs-U*FMq@W5hWJZ?&uu=H|yS`4B{)Y!`5ph$WBx*3y9oNCnn#eAs0+>SHeN z^WXo<&KbGQ?Y2R`_8VV4?*MoGm!-BH`lgaBTU_JkA%am{SJMQpWZeIBlmy=>PBFyC z5LmJuXo0x1o~m77=hB4Z-`@SfAMU(%J6LWD^sAqH^}GW(>1^7Rj3#ic;a1njDU4u; z>h7aL?J;`=(~_)nBUtTfE}h|cbf`604Vg8-T|m?b_Xn1&pZfl*JLf+P&xeBkis=FkASrq7}% z6oRnMu#m+$yD)0Y7%mV7LzyzE2>}hfw-(#|_domeE4RXR%aFBu@-MeUrYps0xem{> z!%6wsLr}~o;%;zS*I8DJ!x54qBU{+0OTV`C^BC)jIdyf_4>hj-TcZ~kp~j)kD_h3-X_SrW(3UO^AN_bG8?NR< z@n>&HsCJGTqxp@Y>gJ!dtND_kalR_q*PPKYHDF3d7yH1_wNpQ|RHiNaI-6`2ZTd{#UyR+WQQ8Jz-X@)Ru z9hI?YB$5>qu zT?%U9QYP{05h2Ii!66h?VP(XWGV?Zr1FVel!LoVC4#A>Gb*X6pr9p|%tv(zYi{yNY;nQaqBQbTeaQwy#vkDVZZEwIkW&uR(9Ey1`8tKwH% zu1!2lOJK5*v4!2&$lnJ(G0qp-siyrRE6ebOanZoH?g@Y*Sz!#XQPV(pQZnW9aRqQ|bhwX+G(zxm2PZ9AY%ZPl+CfF<~>1>zeiE$Z9+8rdo zqk$2BH^*^t44_xKd`7cThpZlczrxE+h^%3c3EUs)&7cJCjw%wCA@c7M{ysDT;Ac*Y z6NnVs6P5tJ3Z|KfA)kgF2goBX(W5@MBk|dgoFGwY?IXrywK-t;2{Hsz(R7f~z{N*>R3A}Hx`hY+DVLyJ*CmW!TW?DL=Ln}~i78>lJzC&Ut9v{`!*kyY|^p6b0C_UYKC5>3#hJhr}` zeVp*k-T%&>ql^mDwPq%a6htI8+540sogbS2URfP1AF==2cvv4^UA2#W?0>a9N$Jbx&ivgEI5qjpMG zd#jak0+NP4B*W6$uqCYor47L;OzFRG+kPim5Ao{%m$U;{GnQi1$nq{G2B; zHjK{ZHIoP4)At_#Z~4~YgZlBsHTmhqBoBLDebgO-A7c4{874fYX62b1LNo;ca7DVq zY8I)?CuOegT|uqo-cu}(Y)KlaWr^beVGM*V60O z9t)V8&}P|JZq5w0OYn49Aw<8&>GNX;>>yV$CSWZ0!862s{Hz^1;P)AxBZsHSl=wA8 z=7&<+BD4wtYc6FO^N?nYnT5_$?!NpLWguww$L|5OOKopb)6OPEk|x2d3lY0e6PT9* zXo@myH;$>+>Q$Bju-kGl4M#Qx>*3S{_hu-Fyxv7(!T9^E+^A=@;Q7(D3AU%$Yg?+0 z!mB_aeeqHx^F4-jZCpt%eA45FKe&wK`d@Z`c|SZjRWn+t6M(ULNgKtrzLQ1Gu>^aQ zCOGMU!-=_yYl4ku1YjgR1;H*lr0U*VRz4nvy1N3!crA9}@nHzX?>+Qstay*&vr%BH z7%j&thz3(EHX0Wq#%PE5HJ`!$bI-o9-+N@<5O|ZGctR!R-d72#scoh2E|p9HSmc`x z=t%G(kf9$-up4YckY#Fqfax3y0KtS}cQ9Z*L9Lwa02-eI>Oc&{3$f5dFhF&uI{kB9!C>0wU8Njre2oCB{U?Ltf4#6 z1_>X5vBm3jYTlCH?J(z<$Bo4W2X)XqGC`3|Lq~$geD`B+>l1xIXr%GbZz16}>(&K^W`bw-iFEb*sCiE4ndxrJ7fG+wO7cjHEfQ zFoSed>77^oUZ^r(`k^o_Y}#8JyK|g3)|A+O@zsC0;%sc$Os%QfY!(o#b%8JpRqYN< z8*s*E3vOE2f@i0RTeLxppqCnv* zrJ1;JNei#6y#3kdzVYOBR(vzi-rajaTEURzA#J&`+)3}=;&|F^rbQXsrI|z%koC&c zvPsGhjqP>#O6R7uaYX`Clj#ae*9>z;=cQupzEJPVB!$-oIdLPtn}YuK>EGJ(iJxPJ z-x<}louVIx47tVcye|?xfYsCvX<1vgl&mYZ$h3 zJK<`(FE=a@uuKN6b%SaaNZr;Yy}UKm&wFBVfdV?Xb~v1P>A4I0cLh4OZ@v75 z%acyWeJbxtw^L*FE>)%5Jeek;?zsLU?Cd4WB%H{KAj@Pk`HcO>*z$moNJRk2`z&d~K18lYLt=3qVtAJZSgJiO^u3 zvj=C(hZ$Bs*HhEA3nXCHXKUNIcfNSRIqmS8-Dcex9#)0TTa!kv-b&~ z`WE_}EmK3>p4Jr^!yK7jGigI(oGo5xKzrM{G3(>GjZ(jM?)ZfRxrgn(fNtCN#of*F z{mFqj8z?Jvj$kx?1NORqYKaT-$TuTzce()9dF>+#A@o-B-}_ zEd(HkSGRq_+qI0(xP2ds6+_r`u5%S_6%*FF!=~SLGrzpla(U44J3qSm$lj$B=k5#8 a`S$;8gm&W5$O$3<0000
- Free Plan + Free Plan
Upgrade diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index bdbc6acc7d..416911ead1 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -118,7 +118,7 @@ export function SideMenu({ return (
; export type IconAvatar = Extract; export type ImageAvatar = Extract; +export type LettersAvatar = Extract; export function parseAvatar(json: Prisma.JsonValue, defaultAvatar: Avatar): Avatar { if (!json || typeof json !== "object") { @@ -60,6 +63,10 @@ export function Avatar({ switch (avatar.type) { case "icon": return ; + case "letters": + return ( + + ); case "image": return ; } @@ -76,22 +83,57 @@ export const avatarIcons: Record + + {letters} + + + ); +} + function AvatarIcon({ avatar, className, @@ -106,7 +148,7 @@ function AvatarIcon({ color: avatar.hex, }; - const IconComponent = avatarIcons[avatar.name] || defaultAvatarIcon.name; + const IconComponent = avatarIcons[avatar.name]; return ( diff --git a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts index b6cea9afe9..1ac6685944 100644 --- a/apps/webapp/app/presenters/OrganizationsPresenter.server.ts +++ b/apps/webapp/app/presenters/OrganizationsPresenter.server.ts @@ -9,7 +9,7 @@ import { type MinimumEnvironment, } from "./SelectBestEnvironmentPresenter.server"; import { sortEnvironments } from "~/utils/environmentSort"; -import { defaultAvatarIcon, parseAvatar } from "~/components/primitives/Avatar"; +import { defaultAvatar, parseAvatar } from "~/components/primitives/Avatar"; export class OrganizationsPresenter { #prismaClient: PrismaClient; @@ -150,7 +150,7 @@ export class OrganizationsPresenter { id: org.id, slug: org.slug, title: org.title, - avatar: parseAvatar(org.avatar, defaultAvatarIcon), + avatar: parseAvatar(org.avatar, defaultAvatar), projects: org.projects.map((project) => ({ id: project.id, slug: project.slug, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index 586ec8099b..481234b862 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -1,6 +1,12 @@ +import colorWheelIcon from "../../assets/images/color-wheel.png"; import { conform, useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; -import { ExclamationTriangleIcon, FolderIcon, TrashIcon } from "@heroicons/react/20/solid"; +import { + CheckIcon, + ExclamationTriangleIcon, + FolderIcon, + TrashIcon, +} from "@heroicons/react/20/solid"; import { Form, type MetaFunction, useActionData, useNavigation } from "@remix-run/react"; import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -12,9 +18,10 @@ import { AvatarData, avatarIcons, AvatarType, - defaultAvatarColors, - defaultAvatarIcon, + defaultAvatar, parseAvatar, + defaultAvatarHex, + defaultAvatarColors, } from "~/components/primitives/Avatar"; import { Button } from "~/components/primitives/Buttons"; import { Fieldset } from "~/components/primitives/Fieldset"; @@ -26,7 +33,8 @@ import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; -import { SpinnerWhite } from "~/components/primitives/Spinner"; +import { Popover, PopoverContent, PopoverCustomTrigger } from "~/components/primitives/Popover"; +import { Spinner, SpinnerWhite } from "~/components/primitives/Spinner"; import { prisma } from "~/db.server"; import { useOrganization } from "~/hooks/useOrganizations"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; @@ -70,7 +78,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { } return typedjson({ - organization: { ...organization, avatar: parseAvatar(organization.avatar, defaultAvatarIcon) }, + organization: { ...organization, avatar: parseAvatar(organization.avatar, defaultAvatar) }, }); }; @@ -352,33 +360,55 @@ export default function Page() { } function LogoForm({ organization }: { organization: { avatar: Avatar } }) { - const lastSubmission = useActionData(); const navigation = useNavigation(); - const [avatarForm] = useForm({ - id: "avatar-organization", - // TODO: type this - lastSubmission: lastSubmission as any, - shouldRevalidate: "onSubmit", - onValidate({ formData }) { - return parse(formData, { - schema: createSchema(), - }); - }, - }); + const isSubmitting = + navigation.state != "idle" && navigation.formData?.get("action") === "avatar"; + + const avatar = navigation.formData + ? avatarFromFormData(navigation.formData) ?? organization.avatar + : organization.avatar; - const hex = "hex" in organization.avatar ? organization.avatar.hex : defaultAvatarColors[0]; + const hex = "hex" in avatar ? avatar.hex : defaultAvatarHex; return (
-
- +
+
+ {/* Letters */} +
+ + + + + + {/* Icons */} {Object.entries(avatarIcons).map(([name]) => ( -
+ @@ -386,11 +416,14 @@ function LogoForm({ organization }: { organization: { avatar: Avatar } }) {
); } + +function HexPopover({ avatar, hex }: { avatar: Avatar; hex: string }) { + return ( + + + + + +
+ + + {"name" in avatar && } + {defaultAvatarColors.map((color) => ( + + ))} + +
+
+ ); +} + +function avatarFromFormData(formData: FormData): Avatar | undefined { + const action = formData.get("action"); + if (!action || action !== "avatar") { + return undefined; + } + + const type = formData.get("type"); + const hex = formData.get("hex"); + + if (type === "letters") { + return { + type: "letters", + hex: hex as string, + }; + } + + if (type === "icon") { + return { + type: "icon", + name: formData.get("name") as string, + hex: hex as string, + }; + } + + return undefined; +} diff --git a/apps/webapp/tailwind.config.js b/apps/webapp/tailwind.config.js index af13215ecc..43200351bc 100644 --- a/apps/webapp/tailwind.config.js +++ b/apps/webapp/tailwind.config.js @@ -65,6 +65,7 @@ const charcoal = { 650: "#2C3034", 700: "#272A2E", 750: "#212327", + 775: "#1C1E21", 800: "#1A1B1F", 850: "#15171A", 900: "#121317", From c64b23b50be86956a94e7268d7d6a971af899a57 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 18:44:35 +0000 Subject: [PATCH 66/95] You can change the color of your icon --- .../app/components/navigation/SideMenu.tsx | 2 +- .../webapp/app/components/primitives/Avatar.tsx | 6 +++--- .../app/components/primitives/Popover.tsx | 2 +- .../route.tsx | 17 +++++++++++------ .../app/routes/storybook.avatar/route.tsx | 11 ++++++++--- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 416911ead1..9d17638261 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -298,7 +298,7 @@ function ProjectSelector({ >
-
+
diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index e11a237abb..694192fbfb 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -120,15 +120,15 @@ function AvatarLetters({ }; return ( - + - {letters} + {letters} ); diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index 7e202f6c14..ac7f7090e2 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -10,7 +10,7 @@ import { cn } from "~/utils/cn"; import { type ButtonContentPropsType, LinkButton } from "./Buttons"; import { Paragraph, type ParagraphVariant } from "./Paragraph"; import { ShortcutKey } from "./ShortcutKey"; -import { RenderIcon } from "./Icon"; +import { type RenderIcon } from "./Icon"; const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index 481234b862..bd5d29a752 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -33,7 +33,12 @@ import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; -import { Popover, PopoverContent, PopoverCustomTrigger } from "~/components/primitives/Popover"; +import { + Popover, + PopoverContent, + PopoverCustomTrigger, + PopoverTrigger, +} from "~/components/primitives/Popover"; import { Spinner, SpinnerWhite } from "~/components/primitives/Spinner"; import { prisma } from "~/db.server"; import { useOrganization } from "~/hooks/useOrganizations"; @@ -449,15 +454,15 @@ function LogoForm({ organization }: { organization: { avatar: Avatar } }) { function HexPopover({ avatar, hex }: { avatar: Avatar; hex: string }) { return ( - - - + + + -
+ {"name" in avatar && } diff --git a/apps/webapp/app/routes/storybook.avatar/route.tsx b/apps/webapp/app/routes/storybook.avatar/route.tsx index a5331ac4ed..0f5fed5de8 100644 --- a/apps/webapp/app/routes/storybook.avatar/route.tsx +++ b/apps/webapp/app/routes/storybook.avatar/route.tsx @@ -1,10 +1,15 @@ -import { Avatar, avatarIcons, defaultAvatarColors } from "~/components/primitives/Avatar"; +import { + Avatar, + avatarIcons, + defaultAvatarColors, + type IconAvatar, +} from "~/components/primitives/Avatar"; // Map tablerIcons Set to Avatar array with cycling colors -const avatars: Avatar[] = Object.entries(avatarIcons).map(([iconName], index) => ({ +const avatars: IconAvatar[] = Object.entries(avatarIcons).map(([iconName], index) => ({ type: "icon", name: iconName, - hex: defaultAvatarColors[index % defaultAvatarColors.length], // Cycle through colors + hex: defaultAvatarColors[index % defaultAvatarColors.length].hex, // Cycle through colors })); export default function Story() { From 6a32167a97ce453addefa20e21af789346c3b52e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 19:03:07 +0000 Subject: [PATCH 67/95] Avatar improvements --- .../app/components/layout/AppLayout.tsx | 21 +++ .../app/components/primitives/Avatar.tsx | 38 ++++- .../route.tsx | 158 +++++++++--------- 3 files changed, 139 insertions(+), 78 deletions(-) diff --git a/apps/webapp/app/components/layout/AppLayout.tsx b/apps/webapp/app/components/layout/AppLayout.tsx index fc1fb71541..a38607aab7 100644 --- a/apps/webapp/app/components/layout/AppLayout.tsx +++ b/apps/webapp/app/components/layout/AppLayout.tsx @@ -66,3 +66,24 @@ export function MainCenteredContainer({
); } + +export function MainHorizontallyCenteredContainer({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+
+ {children} +
+
+ ); +} diff --git a/apps/webapp/app/components/primitives/Avatar.tsx b/apps/webapp/app/components/primitives/Avatar.tsx index 694192fbfb..3c62c0900b 100644 --- a/apps/webapp/app/components/primitives/Avatar.tsx +++ b/apps/webapp/app/components/primitives/Avatar.tsx @@ -8,6 +8,7 @@ import { StarIcon, } from "@heroicons/react/24/solid"; import { type Prisma } from "@trigger.dev/database"; +import { useLayoutEffect, useRef, useState } from "react"; import { z } from "zod"; import { useOrganization } from "~/hooks/useOrganizations"; import { logger } from "~/services/logger.server"; @@ -74,7 +75,6 @@ export function Avatar({ export const avatarIcons: Record>> = { "hero:building-office-2": BuildingOffice2Icon, - "hero:cube": CubeIcon, "hero:rocket-launch": RocketLaunchIcon, "hero:code-bracket-square": CodeBracketSquareIcon, "hero:fire": FireIcon, @@ -112,6 +112,36 @@ function AvatarLetters({ includePadding?: boolean; }) { const organization = useOrganization(); + const containerRef = useRef(null); + const textRef = useRef(null); + const [fontSize, setFontSize] = useState("1rem"); + + useLayoutEffect(() => { + if (containerRef.current) { + const containerWidth = containerRef.current.offsetWidth; + // Set font size to 60% of container width (adjust as needed) + setFontSize(`${containerWidth * 0.6}px`); + } + + // Optional: Create a ResizeObserver for dynamic resizing + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.target === containerRef.current) { + const containerWidth = entry.contentRect.width; + setFontSize(`${containerWidth * 0.6}px`); + } + } + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, []); + const letters = organization.title.slice(0, 2); const classes = cn("grid place-items-center", className); @@ -121,14 +151,18 @@ function AvatarLetters({ return ( + {/* This is the square container */} - {letters} + + {letters} + ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index bd5d29a752..02a4eb5fc9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -12,7 +12,11 @@ import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/s import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { + MainHorizontallyCenteredContainer, + PageBody, + PageContainer, +} from "~/components/layout/AppLayout"; import { Avatar, AvatarData, @@ -281,84 +285,86 @@ export default function Page() { -
-
- -
- -
- - -
- - - +
+
+ +
+ +
+ + +
+ + + + {organizationName.error} + + + Rename organization + + } /> - {organizationName.error} - - - Rename organization - - } - /> -
- -
- -
- Danger zone -
- -
- - - + +
+ +
+ Danger zone +
+ +
+ + + + {organizationSlug.error} + {deleteForm.error} + + This change is irreversible, so please be certain. Type in the Organization + slug {organization.slug} and + then press Delete. + + + + Delete organization + + } /> - {organizationSlug.error} - {deleteForm.error} - - This change is irreversible, so please be certain. Type in the Organization slug{" "} - {organization.slug} and then - press Delete. - - - - Delete organization - - } - /> -
- +
+ +
-
+ ); From b8fdb56fda1da9bd0312f3e58ac91eb496b33c1f Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 19:46:36 +0000 Subject: [PATCH 68/95] Bugfix for mising prop --- apps/webapp/app/components/BlankStatePanels.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/components/BlankStatePanels.tsx b/apps/webapp/app/components/BlankStatePanels.tsx index d272ca3e4c..b0b49fa2cb 100644 --- a/apps/webapp/app/components/BlankStatePanels.tsx +++ b/apps/webapp/app/components/BlankStatePanels.tsx @@ -401,6 +401,7 @@ function AlertsNoneProd() { } function SwitcherPanel() { + const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); @@ -410,6 +411,7 @@ function SwitcherPanel() { Switch to a deployed environment Date: Fri, 14 Mar 2025 19:46:59 +0000 Subject: [PATCH 69/95] Removed some old env badges --- .../app/components/runs/v3/ReplayRunDialog.tsx | 8 ++++---- .../app/components/runs/v3/TaskRunsTable.tsx | 18 +++++------------- .../presenters/v3/RunListPresenter.server.ts | 3 +-- .../route.tsx | 8 +------- .../route.tsx | 6 +----- .../route.tsx | 3 --- 6 files changed, 12 insertions(+), 34 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx index e29b8a9e4b..01f9189b3e 100644 --- a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -1,9 +1,9 @@ import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useNavigation, useSubmit } from "@remix-run/react"; import { useCallback, useEffect, useRef } from "react"; -import { UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson"; +import { type UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson"; import { JSONEditor } from "~/components/code/JSONEditor"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Button } from "~/components/primitives/Buttons"; import { DialogContent, DialogHeader } from "~/components/primitives/Dialog"; import { Header3 } from "~/components/primitives/Headers"; @@ -135,7 +135,7 @@ function ReplayForm({ const env = environments.find((env) => env.id === value)!; return (
- +
); }} @@ -143,7 +143,7 @@ function ReplayForm({ {(matches) => matches.map((env) => ( - + )) } diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index f948a7e3e9..ea62f28478 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -28,7 +28,6 @@ import { } from "~/presenters/v3/RunListPresenter.server"; import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter"; import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder"; -import { EnvironmentLabel, FullEnvironmentCombo } from "../../environments/EnvironmentLabel"; import { DateTime } from "../../primitives/DateTime"; import { Paragraph } from "../../primitives/Paragraph"; import { Spinner } from "../../primitives/Spinner"; @@ -137,7 +136,6 @@ export function TaskRunsTable({ )} Run # - Env Task Version {total === 0 && !hasFilters ? ( - + {!isLoading && } ) : runs.length === 0 ? ( @@ -312,12 +310,6 @@ export function TaskRunsTable({ {formatNumber(run.number)} - - - {run.taskIdentifier} @@ -402,7 +394,7 @@ export function TaskRunsTable({ )} {isLoading && ( Loading… @@ -542,7 +534,7 @@ function BlankState({ isLoading, filters }: Pick; + if (isLoading) return ; const { environments, tasks, from, to, ...otherFilters } = filters; @@ -553,7 +545,7 @@ function BlankState({ isLoading, filters }: Pick filterArray.length === 0) ) { return ( - + There are no runs for {filters.tasks[0]} @@ -581,7 +573,7 @@ function BlankState({ isLoading, filters }: Pick +
No runs match your filters. Try refreshing, modifying your filters or run a test. diff --git a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts index 590e499a77..601be11bcc 100644 --- a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts @@ -5,7 +5,7 @@ import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; import { isCancellableRunStatus, isFinalRunStatus } from "~/v3/taskStatus"; import { BasePresenter } from "./basePresenter.server"; import { getAllTaskIdentifiers } from "~/models/task.server"; -import { Direction } from "~/components/ListPagination"; +import { type Direction } from "~/components/ListPagination"; export type RunListOptions = { userId?: string; @@ -65,7 +65,6 @@ export class RunListPresenter extends BasePresenter { (tasks !== undefined && tasks.length > 0) || (versions !== undefined && versions.length > 0) || hasStatusFilters || - (environments !== undefined && environments.length > 0) || (period !== undefined && period !== "all") || (bulkId !== undefined && bulkId !== "") || from !== undefined || diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index 32d364eb19..5c796ab177 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -156,7 +156,6 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) { ID - Env @@ -210,12 +209,7 @@ function BatchesTable({ batches, hasFilters, filters }: BatchList) { {batch.friendlyId} - - - + {batch.batchVersion === "v1" ? ( Deploy - Env Version - - - {deployment.version} @@ -231,7 +227,7 @@ export default function Page() { ); }) ) : ( - + No deploys match your filters diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index d478be346c..5c4c678b16 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -159,13 +159,10 @@ type LoaderData = SerializeFrom; export default function Page() { const { run, trace, resizable, maximumLiveReloadingSetting } = useLoaderData(); - const user = useUser(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); - const usernameForEnv = user.id !== run.environment.userId ? run.environment.userName : undefined; - return ( <> From 7898afc0558e30224a327efb4b4ca7c0f6ab8e7e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 19:47:03 +0000 Subject: [PATCH 70/95] Fixed replaying --- .../app/routes/resources.taskruns.$runParam.replay.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts index 6aa61ffdaa..0fcaf0981f 100644 --- a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts +++ b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts @@ -1,5 +1,5 @@ import { parse } from "@conform-to/zod"; -import { ActionFunction, json, LoaderFunctionArgs } from "@remix-run/node"; +import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/node"; import { prettyPrintPacket } from "@trigger.dev/core/v3"; import { typedjson } from "remix-typedjson"; import { z } from "zod"; @@ -103,6 +103,11 @@ export const action: ActionFunction = async ({ request, params }) => { friendlyId: runParam, }, include: { + runtimeEnvironment: { + select: { + slug: true, + }, + }, project: { include: { organization: true, @@ -134,6 +139,7 @@ export const action: ActionFunction = async ({ request, params }) => { slug: taskRun.project.organization.slug, }, { slug: taskRun.project.slug }, + { slug: taskRun.runtimeEnvironment.slug }, { friendlyId: newRun.friendlyId }, { spanId: newRun.spanId } ); From 3e0dc91b134ea919587527781235a573e2f4bf18 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 14 Mar 2025 22:56:03 +0000 Subject: [PATCH 71/95] Removed EnvironmentLabel --- .../route.tsx | 1 - .../route.tsx | 2 +- .../route.tsx | 16 ++++++---------- .../route.tsx | 9 +++++---- .../route.tsx | 6 ++++-- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index 5c796ab177..8fd0fe8861 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -11,7 +11,6 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { BatchesNone } from "~/components/BlankStatePanels"; import { ListPagination } from "~/components/ListPagination"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx index d8c7188578..f2208cd3fe 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx @@ -9,7 +9,7 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense } from "react"; import { typeddefer, useTypedLoaderData } from "remix-typedjson"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; -import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Feedback } from "~/components/Feedback"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index 2e3e8060bf..553a98fe29 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -3,25 +3,23 @@ import { parse } from "@conform-to/zod"; import { BookOpenIcon, InformationCircleIcon, - LockOpenIcon, PencilSquareIcon, PlusIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { ArrowUpCircleIcon } from "@heroicons/react/24/outline"; -import { Form, MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react"; +import { Form, type MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react"; import { - ActionFunctionArgs, - LoaderFunctionArgs, + type ActionFunctionArgs, + type LoaderFunctionArgs, json, redirectDocument, } from "@remix-run/server-runtime"; -import { RuntimeEnvironment } from "@trigger.dev/database"; +import { type RuntimeEnvironment } from "@trigger.dev/database"; import { Fragment, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; -import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; @@ -35,7 +33,6 @@ import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; -import { Paragraph } from "~/components/primitives/Paragraph"; import { Switch } from "~/components/primitives/Switch"; import { Table, @@ -52,7 +49,7 @@ import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { - EnvironmentVariableWithSetValues, + type EnvironmentVariableWithSetValues, EnvironmentVariablesPresenter, } from "~/presenters/v3/EnvironmentVariablesPresenter.server"; import { requireUserId } from "~/services/session.server"; @@ -61,7 +58,6 @@ import { EnvironmentParamSchema, ProjectParamSchema, docsPath, - v3BillingPath, v3EnvironmentVariablesPath, v3NewEnvironmentVariablesPath, } from "~/utils/pathBuilder"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 5c4c678b16..268d2aac35 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -27,7 +27,7 @@ import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParen import tileBgPath from "~/assets/images/error-banner-tile@2x.png"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { InlineCode } from "~/components/code/InlineCode"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { PageBody } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; @@ -75,7 +75,7 @@ import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useReplaceSearchParams } from "~/hooks/useReplaceSearchParams"; import { type Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys"; -import { useHasAdminAccess, useUser } from "~/hooks/useUser"; +import { useHasAdminAccess } from "~/hooks/useUser"; import { RunPresenter } from "~/presenters/v3/RunPresenter.server"; import { getImpersonationId } from "~/services/impersonation.server"; import { getResizableSnapshot } from "~/services/resizablePanel.server"; @@ -1262,8 +1262,9 @@ function ConnectedDevWarning() {
Runs usually start within 1 second in{" "} - . Check you're running the - CLI: npx trigger.dev@latest dev + . + Check you're running the CLI:{" "} + npx trigger.dev@latest dev
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index 4d0707c146..99f1c5f367 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -16,7 +16,7 @@ import { typedjson, useTypedFetcher } from "remix-typedjson"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { AdminDebugRun } from "~/components/admin/debugRun"; import { CodeBlock } from "~/components/code/CodeBlock"; -import { EnvironmentLabel, FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Feedback } from "~/components/Feedback"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; @@ -588,7 +588,9 @@ function RunBody({
+ {run.schedule.description} } From afbaf6b84b0ba97125db11bcf6e8abe69e41c157 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 17:01:38 +0000 Subject: [PATCH 72/95] Old env badge deleted, changed everywhere to the new one --- .../app/components/admin/debugTooltip.tsx | 2 +- .../environments/EnvironmentLabel.tsx | 128 +----------------- .../navigation/EnvironmentSelector.tsx | 8 +- .../components/navigation/SideMenuItem.tsx | 5 +- .../components/runs/v3/ReplayRunDialog.tsx | 6 +- .../app/components/runs/v3/SharedFilters.tsx | 2 +- .../route.tsx | 4 +- .../route.tsx | 7 +- .../route.tsx | 6 +- .../route.tsx | 4 +- .../route.tsx | 4 +- .../route.tsx | 2 +- .../route.tsx | 11 +- .../route.tsx | 6 +- .../route.tsx | 4 +- .../route.tsx | 7 +- .../route.tsx | 7 +- .../route.tsx | 6 +- .../route.tsx | 4 +- .../route.tsx | 4 +- .../storybook.environment-label/route.tsx | 9 -- .../routes/storybook.input-fields/route.tsx | 4 +- 22 files changed, 49 insertions(+), 191 deletions(-) diff --git a/apps/webapp/app/components/admin/debugTooltip.tsx b/apps/webapp/app/components/admin/debugTooltip.tsx index f761e23fa9..2dfcce9634 100644 --- a/apps/webapp/app/components/admin/debugTooltip.tsx +++ b/apps/webapp/app/components/admin/debugTooltip.tsx @@ -58,7 +58,7 @@ function Content({ children }: { children: React.ReactNode }) { Project ref - {project.ref} + {project.externalRef} )} diff --git a/apps/webapp/app/components/environments/EnvironmentLabel.tsx b/apps/webapp/app/components/environments/EnvironmentLabel.tsx index e9154a522c..31a346e192 100644 --- a/apps/webapp/app/components/environments/EnvironmentLabel.tsx +++ b/apps/webapp/app/components/environments/EnvironmentLabel.tsx @@ -9,128 +9,6 @@ import { } from "~/assets/icons/EnvironmentIcons"; type Environment = Pick; -const variants = { - small: "h-4 text-xxs px-[0.1875rem] rounded-[2px]", - large: "h-6 text-xs px-1.5 rounded", -}; - -export function EnvironmentTypeLabel({ - environment, - size = "small", - className, -}: { - environment: Environment; - size?: keyof typeof variants; - className?: string; -}) { - return ( - - {environmentTypeTitle(environment)} - - ); -} - -export function EnvironmentLabel({ - environment, - size = "small", - userName, - className, -}: { - environment: Environment; - size?: keyof typeof variants; - userName?: string; - className?: string; -}) { - return ( - - {environmentTitle(environment, userName)} - - ); -} - -type EnvironmentWithUsername = Environment & { userName?: string }; - -export function EnvironmentLabels({ - environments, - size = "small", - className, -}: { - environments: EnvironmentWithUsername[]; - size?: keyof typeof variants; - className?: string; -}) { - const devEnvironments = sortEnvironments( - environments.filter((env) => env.type === "DEVELOPMENT") - ); - const firstDevEnvironment = devEnvironments[0]; - const otherDevEnvironments = devEnvironments.slice(1); - const otherEnvironments = environments.filter((env) => env.type !== "DEVELOPMENT"); - - return ( -
- {firstDevEnvironment && ( - - )} - {otherDevEnvironments.length > 0 ? ( - - +{otherDevEnvironments.length} - - } - content={ -
- {otherDevEnvironments.map((environment, index) => ( - - ))} -
- } - /> - ) : null} - {otherEnvironments.map((environment, index) => ( - - ))} -
- ); -} export function EnvironmentIcon({ environment, @@ -156,7 +34,7 @@ export function EnvironmentIcon({ } } -export function FullEnvironmentCombo({ +export function EnvironmentCombo({ environment, className, }: { @@ -166,12 +44,12 @@ export function FullEnvironmentCombo({ return ( - + ); } -export function FullEnvironmentLabel({ +export function EnvironmentLabel({ environment, className, }: { diff --git a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx index c9ebd295e4..5df3fe6aa3 100644 --- a/apps/webapp/app/components/navigation/EnvironmentSelector.tsx +++ b/apps/webapp/app/components/navigation/EnvironmentSelector.tsx @@ -2,7 +2,7 @@ import { useNavigation } from "@remix-run/react"; import { useEffect, useState } from "react"; import { useEnvironmentSwitcher } from "~/hooks/useEnvironmentSwitcher"; import { type MatchedOrganization } from "~/hooks/useOrganizations"; -import { FullEnvironmentCombo } from "../environments/EnvironmentLabel"; +import { EnvironmentCombo } from "../environments/EnvironmentLabel"; import { Popover, PopoverArrowTrigger, @@ -46,7 +46,7 @@ export function EnvironmentSelector({ fullWidth className={cn("h-7 overflow-hidden py-1 pl-2", className)} > - + } + title={} isSelected={env.id === environment.id} /> ))} @@ -75,7 +75,7 @@ export function EnvironmentSelector({ )} title={
- + Upgrade
} diff --git a/apps/webapp/app/components/navigation/SideMenuItem.tsx b/apps/webapp/app/components/navigation/SideMenuItem.tsx index 54c57b388c..278bfd9188 100644 --- a/apps/webapp/app/components/navigation/SideMenuItem.tsx +++ b/apps/webapp/app/components/navigation/SideMenuItem.tsx @@ -2,6 +2,7 @@ import { type AnchorHTMLAttributes } from "react"; import { usePathName } from "~/hooks/usePathName"; import { cn } from "~/utils/cn"; import { LinkButton } from "../primitives/Buttons"; +import { type RenderIcon } from "../primitives/Icon"; export function SideMenuItem({ icon, @@ -14,10 +15,10 @@ export function SideMenuItem({ badge, target, }: { - icon?: React.ComponentType; + icon?: RenderIcon; activeIconColor?: string; inactiveIconColor?: string; - trailingIcon?: React.ComponentType; + trailingIcon?: RenderIcon; trailingIconClassName?: string; name: string; to: string; diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx index 01f9189b3e..f6faab1923 100644 --- a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -3,7 +3,7 @@ import { Form, useNavigation, useSubmit } from "@remix-run/react"; import { useCallback, useEffect, useRef } from "react"; import { type UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson"; import { JSONEditor } from "~/components/code/JSONEditor"; -import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Button } from "~/components/primitives/Buttons"; import { DialogContent, DialogHeader } from "~/components/primitives/Dialog"; import { Header3 } from "~/components/primitives/Headers"; @@ -135,7 +135,7 @@ function ReplayForm({ const env = environments.find((env) => env.id === value)!; return (
- +
); }} @@ -143,7 +143,7 @@ function ReplayForm({ {(matches) => matches.map((env) => ( - + )) } diff --git a/apps/webapp/app/components/runs/v3/SharedFilters.tsx b/apps/webapp/app/components/runs/v3/SharedFilters.tsx index 03474b5960..7e00bec892 100644 --- a/apps/webapp/app/components/runs/v3/SharedFilters.tsx +++ b/apps/webapp/app/components/runs/v3/SharedFilters.tsx @@ -100,7 +100,7 @@ export function EnvironmentsDropdown({ value={item.id} shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })} > - + ))} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index 4d8b0482a7..526798cd78 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -9,7 +9,7 @@ import { useEffect, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; -import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Callout, variantClasses } from "~/components/primitives/Callout"; import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; @@ -416,7 +416,7 @@ export default function Page() { - + {environmentTypes.error} {form.error} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx index a620cc31a1..08f40cffb6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx @@ -18,10 +18,7 @@ import assertNever from "assert-never"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { AlertsNoneDev, AlertsNoneDeployed } from "~/components/BlankStatePanels"; -import { - EnvironmentTypeLabel, - FullEnvironmentCombo, -} from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; @@ -240,7 +237,7 @@ export default function Page() {
{alertChannel.environmentTypes.map((environmentType) => ( - ( - + - + ( - + {environment.queued} {environment.concurrency} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index d5a14f4234..6a064e4995 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -4,7 +4,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { UserAvatar } from "~/components/UserProfilePhoto"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; -import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Badge } from "~/components/primitives/Badge"; import { LinkButton } from "~/components/primitives/Buttons"; import { DateTimeAccurate } from "~/components/primitives/DateTime"; @@ -132,7 +132,7 @@ export default function Page() { Environment - + diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index 14f7e4c401..32945e7375 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -6,7 +6,7 @@ import { z } from "zod"; import { PromoteIcon } from "~/assets/icons/PromoteIcon"; import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels"; import { UserAvatar } from "~/components/UserProfilePhoto"; -import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index 64a1d73a34..0fc0a7b5b1 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -16,8 +16,8 @@ import { z } from "zod"; import { environmentTextClassName, environmentTitle, - FullEnvironmentCombo, - FullEnvironmentLabel, + EnvironmentCombo, + EnvironmentLabel, } from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { CheckboxWithLabel } from "~/components/primitives/Checkbox"; @@ -232,7 +232,7 @@ export default function Page() { value={environment.id} name="environmentIds" type="radio" - label={} + label={} variant="button" /> ))} @@ -245,10 +245,7 @@ export default function Page() { className="flex w-fit cursor-pointer items-center gap-2 rounded border border-dashed border-charcoal-600 py-3 pl-3 pr-4 transition hover:border-charcoal-500 hover:bg-charcoal-850" > - + diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index 553a98fe29..cd8317580c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -19,7 +19,7 @@ import { Fragment, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { InlineCode } from "~/components/code/InlineCode"; -import { FullEnvironmentCombo } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; @@ -233,7 +233,7 @@ export default function Page() { Key {environments.map((environment) => ( - + ))} Actions @@ -401,7 +401,7 @@ function EditEnvironmentVariablePanel({ className="flex items-center justify-end" htmlFor={`values[${index}].value`} > - + Runs usually start within 1 second in{" "} - . + . Check you're running the CLI:{" "} npx trigger.dev@latest dev diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx index 1a56af2b0c..5d99c2f195 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx @@ -13,10 +13,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { InlineCode } from "~/components/code/InlineCode"; -import { - EnvironmentLabels, - FullEnvironmentCombo, -} from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; import { @@ -269,7 +266,7 @@ export default function Page() {
{schedule.environments.map((env) => ( - + ))}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index 21841c2541..1c26d5d76d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -7,10 +7,7 @@ import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { Feedback } from "~/components/Feedback"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { InlineCode } from "~/components/code/InlineCode"; -import { - EnvironmentLabels, - FullEnvironmentCombo, -} from "~/components/environments/EnvironmentLabel"; +import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; @@ -458,7 +455,7 @@ function SchedulesTable({
{schedule.environments.map((env) => ( - + ))}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 69cb1bde57..99fa18a827 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -7,7 +7,7 @@ import { type TaskRunStatus } from "@trigger.dev/database"; import { useCallback, useEffect, useRef, useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { JSONEditor } from "~/components/code/JSONEditor"; -import { FullEnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { Button } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { DateField } from "~/components/primitives/DateField"; @@ -326,7 +326,7 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa This test will run in - +
diff --git a/apps/webapp/app/routes/storybook.environment-label/route.tsx b/apps/webapp/app/routes/storybook.environment-label/route.tsx index 656425291e..f0654edd5f 100644 --- a/apps/webapp/app/routes/storybook.environment-label/route.tsx +++ b/apps/webapp/app/routes/storybook.environment-label/route.tsx @@ -1,23 +1,14 @@ import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { Header2 } from "~/components/primitives/Headers"; export default function Story() { return (
- Small (default)
-
- Large - - - - -
); } diff --git a/apps/webapp/app/routes/storybook.input-fields/route.tsx b/apps/webapp/app/routes/storybook.input-fields/route.tsx index 68c2a78f5b..6e5b95fe95 100644 --- a/apps/webapp/app/routes/storybook.input-fields/route.tsx +++ b/apps/webapp/app/routes/storybook.input-fields/route.tsx @@ -55,14 +55,14 @@ function InputFieldSet({ disabled }: { disabled?: boolean }) { disabled={disabled} variant="large" placeholder="Search" - icon={} + icon={} shortcut="⌘K" /> } + icon={} shortcut="⌘K" /> Date: Sun, 16 Mar 2025 17:01:48 +0000 Subject: [PATCH 73/95] Fix for Slack integration paths --- ...ram.env.$envParam.alerts.new.connect-to-slack.ts} | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) rename apps/webapp/app/routes/{_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new.connect-to-slack.ts => _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts} (84%) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new.connect-to-slack.ts b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts similarity index 84% rename from apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new.connect-to-slack.ts rename to apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts index 71a9c88f01..99dc07f12a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.alerts.new.connect-to-slack.ts +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new.connect-to-slack.ts @@ -1,10 +1,12 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { prisma } from "~/db.server"; +import { env } from "~/env.server"; import { redirectWithSuccessMessage } from "~/models/message.server"; import { OrgIntegrationRepository } from "~/models/orgIntegration.server"; import { findProjectBySlug } from "~/models/project.server"; import { requireUserId } from "~/services/session.server"; import { + EnvironmentParamSchema, ProjectParamSchema, v3NewProjectAlertPath, v3NewProjectAlertPathConnectToSlackPath, @@ -12,7 +14,7 @@ import { export async function loader({ request, params }: LoaderFunctionArgs) { const userId = await requireUserId(request); - const { organizationSlug, projectParam } = ProjectParamSchema.parse(params); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); @@ -30,7 +32,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (integration) { return redirectWithSuccessMessage( - `${v3NewProjectAlertPath({ slug: organizationSlug }, project)}?option=slack`, + `${v3NewProjectAlertPath({ slug: organizationSlug }, project, { + slug: envParam, + })}?option=slack`, request, "Successfully connected your Slack workspace" ); @@ -40,7 +44,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { "SLACK", project.organizationId, request, - v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project) + v3NewProjectAlertPathConnectToSlackPath({ slug: organizationSlug }, project, { + slug: envParam, + }) ); } } From 273c30de5814704308cee005c032b4737bb324f8 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 17:08:09 +0000 Subject: [PATCH 74/95] Fix for waitpoint completion form moving --- .../route.tsx | 2 +- .../route.tsx | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) rename apps/webapp/app/routes/{resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete => resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete}/route.tsx (94%) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx index 372e9766cc..229aeb862e 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx @@ -71,7 +71,7 @@ import { import { CompleteWaitpointForm, ForceTimeout, -} from "../resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete/route"; +} from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route"; import { useEnvironment } from "~/hooks/useEnvironment"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx similarity index 94% rename from apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete/route.tsx rename to apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx index 0e9e9dbe86..816eeb0d06 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.waitpoints.$waitpointFriendlyId.complete/route.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx @@ -1,7 +1,7 @@ import { env } from "~/env.server"; import { parse } from "@conform-to/zod"; import { Form, useLocation, useNavigation, useSubmit } from "@remix-run/react"; -import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; +import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { conditionallyExportPacket, IOPacket, @@ -25,9 +25,10 @@ import { useProject } from "~/hooks/useProject"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { logger } from "~/services/logger.server"; import { requireUserId } from "~/services/session.server"; -import { ProjectParamSchema, v3RunsPath } from "~/utils/pathBuilder"; +import { EnvironmentParamSchema, ProjectParamSchema, v3RunsPath } from "~/utils/pathBuilder"; import { engine } from "~/v3/runEngine.server"; import { SpinnerWhite } from "~/components/primitives/Spinner"; +import { useEnvironment } from "~/hooks/useEnvironment"; const CompleteWaitpointFormData = z.discriminatedUnion("type", [ z.object({ @@ -44,13 +45,13 @@ const CompleteWaitpointFormData = z.discriminatedUnion("type", [ }), ]); -const Params = ProjectParamSchema.extend({ +const Params = EnvironmentParamSchema.extend({ waitpointFriendlyId: z.string(), }); export const action = async ({ request, params }: ActionFunctionArgs) => { const userId = await requireUserId(request); - const { organizationSlug, projectParam, waitpointFriendlyId } = Params.parse(params); + const { organizationSlug, projectParam, envParam, waitpointFriendlyId } = Params.parse(params); const formData = await request.formData(); const submission = parse(formData, { schema: CompleteWaitpointFormData }); @@ -181,7 +182,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { const errorMessage = `Something went wrong. Please try again.`; return redirectWithErrorMessage( - v3RunsPath({ slug: organizationSlug }, { slug: projectParam }), + v3RunsPath({ slug: organizationSlug }, { slug: projectParam }, { slug: envParam }), request, errorMessage ); @@ -191,12 +192,6 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { type FormWaitpoint = Pick; export function CompleteWaitpointForm({ waitpoint }: { waitpoint: FormWaitpoint }) { - const navigation = useNavigation(); - const submit = useSubmit(); - const isLoading = navigation.state !== "idle"; - const organization = useOrganization(); - const project = useProject(); - return (
{waitpoint.type === "DATETIME" ? ( @@ -227,6 +222,7 @@ function CompleteDateTimeWaitpointForm({ const isLoading = navigation.state !== "idle"; const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); const timeToComplete = waitpoint.completedAfter.getTime() - Date.now(); if (timeToComplete < 0) { @@ -239,7 +235,7 @@ function CompleteDateTimeWaitpointForm({ return (
@@ -292,8 +288,10 @@ function CompleteManualWaitpointForm({ waitpoint }: { waitpoint: { friendlyId: s const isLoading = navigation.state !== "idle"; const organization = useOrganization(); const project = useProject(); + const environment = useEnvironment(); + const currentJson = useRef("{\n\n}"); - const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/waitpoints/${waitpoint.friendlyId}/complete`; + const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/waitpoints/${waitpoint.friendlyId}/complete`; const submitForm = useCallback( (e: React.FormEvent) => { @@ -382,7 +380,9 @@ export function ForceTimeout({ waitpoint }: { waitpoint: { friendlyId: string } const isLoading = navigation.state !== "idle"; const organization = useOrganization(); const project = useProject(); - const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/waitpoints/${waitpoint.friendlyId}/complete`; + const environment = useEnvironment(); + + const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/waitpoints/${waitpoint.friendlyId}/complete`; return ( From 1a0e99da19991c979aab73c81c0596f039aa7e5a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 17:13:42 +0000 Subject: [PATCH 75/95] Bulk replay/cancel env fix --- .../route.tsx | 2 ++ apps/webapp/app/routes/resources.taskruns.bulk.cancel.ts | 4 +++- apps/webapp/app/routes/resources.taskruns.bulk.replay.ts | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index fbc2c00797..d4790dc2d3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -353,6 +353,7 @@ function CancelRuns({ onOpen }: { onOpen: (open: boolean) => void }) { + {[...selectedItems].map((runId) => ( ))} @@ -411,6 +412,7 @@ function ReplayRuns({ onOpen }: { onOpen: (open: boolean) => void }) { + {[...selectedItems].map((runId) => ( ))} diff --git a/apps/webapp/app/routes/resources.taskruns.bulk.cancel.ts b/apps/webapp/app/routes/resources.taskruns.bulk.cancel.ts index 487ee6d1e2..7a9c210934 100644 --- a/apps/webapp/app/routes/resources.taskruns.bulk.cancel.ts +++ b/apps/webapp/app/routes/resources.taskruns.bulk.cancel.ts @@ -1,5 +1,5 @@ import { parse } from "@conform-to/zod"; -import { ActionFunctionArgs } from "@remix-run/router"; +import { type ActionFunctionArgs } from "@remix-run/router"; import { z } from "zod"; import { prisma } from "~/db.server"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; @@ -11,6 +11,7 @@ import { CreateBulkActionService } from "~/v3/services/bulk/createBulkAction.ser const FormSchema = z.object({ organizationSlug: z.string(), projectSlug: z.string(), + environmentSlug: z.string(), failedRedirect: z.string(), runIds: z.array(z.string()).or(z.string()), }); @@ -65,6 +66,7 @@ export async function action({ request }: ActionFunctionArgs) { const path = v3RunsPath( { slug: submission.value.organizationSlug }, { slug: project.slug }, + { slug: submission.value.environmentSlug }, { bulkId: result.friendlyId, } diff --git a/apps/webapp/app/routes/resources.taskruns.bulk.replay.ts b/apps/webapp/app/routes/resources.taskruns.bulk.replay.ts index ee30158c77..77a3df0d6c 100644 --- a/apps/webapp/app/routes/resources.taskruns.bulk.replay.ts +++ b/apps/webapp/app/routes/resources.taskruns.bulk.replay.ts @@ -1,5 +1,5 @@ import { parse } from "@conform-to/zod"; -import { ActionFunctionArgs } from "@remix-run/router"; +import { type ActionFunctionArgs } from "@remix-run/router"; import { z } from "zod"; import { prisma } from "~/db.server"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; @@ -11,6 +11,7 @@ import { CreateBulkActionService } from "~/v3/services/bulk/createBulkAction.ser const FormSchema = z.object({ organizationSlug: z.string(), projectSlug: z.string(), + environmentSlug: z.string(), failedRedirect: z.string(), runIds: z.array(z.string()).or(z.string()), }); @@ -65,6 +66,7 @@ export async function action({ request }: ActionFunctionArgs) { const path = v3RunsPath( { slug: submission.value.organizationSlug }, { slug: project.slug }, + { slug: submission.value.environmentSlug }, { bulkId: result.friendlyId, } From a0b71792f95ba01fe5f17d6659105c5ce15ddc7d Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 17:14:38 +0000 Subject: [PATCH 76/95] Fix for alert webhook path --- .../v3/services/alerts/deliverAlert.server.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts b/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts index c373b11f8a..5eff1a6c54 100644 --- a/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts +++ b/apps/webapp/app/v3/services/alerts/deliverAlert.server.ts @@ -1,27 +1,27 @@ import { - ChatPostMessageArguments, + type ChatPostMessageArguments, ErrorCode, - WebAPIHTTPError, - WebAPIPlatformError, - WebAPIRateLimitedError, - WebAPIRequestError, + type WebAPIHTTPError, + type WebAPIPlatformError, + type WebAPIRateLimitedError, + type WebAPIRequestError, } from "@slack/web-api"; import { Webhook, TaskRunError, createJsonErrorObject, - RunFailedWebhook, - DeploymentFailedWebhook, - DeploymentSuccessWebhook, + type RunFailedWebhook, + type DeploymentFailedWebhook, + type DeploymentSuccessWebhook, isOOMRunError, } from "@trigger.dev/core/v3"; import assertNever from "assert-never"; import { subtle } from "crypto"; -import { Prisma, prisma, PrismaClientOrTransaction } from "~/db.server"; +import { type Prisma, type prisma, type PrismaClientOrTransaction } from "~/db.server"; import { env } from "~/env.server"; import { OrgIntegrationRepository, - OrganizationIntegrationForService, + type OrganizationIntegrationForService, } from "~/models/orgIntegration.server"; import { ProjectAlertEmailProperties, @@ -37,7 +37,7 @@ import { commonWorker } from "~/v3/commonWorker.server"; import { FINAL_ATTEMPT_STATUSES } from "~/v3/taskStatus"; import { BaseService } from "../baseService.server"; import { generateFriendlyId } from "~/v3/friendlyIdentifiers"; -import { ProjectAlertChannelType, ProjectAlertType } from "@trigger.dev/database"; +import { type ProjectAlertChannelType, type ProjectAlertType } from "@trigger.dev/database"; import { alertsRateLimiter } from "~/v3/alertsRateLimiter.server"; import { v3RunPath } from "~/utils/pathBuilder"; import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server"; @@ -380,6 +380,7 @@ export class DeliverAlertService extends BaseService { dashboardUrl: `${env.APP_ORIGIN}${v3RunPath( alert.project.organization, alert.project, + alert.environment, alert.taskRun )}`, }, From f09ff38a9361e684bc7ffa3b2350493824a352a5 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 17:26:13 +0000 Subject: [PATCH 77/95] Redirect projects/v3/* to project/* --- .../routes/orgs.$organizationSlug.projects.v3.$.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 apps/webapp/app/routes/orgs.$organizationSlug.projects.v3.$.ts diff --git a/apps/webapp/app/routes/orgs.$organizationSlug.projects.v3.$.ts b/apps/webapp/app/routes/orgs.$organizationSlug.projects.v3.$.ts new file mode 100644 index 0000000000..58eb8ea2fe --- /dev/null +++ b/apps/webapp/app/routes/orgs.$organizationSlug.projects.v3.$.ts @@ -0,0 +1,11 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const organizationSlug = params.organizationSlug; + const path = params["*"]; + + const url = new URL(request.url); + url.pathname = `/orgs/${organizationSlug}/projects/${path}`; + + return redirect(url.toString()); +}; From 1f95cc79d01cc0d85407efdf58549cd78e10acfd Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 17:41:35 +0000 Subject: [PATCH 78/95] Fixes for CLI redirect routes --- ....v3.$projectRef.deployments.$deploymentParam.ts | 4 ++-- ...rojects.v3.$projectRef.environment-variables.ts | 2 +- .../app/routes/projects.v3.$projectRef.runs.ts | 14 ++++++-------- .../app/routes/projects.v3.$projectRef.test.ts | 12 +++++++++--- apps/webapp/app/routes/projects.v3.$projectRef.ts | 4 ++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts b/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts index a27725c826..e4f83a13ad 100644 --- a/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts +++ b/apps/webapp/app/routes/projects.v3.$projectRef.deployments.$deploymentParam.ts @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; import { requireUserId } from "~/services/session.server"; @@ -35,6 +35,6 @@ export async function loader({ params, request }: LoaderFunctionArgs) { // Redirect to the project's runs page return redirect( - `/orgs/${project.organization.slug}/projects/v3/${project.slug}/deployments/${validatedParams.deploymentParam}` + `/orgs/${project.organization.slug}/projects/${project.slug}/deployments/${validatedParams.deploymentParam}` ); } diff --git a/apps/webapp/app/routes/projects.v3.$projectRef.environment-variables.ts b/apps/webapp/app/routes/projects.v3.$projectRef.environment-variables.ts index 0882c3a7af..320fd23056 100644 --- a/apps/webapp/app/routes/projects.v3.$projectRef.environment-variables.ts +++ b/apps/webapp/app/routes/projects.v3.$projectRef.environment-variables.ts @@ -34,6 +34,6 @@ export async function loader({ params, request }: LoaderFunctionArgs) { // Redirect to the project's runs page return redirect( - `/orgs/${project.organization.slug}/projects/v3/${project.slug}/environment-variables` + `/orgs/${project.organization.slug}/projects/${project.slug}/environment-variables` ); } diff --git a/apps/webapp/app/routes/projects.v3.$projectRef.runs.ts b/apps/webapp/app/routes/projects.v3.$projectRef.runs.ts index 0cb788dbd0..f10f45d53a 100644 --- a/apps/webapp/app/routes/projects.v3.$projectRef.runs.ts +++ b/apps/webapp/app/routes/projects.v3.$projectRef.runs.ts @@ -1,7 +1,7 @@ -import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; -import { EnvSlug, isEnvSlug } from "~/models/api-key.server"; +import { type EnvSlug, isEnvSlug } from "~/models/api-key.server"; import { requireUserId } from "~/services/session.server"; const ParamsSchema = z.object({ @@ -41,15 +41,13 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const env = await getEnvFromSlug(project.id, userId, envSlug); if (env) { - url.searchParams.set("environments", env.id); + return redirect( + `/orgs/${project.organization.slug}/projects/${project.slug}/env/${envSlug}/runs${url.search}` + ); } - - url.searchParams.delete("envSlug"); } - return redirect( - `/orgs/${project.organization.slug}/projects/v3/${project.slug}/runs${url.search}` - ); + return redirect(`/orgs/${project.organization.slug}/projects/${project.slug}`); } async function getEnvFromSlug(projectId: string, userId: string, envSlug: EnvSlug) { diff --git a/apps/webapp/app/routes/projects.v3.$projectRef.test.ts b/apps/webapp/app/routes/projects.v3.$projectRef.test.ts index 5695f28616..a853a29f5e 100644 --- a/apps/webapp/app/routes/projects.v3.$projectRef.test.ts +++ b/apps/webapp/app/routes/projects.v3.$projectRef.test.ts @@ -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 { v3ProjectPath, v3TestPath } from "~/utils/pathBuilder"; const ParamsSchema = z.object({ projectRef: z.string(), @@ -33,8 +34,13 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } const url = new URL(request.url); + const environment = url.searchParams.get("environment"); - return redirect( - `/orgs/${project.organization.slug}/projects/v3/${project.slug}/test${url.search}` - ); + if (environment) { + return redirect( + v3TestPath({ slug: project.organization.slug }, { slug: project.slug }, { slug: environment }) + ); + } + + return redirect(v3ProjectPath({ slug: project.organization.slug }, { slug: project.slug })); } diff --git a/apps/webapp/app/routes/projects.v3.$projectRef.ts b/apps/webapp/app/routes/projects.v3.$projectRef.ts index 4a702ff2ab..856a93c4ac 100644 --- a/apps/webapp/app/routes/projects.v3.$projectRef.ts +++ b/apps/webapp/app/routes/projects.v3.$projectRef.ts @@ -1,4 +1,4 @@ -import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; import { z } from "zod"; import { prisma } from "~/db.server"; import { requireUserId } from "~/services/session.server"; @@ -33,5 +33,5 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } // Redirect to the project's runs page - return redirect(`/orgs/${project.organization.slug}/projects/v3/${project.slug}`); + return redirect(`/orgs/${project.organization.slug}/projects/${project.slug}`); } From d412b82e73917d8d1df58276a4d548e8f20195ff Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 17:48:52 +0000 Subject: [PATCH 79/95] Remove welcome email (unused) --- apps/webapp/app/services/email.server.ts | 31 +++++++----------------- internal-packages/emails/src/index.tsx | 9 ------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/apps/webapp/app/services/email.server.ts b/apps/webapp/app/services/email.server.ts index cb9e94c3b7..0f14fb28b0 100644 --- a/apps/webapp/app/services/email.server.ts +++ b/apps/webapp/app/services/email.server.ts @@ -32,8 +32,10 @@ const alertsClient = singleton( ); function buildTransportOptions(alerts?: boolean): MailTransportOptions { - const transportType = alerts ? env.ALERT_EMAIL_TRANSPORT : env.EMAIL_TRANSPORT - logger.debug(`Constructing email transport '${transportType}' for usage '${alerts?'alerts':'general'}'`) + const transportType = alerts ? env.ALERT_EMAIL_TRANSPORT : env.EMAIL_TRANSPORT; + logger.debug( + `Constructing email transport '${transportType}' for usage '${alerts ? "alerts" : "general"}'` + ); switch (transportType) { case "aws-ses": @@ -43,8 +45,8 @@ function buildTransportOptions(alerts?: boolean): MailTransportOptions { type: "resend", config: { apiKey: alerts ? env.ALERT_RESEND_API_KEY : env.RESEND_API_KEY, - } - } + }, + }; case "smtp": return { type: "smtp", @@ -54,9 +56,9 @@ function buildTransportOptions(alerts?: boolean): MailTransportOptions { secure: alerts ? env.ALERT_SMTP_SECURE : env.SMTP_SECURE, auth: { user: alerts ? env.ALERT_SMTP_USER : env.SMTP_USER, - pass: alerts ? env.ALERT_SMTP_PASSWORD : env.SMTP_PASSWORD - } - } + pass: alerts ? env.ALERT_SMTP_PASSWORD : env.SMTP_PASSWORD, + }, + }, }; default: return { type: undefined }; @@ -87,21 +89,6 @@ export async function sendPlainTextEmail(options: SendPlainTextOptions) { return client.sendPlainText(options); } -export async function scheduleWelcomeEmail(user: User) { - //delay for one minute in development, longer in production - const delay = process.env.NODE_ENV === "development" ? 1000 * 60 : 1000 * 60 * 22; - - await workerQueue.enqueue( - "scheduleEmail", - { - email: "welcome", - to: user.email, - name: user.name ?? undefined, - }, - { runAt: new Date(Date.now() + delay) } - ); -} - export async function scheduleEmail(data: DeliverEmail, delay?: { seconds: number }) { const runAt = delay ? new Date(Date.now() + delay.seconds * 1000) : undefined; await workerQueue.enqueue("scheduleEmail", data, { runAt }); diff --git a/internal-packages/emails/src/index.tsx b/internal-packages/emails/src/index.tsx index 189849d5c8..16e1da7ccb 100644 --- a/internal-packages/emails/src/index.tsx +++ b/internal-packages/emails/src/index.tsx @@ -19,10 +19,6 @@ export { type MailTransportOptions }; export const DeliverEmailSchema = z .discriminatedUnion("email", [ - z.object({ - email: z.literal("welcome"), - name: z.string().optional(), - }), z.object({ email: z.literal("magic_link"), magicLink: z.string().url(), @@ -86,11 +82,6 @@ export class EmailClient { component: ReactElement; } { switch (data.email) { - case "welcome": - return { - subject: "✨ Welcome to Trigger.dev!", - component: , - }; case "magic_link": return { subject: "Magic sign-in link for Trigger.dev", From 28135d547b987bbe53c70b30d33c6cb46c8f1a47 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 18:13:05 +0000 Subject: [PATCH 80/95] Change how we count schedules towards your limits --- .../v3/ScheduleListPresenter.server.ts | 8 +++---- .../app/v3/services/checkSchedule.server.ts | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts index db16d742fa..19b84121d0 100644 --- a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts @@ -5,6 +5,7 @@ import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; import { getCurrentPlan, getLimit, getLimits } from "~/services/platform.v3.server"; import { calculateNextScheduledTimestamp } from "~/v3/utils/calculateNextSchedule.server"; import { BasePresenter } from "./basePresenter.server"; +import { CheckScheduleService } from "~/v3/services/checkSchedule.server"; type ScheduleListOptions = { projectId: string; @@ -86,10 +87,9 @@ export class ScheduleListPresenter extends BasePresenter { }, }); - const schedulesCount = await this._prisma.taskSchedule.count({ - where: { - projectId, - }, + const schedulesCount = await CheckScheduleService.getUsedSchedulesCount({ + prisma: this._replica, + environments: project.environments, }); //get all possible scheduled tasks diff --git a/apps/webapp/app/v3/services/checkSchedule.server.ts b/apps/webapp/app/v3/services/checkSchedule.server.ts index a959d13008..c6296df874 100644 --- a/apps/webapp/app/v3/services/checkSchedule.server.ts +++ b/apps/webapp/app/v3/services/checkSchedule.server.ts @@ -4,6 +4,7 @@ import { BaseService, ServiceValidationError } from "./baseService.server"; import { getLimit } from "~/services/platform.v3.server"; import { getTimezones } from "~/utils/timezones.server"; import { env } from "~/env.server"; +import { type PrismaClientOrTransaction, type RuntimeEnvironmentType } from "@trigger.dev/database"; type Schedule = { cron: string; @@ -90,4 +91,27 @@ export class CheckScheduleService extends BaseService { } } } + + static async getUsedSchedulesCount({ + prisma, + environments, + }: { + prisma: PrismaClientOrTransaction; + environments: { id: string; type: RuntimeEnvironmentType }[]; + }) { + const deployedEnvironments = environments.filter((env) => env.type !== "DEVELOPMENT"); + const schedulesCount = await prisma.taskScheduleInstance.count({ + where: { + environmentId: { + in: deployedEnvironments.map((env) => env.id), + }, + active: true, + taskSchedule: { + active: true, + }, + }, + }); + + return schedulesCount; + } } From 68317d62a4b9b15d5f7f9363b096ad569fc9d21c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 18:14:01 +0000 Subject: [PATCH 81/95] Use new schedules limits when checking a schedule --- apps/webapp/app/v3/services/checkSchedule.server.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/v3/services/checkSchedule.server.ts b/apps/webapp/app/v3/services/checkSchedule.server.ts index c6296df874..eff6dcd3ad 100644 --- a/apps/webapp/app/v3/services/checkSchedule.server.ts +++ b/apps/webapp/app/v3/services/checkSchedule.server.ts @@ -70,6 +70,12 @@ export class CheckScheduleService extends BaseService { }, select: { organizationId: true, + environments: { + select: { + id: true, + type: true, + }, + }, }, }); @@ -78,10 +84,9 @@ export class CheckScheduleService extends BaseService { } const limit = await getLimit(project.organizationId, "schedules", 100_000_000); - const schedulesCount = await this._prisma.taskSchedule.count({ - where: { - projectId, - }, + const schedulesCount = await CheckScheduleService.getUsedSchedulesCount({ + prisma: this._prisma, + environments: project.environments, }); if (schedulesCount >= limit) { From 3b719950002924272b9c538d8202a1abcb0bcb62 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 18:39:26 +0000 Subject: [PATCH 82/95] Added projectId back in to task queries (indexes) --- .../app/presenters/v3/TaskListPresenter.server.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts index 24dff779e0..8cba39eda3 100644 --- a/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TaskListPresenter.server.ts @@ -43,6 +43,7 @@ export class TaskListPresenter extends BasePresenter { select: { id: true, type: true, + projectId: true, }, where: { slug: environmentSlug, @@ -88,23 +89,26 @@ export class TaskListPresenter extends BasePresenter { //then get the activity for each task const activity = this.#getActivity( tasks.map((t) => t.slug), + environment.projectId, environment.id ); const runningStats = this.#getRunningStats( tasks.map((t) => t.slug), + environment.projectId, environment.id ); const durations = this.#getAverageDurations( tasks.map((t) => t.slug), + environment.projectId, environment.id ); return { tasks, environment, activity, runningStats, durations }; } - async #getActivity(tasks: string[], environmentId: string) { + async #getActivity(tasks: string[], projectId: string, environmentId: string) { if (tasks.length === 0) { return {}; } @@ -126,6 +130,7 @@ export class TaskListPresenter extends BasePresenter { ${sqlDatabaseSchema}."TaskRun" as tr WHERE tr."taskIdentifier" IN (${Prisma.join(tasks)}) + AND tr."projectId" = ${projectId} AND tr."runtimeEnvironmentId" = ${environmentId} AND tr."createdAt" >= (current_date - interval '6 days') GROUP BY @@ -179,7 +184,7 @@ export class TaskListPresenter extends BasePresenter { }, {} as Record)[]>); } - async #getRunningStats(tasks: string[], environmentId: string) { + async #getRunningStats(tasks: string[], projectId: string, environmentId: string) { if (tasks.length === 0) { return {}; } @@ -199,6 +204,7 @@ export class TaskListPresenter extends BasePresenter { ${sqlDatabaseSchema}."TaskRun" as tr WHERE tr."taskIdentifier" IN (${Prisma.join(tasks)}) + AND tr."projectId" = ${projectId} AND tr."runtimeEnvironmentId" = ${environmentId} AND tr."status" = ANY(ARRAY[${Prisma.join([ ...QUEUED_STATUSES, @@ -239,7 +245,7 @@ export class TaskListPresenter extends BasePresenter { return result; } - async #getAverageDurations(tasks: string[], environmentId: string) { + async #getAverageDurations(tasks: string[], projectId: string, environmentId: string) { if (tasks.length === 0) { return {}; } @@ -257,6 +263,7 @@ export class TaskListPresenter extends BasePresenter { ${sqlDatabaseSchema}."TaskRun" as tr WHERE tr."taskIdentifier" IN (${Prisma.join(tasks)}) + AND tr."projectId" = ${projectId} AND tr."runtimeEnvironmentId" = ${environmentId} AND tr."createdAt" >= (current_date - interval '6 days') AND tr."status" IN ('COMPLETED_SUCCESSFULLY', 'COMPLETED_WITH_ERRORS') From 36a232ab4ebc12ca8592f6dde54408d861ef631a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 21:43:04 +0000 Subject: [PATCH 83/95] WIP dev presence --- .../app/assets/icons/ConnectionIcons.tsx | 53 +++++++ apps/webapp/app/components/DevPresence.tsx | 86 +++++++++++ .../app/components/navigation/SideMenu.tsx | 4 +- .../app/components/primitives/Popover.tsx | 2 +- .../webapp/app/routes/engine.v1.dev.config.ts | 4 +- .../app/routes/engine.v1.dev.presence.ts | 3 - ...rojectParam.env.$envParam.dev.presence.tsx | 141 ++++++++++++++++++ apps/webapp/app/utils/sse.ts | 18 ++- 8 files changed, 298 insertions(+), 13 deletions(-) create mode 100644 apps/webapp/app/assets/icons/ConnectionIcons.tsx create mode 100644 apps/webapp/app/components/DevPresence.tsx create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx diff --git a/apps/webapp/app/assets/icons/ConnectionIcons.tsx b/apps/webapp/app/assets/icons/ConnectionIcons.tsx new file mode 100644 index 0000000000..74f1ee5458 --- /dev/null +++ b/apps/webapp/app/assets/icons/ConnectionIcons.tsx @@ -0,0 +1,53 @@ +export function ConnectedIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} + +export function DisconnectedIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/apps/webapp/app/components/DevPresence.tsx b/apps/webapp/app/components/DevPresence.tsx new file mode 100644 index 0000000000..f323dd725d --- /dev/null +++ b/apps/webapp/app/components/DevPresence.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from "react"; +import { ConnectedIcon, DisconnectedIcon } from "~/assets/icons/ConnectionIcons"; +import { useDebounce } from "~/hooks/useDebounce"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useEventSource } from "~/hooks/useEventSource"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog"; +import { Button } from "./primitives/Buttons"; + +export function useDevPresence() { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const streamedEvents = useEventSource( + `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/dev/presence`, + { + event: "presence", + } + ); + + const [lastSeen, setLastSeen] = useState(null); + + const debouncer = useDebounce((seen: Date | null) => { + setLastSeen(seen); + }, 3_000); + + useEffect(() => { + if (streamedEvents === null) { + debouncer(null); + return; + } + + try { + const data = JSON.parse(streamedEvents) as any; + if ("lastSeen" in data && data.lastSeen) { + // Parse the timestamp string into a Date object + try { + const lastSeenDate = new Date(data.lastSeen); + debouncer(lastSeenDate); + } catch (error) { + console.log("DevPresence: Failed to parse lastSeen timestamp", { error }); + debouncer(null); + } + } else { + debouncer(null); + } + } catch (error) { + console.log("DevPresence: Failed to parse presence message", { error }); + debouncer(null); + } + }, [streamedEvents]); + + return { lastSeen }; +} + +export function DevPresence() { + const { lastSeen } = useDevPresence(); + const isConnected = lastSeen && lastSeen > new Date(Date.now() - 120_000); + + return ( + + + + ); +} diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 9d17638261..9988f28de5 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -71,6 +71,7 @@ import { SideMenuItem } from "./SideMenuItem"; import { SideMenuSection } from "./SideMenuSection"; import { ButtonContent, LinkButton } from "../primitives/Buttons"; import { TextLink } from "../primitives/TextLink"; +import { DevPresence } from "../DevPresence"; type SideMenuUser = Pick & { isImpersonating: boolean }; export type SideMenuProject = Pick< @@ -141,12 +142,13 @@ export function SideMenu({
-
+
+ {environment.type === "DEVELOPMENT" && }
diff --git a/apps/webapp/app/components/primitives/Popover.tsx b/apps/webapp/app/components/primitives/Popover.tsx index ac7f7090e2..6fc0bffe6f 100644 --- a/apps/webapp/app/components/primitives/Popover.tsx +++ b/apps/webapp/app/components/primitives/Popover.tsx @@ -161,7 +161,7 @@ function PopoverArrowTrigger({ { - //todo set a string instead, with the expire on the same call - //won't need multi - // Set initial presence with more context await redis.setex(presenceKey, env.DEV_PRESENCE_TTL_MS / 1000, Date.now().toString()); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx new file mode 100644 index 0000000000..4d0188d092 --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx @@ -0,0 +1,141 @@ +import { $replica } from "~/db.server"; +import { requireUserId } from "~/services/session.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { env } from "~/env.server"; +import { DevPresenceStream } from "~/presenters/v3/DevPresenceStream.server"; +import { logger } from "~/services/logger.server"; +import { createSSELoader, type SendFunction } from "~/utils/sse"; +import Redis from "ioredis"; + +export const loader = createSSELoader({ + timeout: env.DEV_PRESENCE_TTL_MS, + interval: env.DEV_PRESENCE_POLL_INTERVAL_MS, + debug: true, + handler: async ({ id, controller, debug, request, params }) => { + const userId = await requireUserId(request); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); + + const environment = await $replica.runtimeEnvironment.findFirst({ + where: { + slug: envParam, + type: "DEVELOPMENT", + project: { + slug: projectParam, + }, + organization: { + slug: organizationSlug, + members: { + some: { + userId, + }, + }, + }, + }, + }); + + if (!environment) { + throw new Response("Not Found", { status: 404 }); + } + + const presenceKey = DevPresenceStream.getPresenceKey(environment.id); + const presenceChannel = DevPresenceStream.getPresenceChannel(environment.id); + + // Create two Redis clients - one for subscribing and one for regular commands + const redisConfig = { + port: env.RUN_ENGINE_DEV_PRESENCE_REDIS_PORT ?? undefined, + host: env.RUN_ENGINE_DEV_PRESENCE_REDIS_HOST ?? undefined, + username: env.RUN_ENGINE_DEV_PRESENCE_REDIS_USERNAME ?? undefined, + password: env.RUN_ENGINE_DEV_PRESENCE_REDIS_PASSWORD ?? undefined, + enableAutoPipelining: true, + ...(env.RUN_ENGINE_DEV_PRESENCE_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), + }; + + // Subscriber client for pubsub + const subRedis = new Redis(redisConfig); + + // Command client for regular Redis commands + const cmdRedis = new Redis(redisConfig); + + const checkAndSendPresence = async (send: SendFunction) => { + try { + // Use the command client for the GET operation + const currentPresenceValue = await cmdRedis.get(presenceKey); + const isConnected = !!currentPresenceValue; + + // Format lastSeen as ISO string if it exists + let lastSeen = null; + if (currentPresenceValue) { + // Check if it's a numeric timestamp + if (!isNaN(Number(currentPresenceValue))) { + // Convert numeric timestamp to ISO string + lastSeen = new Date(parseInt(currentPresenceValue, 10)).toISOString(); + } else { + // It's already a string format, make sure it's ISO + try { + lastSeen = new Date(currentPresenceValue).toISOString(); + } catch (e) { + // If parsing fails, use current time as fallback + lastSeen = new Date().toISOString(); + logger.warn("Failed to parse lastSeen value, using current time", { + originalValue: currentPresenceValue, + }); + } + } + } + + send({ + event: "presence", + data: JSON.stringify({ + type: isConnected ? "connected" : "disconnected", + environmentId: environment.id, + timestamp: new Date().toISOString(), // Also standardize this to ISO + lastSeen: lastSeen, + }), + }); + + return isConnected; + } catch (error) { + // Handle the case where the controller is closed + logger.debug("Failed to send presence data, stream might be closed", { error }); + return false; + } + }; + + return { + beforeStream: async () => { + logger.debug("Start dev presence listening SSE session", { + environmentId: environment.id, + presenceChannel, + }); + }, + initStream: async ({ send }) => { + await checkAndSendPresence(send); + + //start subscribing with the subscriber client + await subRedis.subscribe(presenceChannel); + + subRedis.on("message", async (channel, message) => { + if (channel === presenceChannel) { + try { + await checkAndSendPresence(send); + } catch (error) { + logger.error("Failed to parse presence message", { error, message }); + } + } + }); + + send({ event: "time", data: new Date().toISOString() }); + }, + iterator: async ({ send, date }) => { + await checkAndSendPresence(send); + }, + cleanup: async ({ send }) => { + await checkAndSendPresence(send); + + await subRedis.unsubscribe(presenceChannel); + await subRedis.quit(); + await cmdRedis.quit(); + }, + }; + }, +}); diff --git a/apps/webapp/app/utils/sse.ts b/apps/webapp/app/utils/sse.ts index ef6135a866..9f3452cf93 100644 --- a/apps/webapp/app/utils/sse.ts +++ b/apps/webapp/app/utils/sse.ts @@ -1,8 +1,9 @@ -import { LoaderFunctionArgs } from "@remix-run/node"; +import { type LoaderFunctionArgs } from "@remix-run/node"; +import { type Params } from "@remix-run/router"; import { eventStream } from "remix-utils/sse/server"; import { setInterval } from "timers/promises"; -type SendFunction = Parameters[1]>[0]; +export type SendFunction = Parameters[1]>[0]; type HandlerParams = { send: SendFunction; @@ -15,12 +16,13 @@ type SSEHandlers = { initStream?: (params: HandlerParams) => Promise | boolean | void; /** Return false to stop */ iterator?: (params: HandlerParams & { date: Date }) => Promise | boolean | void; - cleanup?: () => void; + cleanup?: (params: HandlerParams) => void; }; type SSEContext = { id: string; request: Request; + params: Params; controller: AbortController; debug: (message: string) => void; }; @@ -38,19 +40,23 @@ const connections: Set = new Set(); export function createSSELoader(options: SSEOptions) { const { timeout, interval = 500, debug = false, handler } = options; - return async function loader({ request }: LoaderFunctionArgs) { + return async function loader({ request, params }: LoaderFunctionArgs) { const id = request.headers.get("x-request-id") || Math.random().toString(36).slice(2, 8); const internalController = new AbortController(); const timeoutSignal = AbortSignal.timeout(timeout); const log = (message: string) => { - if (debug) console.log(`SSE: [${id}] ${message} (${connections.size} open connections)`); + if (debug) + console.log( + `SSE: [${request.url} ${id}] ${message} (${connections.size} open connections)` + ); }; const context: SSEContext = { id, request, + params, controller: internalController, debug: log, }; @@ -167,7 +173,7 @@ export function createSSELoader(options: SSEOptions) { log("Cleanup called"); if (handlers.cleanup) { try { - handlers.cleanup(); + handlers.cleanup({ send }); } catch (error) { log( `Error in cleanup handler: ${ From c9e6a6d41b15a45f16a963625cfdb3fc02244275 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:00:43 +0000 Subject: [PATCH 84/95] CLI modal --- .../app/assets/images/cli-connected.png | Bin 0 -> 4488 bytes .../app/assets/images/cli-disconnected.png | Bin 0 -> 3538 bytes apps/webapp/app/components/DevPresence.tsx | 48 ++++++++++++++++-- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 apps/webapp/app/assets/images/cli-connected.png create mode 100644 apps/webapp/app/assets/images/cli-disconnected.png diff --git a/apps/webapp/app/assets/images/cli-connected.png b/apps/webapp/app/assets/images/cli-connected.png new file mode 100644 index 0000000000000000000000000000000000000000..cd6b4e37fe1d35e65cd7e7ec64e3bb25bc56e338 GIT binary patch literal 4488 zcmXX~cQ{<|%-V-e|03ekij%}zYiErm- z?h4|E8ur90005Zz|2!mmW_$<4pCkchx*9;;D91K&Lhhz+qz(Y}X}5kmQ2+ovr=FI& zc?by>Gv>o+H9vT`Y3`!1jXvQ-TD7xT<5p=bC7YLSxzDC9mANN7&Mq@02Lgtg3V@!swvUh9)H{J_h^Bb=UmoyBIp7B!b&FX2gEvTw) zhK|yc004`A;HFh+Ym?ufCggtFRZEcr(!PO~<-_$NiJ zi%+Cu<_GN#+3WEoPi#jd?ChG3Xt-|m!q^Y%-G+|#yIj6`%lktdPgm0~@8_3|#f55% zQ!PIIDboe5`;7TS)BvgW!9}o!6iqvMTrSndp&koF59EAejv4!mGN}+8ilYG)pX&y1 zT7UuI$$eoZcbwxvV|9JxnX8`b;%xK)h*@t7UG>8dD5V6_e_DN>v0j^xX%msgtV&vd zVN-@$dHEa{0U%Y}7B|uF)U!G(llGspGm5Ny?{JBXi@qrdrjllKjarJ1;sJn^p-%dJ z8B#hIdmCn(!7nax6|^#G+&GDn^FH;s7xk^Dp9G1E)F_rlzn+;kC=81LRb)n6!*Pyb zSz}||0Ibg4&}MBX^i2BfT&H&~!ds8YeZHb5AnNfD!!tIZTd;jXGs!A6A3c*}9<6g@ zsljmvd2mevz*=|k$7|tzBU?Ass@2M8eIK%9nO*Y&z-=c>a8|4kTSVaDKhsMnSt`U{ zsVRp`I+=c+BpGnqZ4f!hrMxCIm)9hDMKs?j@W?Wr@3H6|9 zG5|PdD4CIoG|-S4P}XGs%HR$Fd5nI`$(t~JkDz9%`{Tsj1(^G$t9N-5kw;?~m&bHe z#H>d}Z_ScoBLUq+`{FlJKBP1N5TND~0__n^@nih6hab9j^P4DLN;6Hb!9RX=!FP&L zvmoHM-`6X|>rl2w+|`mH$vy-Iu1jmef(;2wMMCLGgR(7~^)PlY?aX1ZD@r;y&485V zRie<;@bXJBb*tnn`$TI{*B66Xfs#Zruv({WmyZMp72)7@NVlBpX+$E~jn2ahlA1tc zV0!)u`~pC}bk9e3l^uhh1O&Adp1OYamh`6ZfWsu=Af9k4m%FQ)MWM#$NgkvCk0}2A z_K(@la7Jg8_J!&1qabjoyx9Zz%UZ7(?w^uhKqgBLIV0E`52s%8%mU5yvo=NMf$-sD z+M`(>R0bokId5RV4sUn_ooeieJvl$H+s2JFc8;xwef{v!B?rD%efxa?{MH5wJM>C$ z)wkj9n`A+i&w3A4jlD_DxyC}vG|0Mz*fp3do^dm{f=0qC4R8h$1~`3HT&Jo4>Mu}b zyGVzUj03pYfjuhi>PKqP4%N=J-F24>iFlsqAad-|SHx<>COb}ztot6jE3`WyH?EZp z%LBfL$avm09Z3g%S5K2D5%y(EgJ-F5xa)U*=8m5eu}uERG8PSRMvkIsd&$`V95X( z5fN4`M|CGNcHa}IWm8+MkF_hS93*Z^4st2amAvFV@W@iVbVTcXAX!m+iZgeG=1r_R zoHv;dzdWvOIg)1eMETFuJa!~G7VspuP|)2J-hLh!-X$upMpVHJrk~q#a6K~Y&Rd(B z$SobH!zLqX$@4T#d13a|(sl|gn@aTVz{lyjSaXFL?=cjgu(U&xX6jWv4*U(b6E%9d z-L1x88t9so+Kt#r^)q3LrtD@V2;bL7H|*uhn=FDMgRP2FaTq$?SE2O(jtZyge}8Ai z6@=gDd+RsY#%w|tO{pe~-B@^j#yG#6C1jKF26I(`;P)`C=&?;0^^GJ@SY}q-Zdi5zO)H;W+Ww6vW$kINKG~XPQSl%pWU7&BR$^?N~?(>49xMhkt;f%pE@W^@y>$j z-Wx4mI6UbjZK-L=c%WM;>2<1IoI*vHPW;gca>p>&>aG7g&mL^~y{RZwdT;RQu4R)T zNNl(&ZIv7Ji{5n;#aLWBj+EGq(xVigi4rVCtoSb@K$&t}yRIjQ>`H5J`t8Gzp)aD` zbfJ(7FO+Z7&U9l^B|dTS;fhjAoBuvM7j$6$1z$YBlk~VWIVeT3?#ek$Eyf&`c}9y1 zJND930rjsd)~X!d3g+-YW<(`Hu$gUgVb{u`1e@0t@THkh-6ByQcNgFwaW^ZH|0>q^6pH1XvPssY$#H6Oh-J|1C zV7A6fBU#l%2YMi}DL3y2btfO&uR0&qHs1MwM%#}Sj)N`Si;7(uGUB8(W)r26we8U5 z(+3@C4Owl$<%bQ1GLhbO6qZK(8~s&_SbA(HrF~acP?egVd(OZrzd-G?hXxQ%C9Wf} zw6u7o#$gMzz=fPGXVW;>ue!Ov`R+X!=gD5aI^%a+v;C`Ct|Gvc$yqDx!#XJ3=R@1mOvgGnC zo&Fobsisq}!lzQ`HuprN)jO%!vQDh6#XVyP$#ErSXcszZSi6q@HF%NlQ$jph(i3V6 zPK3Ey)14<Yq3wgX*=1>%Rc(~|Q*7`lf!(H8LJ4enKG5`lyfFB(4A- z%n(XY`IdmsWnK%Y$F*HO0L=%SK-TnXN_4tgZXwHpp{Yv8Z%HA*Z1_;NUUAl ztwUM#3)4fAhDWL3s?0E0@cQ_=yovsIPv$6JBB&x;Z(#DDHWa}1Ass|y`CWT*gl~zE zjtE<5uCd=DJ%YDherjjs>Z>o59Q#ujl&F3MrM&WZR>JgY= z+aBag%dSoZ7Jgf$5T&R@*yiV=)j z#Gn<_{>&3-@tLgs`dgTmITJmXHx^rsZ^*?Kg<8?`#IJTUJ+Y=<`gTmUH>4b@jU6H3L{})Y*#O8#bGKsbSmGtJL$XEk#jTV$lWaEC{0n2^U^R$lFYD z5ZoGuUY1Ic?N7R}7Ge@DlXn(s${0NxcB|^6Jxs0fzY))u5pv7fSFIvy##P->{Caba zjAEMWJCM3inVRk(s@(FCEA|&_^KPwGGa_y6yj9LZIIS?4M^~ zjLH6H2)rvH-SLSk-D_!9oiFTg#;Jnf+$9cn9t-tX7{IQ)M^KOe@q0&^Ri|cM)CdjVyj_Xa2uR-J$wt5W-MABllmv27Fj0wp!nEGr6EWPCD#bGbOQyNu^$d2h2<~sofqu56=@pf#d{iQ<2^isj5KV=$`HvI_ zE@9!mU=Kg!?&m+~)pohEq+hMl@*-M?LW1u^N2SL?WTXx8E|zAZ{4;4m8;D}49xt!o zg!iA+xpog!th{TKClsVIS=xH2P4}|PIQ?8sol?60t;ep!l~|hQMQK~#gTWI=Zp2!B=4RQ{$ge$zlX{oQrKKF zlP-G8tm@j&m>1LOyBbp$WlF)y%GRLrOCN>@<3L_gNJ)W34|&ddi*eO$DPfhi3XxexjdG!r`y(Ir8j-%?m{aFNxRI+2V)$NeKm z!M*>-+UwrjlC!mRyz?#|_n!{+(F%OopHSJnT%Z{j8L_1-DjtI$*T@N364 zGC|#UW$Hl%wjip7q%qy=8iVo_Kiz0}M~suUVN zCub2DE(*3r71<;8%siJQO%0N6DVric1$68`hK{yS&Ju-GOx<`aP0!-3rbsaWK$x#g zP$}IHYT>tEQWUx=Q_^0cNpRmNv8Ez6#VaY^6-P6!tQAEwoZPa zFm`Lo&{^Wwy*!jD*J-##(h6j<5d*UFM;s literal 0 HcmV?d00001 diff --git a/apps/webapp/app/assets/images/cli-disconnected.png b/apps/webapp/app/assets/images/cli-disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..dff3ecc106a629b2b74f5f71699cd156bab10361 GIT binary patch literal 3538 zcmXX}c|26>8-I;8%O$c^Lf1}-GO`azCChM2Axu|xgF>!lLLr5^g&1ScGWKjUy0$FW zzD%THW*8c(A%ir@kbbA`?~nJK^StMMpU-)p=lguW&v}5fJ0~J|P!Iqha^bwCBLJ{d z(6g%mA9TGzkS~R9`$En;hXNp_{Bv+!a8z1{Cb>c#&sl)lvBONr;PElHH3y*nvC!^y zUdWTr1xxcwe{rqOjyt-Zlj`4?#8kWr+65rD{^&wvOxB`VSZxy2q)nqylALl+{6x%cuXK1~JN30Db%cR5Jkm*ht&*W2^C2KyFH0$a{ zOeoGlQ!G9ee24@-O3hE-E5-8&9I(|p7q8QDh!G3cd#Ik%aD3DMO>4yn4y zapHy}0Mu;5SKZHv`YCIA1u$4QpQUr)n({-#L%~POR9dhTp)4{#3?SqiT*ysZb~ii} zVkmuX=}*;eoL-`6t#R3&T4PSwWE`}q5x1hJ{oEJ zJxl2Znx_5gOT#)wv>W0T=yc+9HRg1nekabyO*>fgdtib~1^}EJEb0(*6EY~s)!L1?#2xD5$5OAWLj!uZhA^ zUwGrbt_#LfAC~-#xB|Y|LgYFh(LIL;M=r@YtnSctM86}gHPU}$UCbBoNE^Np^A4eU zZRYekDTxJ)1fV;3sAX(4O=Ic|ngJz8%@U71lg-sDjkmQ{W9c|!IQXxV_GbU|KgJx3 zZ$X|k8Zoek5}(M_haAO~El(!qD0y3}v)wsmPUCZic6|ZXi{~vIdW_n$5!{8y_N0T| z!XeJ=!RI|b7dN-gV((u$V*gUzA#HhLcW&kfIp5e=*V;*0#x=6O14;sv`?&7#L;eCM z5z$kn=b(|OUhmyZosz_rke9vlwT}fRYKgtIPMY|MwG(2IXQ0Cl^pm@$dt*M##bm8W zWKcG%jB8A9pgESX!S}`@zi@~`874yCL6Vb4Rt^h=c67Y&L#zGJ$s^3V_>XxyitPKh zW}O<>hQIb3(q=)x*3XEM?|mZ zO_8q!PO^H5H(kB%N($^qYu@u`cqj6?VQ&o+_*qjf#f$PjYZudYb=Y*AM|-3db%!N8 zf3tYiqxO!L-g2z2x6$zjIrR^Y85nd-E{_lF9j>~=Y^q-MV0*MD4bv}f*Y$hcOHGT| z<@CI10N<(_Ef%oGuEIUtjD_y4ZpQT+-_TW@syzN8t7Ex@n`_%Ilz1H{xFd>oX5H|V zs^jDQI9Z)a+MHJnuKi*xVxn=5K4~!*^MSgE`o(kDXR!UgK-i`{)Z4-ydV9Z@CvP*u zUp?%gemvhR#G=Wm$4KtpE@{cu%uLi|%Q&T_RN-fxt$gJ{S%`@7)B9#u6h@vk%^mX} zlZfX-6h+DTAn;sC+5rrLcvD&{{{Lke0hcJ^dmbe7M@1_;CUOBYU|?@O_~(8yR4&M5 z{co(w)5NHE%z?)u-E7 zCp=2uC`8kWpCZnjz(G`eS6yQySMd$%z$xw&Y{0fT@s?tR>3X~>fs-={TX=z{aS^FM zm%V=9;&dt7VDk7IrHW62NZ62)CRt^~!^`$qA14Q58Fe~Nz0P82M zzy8DTb7R;rKQ8c)#v@LScYMe+oFzuqQvqTF%uAo*ZR6y>khwVv)TY^Gn>Fr(vqO)p z*?}0ws#7@AqldRH<(Xm#h zxT_ri2b63Bx6qW;Z=qRXhBQ{0lm+o->&i7|>3@386@$A?vOqahjk1abl$kvcDUQNf zwUD#`f$Eptq*&Zn1EQhC8O0DIQ(xYK%{N|$Abf*3(#Z5ZQq@WEmyG&g!rj^l-`OdR z(b8g+nS%fBqSEM z!iLY9^I0mIS`WT$EKT%v`3_K*+G*1>qb6~>k9%!0nP>er4|uvh0lN>!%NKDK#%P*z zp4G^Zd)w7o3qoarun=FiDsG6B&8u69i;;Rwpn%3yfufE zBb3TeEuh}^ET|=AC!rQ)17i0B1X5$_08BJFILJ@`gSybE{-}2*;isP4r(w5X5S*o9 zu1}o15Z~TQ>C^}%G$I%)8r@?kMA+_y^js-n5bq#h91;{=clprPww9{dJ;g(|k6ueK z2O@!5Z0ooD zi<5*#RTGQRf%WTFE9uyR{N3{r5`f1NYCF^ew3~0PROz1<)jPVzQ(9`NR}H7{_XY}D zlqSZ!P=Yq$W)&{zQ%X<YYhf9hMgeL{|+QGPZmg9`{FF`hed6s&;rq$|{r&J+9yez+V3L#H?Qb+`b?!Yf_09H<-Hl`r1^WYJ`ik^=pLfeUBtENd-LcmD_S7Q46r literal 0 HcmV?d00001 diff --git a/apps/webapp/app/components/DevPresence.tsx b/apps/webapp/app/components/DevPresence.tsx index f323dd725d..74d7670a0d 100644 --- a/apps/webapp/app/components/DevPresence.tsx +++ b/apps/webapp/app/components/DevPresence.tsx @@ -5,8 +5,20 @@ import { useEnvironment } from "~/hooks/useEnvironment"; import { useEventSource } from "~/hooks/useEventSource"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "./primitives/Dialog"; -import { Button } from "./primitives/Buttons"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTrigger, +} from "./primitives/Dialog"; +import { Button, LinkButton } from "./primitives/Buttons"; +import connectedImage from "../assets/images/cli-connected.png"; +import disconnectedImage from "../assets/images/cli-disconnected.png"; +import { Paragraph } from "./primitives/Paragraph"; +import { PackageManagerProvider, TriggerDevStepV3 } from "./SetupCommands"; +import { docsPath } from "~/utils/pathBuilder"; +import { BookOpenIcon } from "@heroicons/react/20/solid"; export function useDevPresence() { const organization = useOrganization(); @@ -79,7 +91,37 @@ export function DevPresence() { ? "Your dev server is connected to Trigger.dev" : "Your dev server is not connected to Trigger.dev"} -
+
+
+ {isConnected + + {isConnected + ? "Your local dev server is connected to Trigger.dev" + : "Your local dev server is not connected to Trigger.dev"} + +
+ {isConnected ? null : ( +
+ + + + + Run this CLI `dev` command to connect to the Trigger.dev servers to start developing + locally. Keep it running while you develop to stay connected. + +
+ )} +
+ + + CLI docs + + ); From 07dd634c4764752fa57a0bb5733004455d11aa3c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:13:06 +0000 Subject: [PATCH 85/95] Moved things around and use Context --- apps/webapp/app/components/DevPresence.tsx | 120 ++++++------------ .../app/components/navigation/SideMenu.tsx | 81 +++++++++++- apps/webapp/app/hooks/useEventSource.tsx | 6 +- .../route.tsx | 23 ++-- .../_app.orgs.$organizationSlug/route.tsx | 5 +- 5 files changed, 134 insertions(+), 101 deletions(-) diff --git a/apps/webapp/app/components/DevPresence.tsx b/apps/webapp/app/components/DevPresence.tsx index 74d7670a0d..bd69bbe21e 100644 --- a/apps/webapp/app/components/DevPresence.tsx +++ b/apps/webapp/app/components/DevPresence.tsx @@ -1,33 +1,39 @@ -import { useEffect, useState } from "react"; -import { ConnectedIcon, DisconnectedIcon } from "~/assets/icons/ConnectionIcons"; +import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react"; import { useDebounce } from "~/hooks/useDebounce"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useEventSource } from "~/hooks/useEventSource"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTrigger, -} from "./primitives/Dialog"; -import { Button, LinkButton } from "./primitives/Buttons"; -import connectedImage from "../assets/images/cli-connected.png"; -import disconnectedImage from "../assets/images/cli-disconnected.png"; -import { Paragraph } from "./primitives/Paragraph"; -import { PackageManagerProvider, TriggerDevStepV3 } from "./SetupCommands"; -import { docsPath } from "~/utils/pathBuilder"; -import { BookOpenIcon } from "@heroicons/react/20/solid"; -export function useDevPresence() { +// Define Context types +type DevPresenceContextType = { + lastSeen: Date | null; + isConnected: boolean; +}; + +// Create Context with default values +const DevPresenceContext = createContext({ + lastSeen: null, + isConnected: false, +}); + +// Provider component with enabled prop +interface DevPresenceProviderProps { + children: ReactNode; + enabled?: boolean; +} + +export function DevPresenceProvider({ children, enabled = true }: DevPresenceProviderProps) { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); + + // Only subscribe to event source if enabled is true const streamedEvents = useEventSource( `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/dev/presence`, { event: "presence", + disabled: !enabled, } ); @@ -38,7 +44,8 @@ export function useDevPresence() { }, 3_000); useEffect(() => { - if (streamedEvents === null) { + // If disabled or no events, set lastSeen to null + if (!enabled || streamedEvents === null) { debouncer(null); return; } @@ -46,7 +53,6 @@ export function useDevPresence() { try { const data = JSON.parse(streamedEvents) as any; if ("lastSeen" in data && data.lastSeen) { - // Parse the timestamp string into a Date object try { const lastSeenDate = new Date(data.lastSeen); debouncer(lastSeenDate); @@ -61,68 +67,22 @@ export function useDevPresence() { console.log("DevPresence: Failed to parse presence message", { error }); debouncer(null); } - }, [streamedEvents]); + }, [streamedEvents, enabled]); - return { lastSeen }; -} + // Calculate isConnected and memoize the context value + const contextValue = useMemo(() => { + const isConnected = enabled && lastSeen !== null && lastSeen > new Date(Date.now() - 120_000); + return { lastSeen, isConnected }; + }, [lastSeen, enabled]); -export function DevPresence() { - const { lastSeen } = useDevPresence(); - const isConnected = lastSeen && lastSeen > new Date(Date.now() - 120_000); + return {children}; +} - return ( - - - - ); +// Custom hook to use the context +export function useDevPresence() { + const context = useContext(DevPresenceContext); + if (context === undefined) { + throw new Error("useDevPresence must be used within a DevPresenceProvider"); + } + return context; } diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 9988f28de5..df1eb9f377 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -3,6 +3,7 @@ import { ArrowRightOnRectangleIcon, BeakerIcon, BellAlertIcon, + BookOpenIcon, ChartBarIcon, ChevronRightIcon, ClockIcon, @@ -20,6 +21,7 @@ import { import { useNavigation } from "@remix-run/react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import simplur from "simplur"; +import { ConnectedIcon, DisconnectedIcon } from "~/assets/icons/ConnectionIcons"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { Avatar } from "~/components/primitives/Avatar"; @@ -32,6 +34,7 @@ import { type FeedbackType } from "~/routes/resources.feedback"; import { cn } from "~/utils/cn"; import { accountPath, + docsPath, logoutPath, newOrganizationPath, newProjectPath, @@ -53,9 +56,21 @@ import { v3TestPath, v3UsagePath, } from "~/utils/pathBuilder"; +import { useDevPresence } from "../DevPresence"; import { ImpersonationBanner } from "../ImpersonationBanner"; +import { PackageManagerProvider, TriggerDevStepV3 } from "../SetupCommands"; import { UserProfilePhoto } from "../UserProfilePhoto"; +import connectedImage from "../../assets/images/cli-connected.png"; +import disconnectedImage from "../../assets/images/cli-disconnected.png"; import { FreePlanUsage } from "../billing/FreePlanUsage"; +import { Button, ButtonContent, LinkButton } from "../primitives/Buttons"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTrigger, +} from "../primitives/Dialog"; import { Paragraph } from "../primitives/Paragraph"; import { Popover, @@ -64,14 +79,12 @@ import { PopoverMenuItem, PopoverTrigger, } from "../primitives/Popover"; +import { TextLink } from "../primitives/TextLink"; import { EnvironmentSelector } from "./EnvironmentSelector"; import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; import { SideMenuSection } from "./SideMenuSection"; -import { ButtonContent, LinkButton } from "../primitives/Buttons"; -import { TextLink } from "../primitives/TextLink"; -import { DevPresence } from "../DevPresence"; type SideMenuUser = Pick & { isImpersonating: boolean }; export type SideMenuProject = Pick< @@ -148,7 +161,7 @@ export function SideMenu({ project={project} environment={environment} /> - {environment.type === "DEVELOPMENT" && } + {environment.type === "DEVELOPMENT" && }
@@ -496,3 +509,63 @@ function SelectorDivider() { ); } + +export function DevConnection() { + const { isConnected } = useDevPresence(); + + return ( + + + + ); +} diff --git a/apps/webapp/app/hooks/useEventSource.tsx b/apps/webapp/app/hooks/useEventSource.tsx index 47f695e89f..4bcdac6f52 100644 --- a/apps/webapp/app/hooks/useEventSource.tsx +++ b/apps/webapp/app/hooks/useEventSource.tsx @@ -23,12 +23,12 @@ export function useEventSource( return; } - const eventSource = new EventSource(url, init); - eventSource.addEventListener(event ?? "message", handler); - // reset data if dependencies change setData(null); + const eventSource = new EventSource(url, init); + eventSource.addEventListener(event ?? "message", handler); + function handler(event: MessageEvent) { setData(event.data || "UNKNOWN_EVENT_DATA"); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx index 07311cc5de..b21b53faf3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam/route.tsx @@ -1,4 +1,5 @@ import { Outlet } from "@remix-run/react"; +import { DevPresenceProvider } from "~/components/DevPresence"; import { RouteErrorDisplay } from "~/components/ErrorDisplay"; import { MainBody } from "~/components/layout/AppLayout"; import { SideMenu } from "~/components/navigation/SideMenu"; @@ -29,16 +30,18 @@ export default function Project() { return ( <>
- - - - + + + + + +
); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx index 8c15309cd9..7700b7e20d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx @@ -1,13 +1,10 @@ import { Outlet, type ShouldRevalidateFunction, type UIMatch } from "@remix-run/react"; import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { typedjson } from "remix-typedjson"; import { z } from "zod"; import { RouteErrorDisplay } from "~/components/ErrorDisplay"; -import { MainBody } from "~/components/layout/AppLayout"; -import { SideMenu } from "~/components/navigation/SideMenu"; import { useOptionalOrganization } from "~/hooks/useOrganizations"; import { useTypedMatchesData } from "~/hooks/useTypedMatchData"; -import { useUser } from "~/hooks/useUser"; import { OrganizationsPresenter } from "~/presenters/OrganizationsPresenter.server"; import { getImpersonationId } from "~/services/impersonation.server"; import { getCachedUsage, getCurrentPlan } from "~/services/platform.v3.server"; From 70a5013dfa30764a857989703d278d6bdd078924 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:25:38 +0000 Subject: [PATCH 86/95] Fix for p inside p --- .../route.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index d4790dc2d3..b651bc5866 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -343,10 +343,8 @@ function CancelRuns({ onOpen }: { onOpen: (open: boolean) => void }) { Cancel {selectedItems.size} runs? - - Canceling these runs will stop them from running. Only runs that are not already - finished will be canceled, the others will remain in their existing state. - + Canceling these runs will stop them from running. Only runs that are not already finished + will be canceled, the others will remain in their existing state. @@ -402,10 +400,8 @@ function ReplayRuns({ onOpen }: { onOpen: (open: boolean) => void }) { Replay runs? - - Replaying these runs will create a new run for each with the same payload and - environment as the original. It will use the latest version of the code for each task. - + Replaying these runs will create a new run for each with the same payload and environment + as the original. It will use the latest version of the code for each task. From 11987608abfbd9149dff6e95a7c806f417cfee5e Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:25:50 +0000 Subject: [PATCH 87/95] Dev connected status on run page --- .../route.tsx | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 551840997e..1b633f9f1a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -93,6 +93,8 @@ import { } from "~/utils/pathBuilder"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route"; +import { useDevPresence } from "~/components/DevPresence"; +import { DisconnectedIcon } from "~/assets/icons/ConnectionIcons"; const resizableSettings = { parent: { @@ -477,6 +479,8 @@ function TasksTreeView({ const treeScrollRef = useRef(null); const timelineScrollRef = useRef(null); + const displayEvents = showDebug ? events : events.filter((event) => !event.data.isDebug); + const { nodes, getTreeProps, @@ -490,7 +494,7 @@ function TasksTreeView({ scrollToNode, virtualizer, } = useTree({ - tree: showDebug ? events : events.filter((event) => !event.data.isDebug), + tree: displayEvents, selectedId, // collapsedIds, onSelectedIdChanged, @@ -636,7 +640,7 @@ function TasksTreeView({
- {events.length === 1 && environmentType === "DEVELOPMENT" && ( + {displayEvents.length === 1 && environmentType === "DEVELOPMENT" && ( )} @@ -1241,29 +1245,22 @@ function CurrentTimeIndicator({ } function ConnectedDevWarning() { - const [isVisible, setIsVisible] = useState(false); - - useEffect(() => { - const timer = setTimeout(() => { - setIsVisible(true); - }, 3000); - - return () => clearTimeout(timer); - }, []); + const { isConnected } = useDevPresence(); return (
- + } className="mt-2">
+ + Your local dev server is not connected to Trigger.dev. + + Check you're running the CLI: - Runs usually start within 1 second in{" "} - . - Check you're running the CLI:{" "} npx trigger.dev@latest dev
From 5f29652040860633054f54514c5c479ff8ed5d74 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:35:23 +0000 Subject: [PATCH 88/95] Correct dev env (not a teammates) --- ...jects.$projectParam.env.$envParam.dev.presence.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx index 4d0188d092..6bbe9712b9 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx @@ -19,17 +19,12 @@ export const loader = createSSELoader({ where: { slug: envParam, type: "DEVELOPMENT", + orgMember: { + userId, + }, project: { slug: projectParam, }, - organization: { - slug: organizationSlug, - members: { - some: { - userId, - }, - }, - }, }, }); From 4b4cd5f51d985474d175d9df11c7870267f0f76d Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:43:59 +0000 Subject: [PATCH 89/95] Show disconnected message at the end --- .../route.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 1b633f9f1a..527f03e7ca 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -327,6 +327,7 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade shouldLiveReload={shouldLiveReload} maximumLiveReloadingSetting={maximumLiveReloadingSetting} rootRun={run.rootTaskRun} + isCompleted={run.completedAt !== null} /> @@ -454,6 +455,7 @@ type TasksTreeViewProps = { taskIdentifier: string; spanId: string; } | null; + isCompleted: boolean; }; function TasksTreeView({ @@ -468,6 +470,7 @@ function TasksTreeView({ shouldLiveReload, maximumLiveReloadingSetting, rootRun, + isCompleted, }: TasksTreeViewProps) { const isAdmin = useHasAdminAccess(); const [filterText, setFilterText] = useState(""); @@ -573,7 +576,7 @@ function TasksTreeView({ nodes={nodes} getNodeProps={getNodeProps} getTreeProps={getTreeProps} - renderNode={({ node, state }) => ( + renderNode={({ node, state, index }) => ( <>
- {displayEvents.length === 1 && environmentType === "DEVELOPMENT" && ( - - )} + {!isCompleted && + environmentType === "DEVELOPMENT" && + index === displayEvents.length - 1 && } )} onScroll={(scrollTop) => { From d742ba198d5be1896c202fc780af07d281220b3c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:48:15 +0000 Subject: [PATCH 90/95] Minor tweak on project dropdown icon padding --- apps/webapp/app/components/navigation/SideMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index df1eb9f377..2cb002cf45 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -296,7 +296,7 @@ function ProjectSelector({ From 53759491c81cd230a0189807536b107c9237d39c Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Sun, 16 Mar 2025 22:54:23 +0000 Subject: [PATCH 91/95] Fix for inconsistent date format for presence --- .../app/routes/engine.v1.dev.presence.ts | 2 +- ...rojectParam.env.$envParam.dev.presence.tsx | 23 +++++++------------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/routes/engine.v1.dev.presence.ts b/apps/webapp/app/routes/engine.v1.dev.presence.ts index 665359666b..8ddfdf2575 100644 --- a/apps/webapp/app/routes/engine.v1.dev.presence.ts +++ b/apps/webapp/app/routes/engine.v1.dev.presence.ts @@ -41,7 +41,7 @@ export const loader = createSSELoader({ }, initStream: async ({ send }) => { // Set initial presence with more context - await redis.setex(presenceKey, env.DEV_PRESENCE_TTL_MS / 1000, Date.now().toString()); + await redis.setex(presenceKey, env.DEV_PRESENCE_TTL_MS / 1000, new Date().toISOString()); // Publish presence update await redis.publish( diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx index 6bbe9712b9..33ec558d2d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev.presence.tsx @@ -60,21 +60,14 @@ export const loader = createSSELoader({ // Format lastSeen as ISO string if it exists let lastSeen = null; if (currentPresenceValue) { - // Check if it's a numeric timestamp - if (!isNaN(Number(currentPresenceValue))) { - // Convert numeric timestamp to ISO string - lastSeen = new Date(parseInt(currentPresenceValue, 10)).toISOString(); - } else { - // It's already a string format, make sure it's ISO - try { - lastSeen = new Date(currentPresenceValue).toISOString(); - } catch (e) { - // If parsing fails, use current time as fallback - lastSeen = new Date().toISOString(); - logger.warn("Failed to parse lastSeen value, using current time", { - originalValue: currentPresenceValue, - }); - } + try { + lastSeen = new Date(currentPresenceValue).toISOString(); + } catch (e) { + // If parsing fails, use current time as fallback + lastSeen = new Date().toISOString(); + logger.warn("Failed to parse lastSeen value, using current time", { + originalValue: currentPresenceValue, + }); } } From 4f44b00c3f982876217d8091fa9ec5fe5b132217 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 17 Mar 2025 09:29:03 +0000 Subject: [PATCH 92/95] Added a message when pushing to the billing page --- .../route.tsx | 5 ++++- .../route.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx index 3bf162c3ef..7d83711f46 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx @@ -138,7 +138,10 @@ export default function Page() { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx index 78b757dfd5..5635b46c17 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx @@ -134,7 +134,7 @@ export default function Page() { Upgrade for more concurrency From 96ff617bb450810dbd3e6a506a1ffc6dd73e4f63 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 17 Mar 2025 09:32:04 +0000 Subject: [PATCH 93/95] Center the team page --- .../route.tsx | 168 ++++++++++-------- 1 file changed, 89 insertions(+), 79 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx index 0027aa8308..5cb99ddbab 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx @@ -1,15 +1,19 @@ import { useForm } from "@conform-to/react"; import { parse } from "@conform-to/zod"; import { EnvelopeIcon, LockOpenIcon, TrashIcon, UserPlusIcon } from "@heroicons/react/20/solid"; -import { Form, MetaFunction, useActionData } from "@remix-run/react"; -import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { Form, type MetaFunction, useActionData } from "@remix-run/react"; +import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useState } from "react"; -import { UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson"; +import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson"; import invariant from "tiny-invariant"; import { z } from "zod"; import { UserAvatar } from "~/components/UserProfilePhoto"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { + MainHorizontallyCenteredContainer, + PageBody, + PageContainer, +} from "~/components/layout/AppLayout"; import { Alert, AlertCancel, @@ -158,82 +162,88 @@ export default function Page() { - - Members ({limits.used}/{limits.limit}) - -
    - {members.map((member) => ( -
  • - -
    - - {member.user.name}{" "} - {member.user.id === user.id && (You)} - - {member.user.email} -
    -
    - -
    -
  • - ))} -
- - {invites.length > 0 && ( - <> - Pending invites -
    - {invites.map((invite) => ( -
  • -
    - -
    -
    - {invite.email} - - Invite sent {} - -
    -
    - - -
    -
  • - ))} -
- - )} - - {requiresUpgrade ? ( - - - You've used all {limits.limit} of your available team members. Upgrade your plan to - enable more. - - - ) : ( -
- + + Members ({limits.used}/{limits.limit}) + +
    + {members.map((member) => ( +
  • + +
    + + {member.user.name}{" "} + {member.user.id === user.id && (You)} + + {member.user.email} +
    +
    + +
    +
  • + ))} +
+ + {invites.length > 0 && ( + <> + Pending invites +
    + {invites.map((invite) => ( +
  • +
    + +
    +
    + {invite.email} + + Invite sent {} + +
    +
    + + +
    +
  • + ))} +
+ + )} + + {requiresUpgrade ? ( + - Invite a team member -
-
- )} + + You've used all {limits.limit} of your available team members. Upgrade your plan to + enable more. + + + ) : ( +
+ + Invite a team member + +
+ )} +
); From bf7b1f12b50fea5df5b41a7dbd1cdfd6482ea52b Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 17 Mar 2025 09:33:01 +0000 Subject: [PATCH 94/95] Project settings page centered --- .../route.tsx | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx index a5be22a93d..2a57372dba 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx @@ -6,7 +6,11 @@ import { type ActionFunction, json } from "@remix-run/server-runtime"; import { z } from "zod"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { InlineCode } from "~/components/code/InlineCode"; -import { PageBody, PageContainer } from "~/components/layout/AppLayout"; +import { + MainHorizontallyCenteredContainer, + PageBody, + PageContainer, +} from "~/components/layout/AppLayout"; import { Button } from "~/components/primitives/Buttons"; import { ClipboardField } from "~/components/primitives/ClipboardField"; import { Fieldset } from "~/components/primitives/Fieldset"; @@ -144,49 +148,51 @@ export default function Page() { -
+
-
- - - - - This goes in your{" "} - trigger.config file. - - -
- - - +
- - - {projectName.error} + + + + This goes in your{" "} + trigger.config file. + - - Rename project - - } - />
- + +
+ +
+ + + + {projectName.error} + + + Rename project + + } + /> +
+ +
-
+
); From 82989c882abf7baa6e55c02712cbd5e7c0d6469a Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 17 Mar 2025 13:08:11 +0000 Subject: [PATCH 95/95] Improvements to the dev presence --- .../app/components/navigation/SideMenu.tsx | 46 +++++++++++++------ .../app/components/primitives/Tooltip.tsx | 4 +- .../route.tsx | 16 ++++--- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 2cb002cf45..df6431f202 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -85,6 +85,13 @@ import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; import { SideMenuSection } from "./SideMenuSection"; +import { + SimpleTooltip, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "../primitives/Tooltip"; type SideMenuUser = Pick & { isImpersonating: boolean }; export type SideMenuProject = Pick< @@ -515,19 +522,32 @@ export function DevConnection() { return ( - -
+ + + {isConnected ? "Your dev server is connected" : "Your dev server is not connected"} + + + +
{isConnected diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx index 4ec04d0132..80b1427cad 100644 --- a/apps/webapp/app/components/primitives/Tooltip.tsx +++ b/apps/webapp/app/components/primitives/Tooltip.tsx @@ -59,6 +59,7 @@ function SimpleTooltip({ disableHoverableContent = false, className, buttonClassName, + asChild = false, }: { button: React.ReactNode; content: React.ReactNode; @@ -68,11 +69,12 @@ function SimpleTooltip({ disableHoverableContent?: boolean; className?: string; buttonClassName?: string; + asChild?: boolean; }) { return ( - + {button} - } className="mt-2"> + } + className="mt-2" + >
- - Your local dev server is not connected to Trigger.dev. - - Check you're running the CLI: - - npx trigger.dev@latest dev + + Your local dev server is not connectedr. Check you're running the CLI: +