From 93d99c299b35c929e07dcfe7045acf275c065a7b Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 4 Jul 2025 05:20:11 +0000 Subject: [PATCH] chore: a mess of merging everything together --- Cargo.lock | 2 +- cloud/packages/ci-manager/src/build-store.ts | 10 +- cloud/packages/ci-manager/src/common.ts | 10 +- .../ci-manager/src/executors/docker.ts | 38 +- .../ci-manager/src/executors/rivet.ts | 8 +- .../packages/ci-manager/src/kaniko-runner.ts | 8 +- .../packages/ci-manager/src/oci-converter.ts | 59 +- .../packages/ci-manager/src/rivet-uploader.ts | 7 +- cloud/packages/ci-manager/src/server.ts | 38 +- cloud/packages/ci-manager/src/types.ts | 170 +- .../ci-manager/tests/docker-e2e.test.ts | 20 +- .../ci-manager/tests/oci-converter.test.ts | 122 +- .../ci-manager/tests/rivet-e2e.test.ts | 12 +- cloud/packages/ci-manager/tests/test-utils.ts | 17 +- .../ci-manager/tests/upload-workflow.test.ts | 12 +- cloud/packages/ci-manager/tsconfig.json | 75 +- docker/dev-full/docker-compose.yml | 4 +- docker/dev-full/frontend-hub/Dockerfile | 12 + docker/dev-full/frontend-hub/entrypoint.sh | 3 - docker/dev-full/vector-server/vector.yaml | 10 +- docker/monolith/vector-server/vector.yaml | 10 +- examples/functions-rust/rivet.json | 12 +- examples/linear-agent-starter/package.json | 68 +- .../linear-agent-starter/src/actors/app.ts | 6 +- .../src/actors/issue-agent.ts | 8 +- .../linear-agent-starter/src/server/index.ts | 10 +- examples/linear-agent-starter/tsconfig.json | 25 +- examples/multitenant-deploys/package.json | 40 +- examples/multitenant-deploys/src/app.ts | 6 +- examples/multitenant-deploys/src/index.ts | 1 - .../multitenant-deploys/tests/deploy.test.ts | 6 +- examples/multitenant-deploys/tsconfig.json | 22 +- .../system-test-actor/src/container/main.ts | 28 +- examples/system-test-actor/tests/client.ts | 24 +- frontend/apps/hub/ARCHITECTURE.md | 153 + .../environment-command-panel-page.tsx | 1 - .../hub/src/components/error-component.tsx | 2 +- .../src/components/header/header-sub-nav.tsx | 6 +- .../apps/hub/src/components/header/header.tsx | 2 +- .../domains/auth/views/login-view/hooks.ts | 1 - .../actors/actors-actor-details-wrapper.tsx | 52 + .../components/actors/actors-provider.tsx | 65 +- .../components/billing/billing-context.tsx | 2 +- .../billing/billing-portal-button.tsx | 2 +- .../components/dialogs/edit-route-dialog.tsx | 2 +- .../project/components/tags-select.tsx | 2 +- .../project/data/environment-context.tsx | 4 +- .../domains/project/data/project-context.tsx | 8 +- .../domains/project/forms/route-edit-form.tsx | 10 +- .../project/queries/actors/mutations.ts | 2 +- .../project/queries/actors/query-options.ts | 136 +- .../project/queries/billing/mutations.ts | 2 +- .../project/queries/billing/query-options.ts | 3 +- .../queries/environment/query-options.ts | 6 +- .../apps/hub/src/domains/user/queries/type.ts | 2 +- frontend/apps/hub/src/hooks/use-dialog.tsx | 2 +- frontend/apps/hub/src/layouts/root.tsx | 1 - frontend/apps/hub/src/lib/guards.tsx | 1 - frontend/apps/hub/src/lib/utils.ts | 2 +- frontend/apps/hub/src/main.tsx | 2 +- frontend/apps/hub/src/queries/global.ts | 2 +- frontend/apps/hub/src/queries/watch.ts | 2 +- .../apps/hub/src/routes/_authenticated.tsx | 4 +- .../projects/$projectNameId/billing.tsx | 2 +- .../$environmentNameId._v2/actor-versions.tsx | 2 +- .../$environmentNameId._v2/actors.tsx | 24 +- .../$environmentNameId._v2/containers.tsx | 30 +- .../$environmentNameId._v2/functions.tsx | 44 +- .../$environmentNameId._v2/logs.tsx | 78 +- .../environments/$environmentNameId/_v2.tsx | 9 +- .../$environmentNameId/tokens.tsx | 6 +- frontend/apps/studio/src/app.tsx | 12 +- .../apps/studio/src/components/layout.tsx | 6 +- frontend/apps/studio/src/queries/global.ts | 9 +- frontend/apps/studio/src/routes/__root.tsx | 6 +- frontend/apps/studio/src/routes/_layout.tsx | 2 +- .../apps/studio/src/routes/_layout/index.tsx | 11 +- frontend/apps/studio/src/stores/manager.tsx | 18 +- frontend/apps/studio/vite.config.ts | 4 +- frontend/packages/cli/cli.ts | 12 +- frontend/packages/cli/package.json | 6 +- frontend/packages/cli/postinstall.ts | 6 +- frontend/packages/components/package.json | 6 +- .../components/src/actors/actor-build.tsx | 8 +- .../src/actors/actor-config-tab.tsx | 2 +- .../src/actors/actor-connections-tab.tsx | 6 +- .../components/src/actors/actor-context.tsx | 15 +- .../components/src/actors/actor-cpu-stats.tsx | 63 +- .../src/actors/actor-download-logs-button.tsx | 87 +- .../components/src/actors/actor-general.tsx | 8 +- .../components/src/actors/actor-logs-tab.tsx | 16 +- .../components/src/actors/actor-logs.tsx | 6 +- .../src/actors/actor-memory-stats.tsx | 33 +- .../src/actors/actor-metrics-tab.tsx | 4 +- .../components/src/actors/actor-metrics.tsx | 150 +- .../components/src/actors/actor-network.tsx | 8 +- .../components/src/actors/actor-not-found.tsx | 8 +- .../components/src/actors/actor-region.tsx | 10 +- .../components/src/actors/actor-runtime.tsx | 24 +- .../components/src/actors/actor-state-tab.tsx | 6 +- .../src/actors/actor-status-indicator.tsx | 2 +- .../src/actors/actor-status-label.tsx | 2 +- .../components/src/actors/actor-status.tsx | 2 +- .../src/actors/actor-stop-button.tsx | 4 +- .../src/actors/actor-tags-select.tsx | 4 +- .../src/actors/actors-actor-details.tsx | 48 +- .../src/actors/actors-actor-not-found.tsx | 2 +- .../components/src/actors/actors-layout.tsx | 2 +- .../src/actors/actors-list-preview.tsx | 2 +- .../components/src/actors/actors-list-row.tsx | 14 +- .../components/src/actors/actors-list.tsx | 45 +- .../components/src/actors/build-select.tsx | 2 +- .../src/actors/console/actor-console-logs.tsx | 2 +- .../actors/console/actor-console-message.tsx | 1 - .../src/actors/create-actor-button.tsx | 2 +- .../actors/dialogs/create-actor-dialog.tsx | 6 +- .../src/actors/dialogs/go-to-actor-dialog.tsx | 6 +- .../src/actors/form/actor-create-form.tsx | 10 +- .../src/actors/form/build-tags-form.tsx | 4 +- .../components/src/actors/get-started.tsx | 4 +- .../components/src/actors/getting-started.tsx | 4 +- .../src/actors/worker/actor-repl.worker.ts | 10 +- .../actors/worker/actor-worker-container.ts | 6 +- .../actors/worker/actor-worker-context.tsx | 10 +- .../packages/components/src/copy-area.tsx | 2 +- .../src/dialogs/feedback-dialog.tsx | 6 +- .../packages/components/src/docs-sheet.tsx | 12 +- .../components/src/forms/feedback-form.tsx | 6 +- .../components/src/mdx/code-buttons.tsx | 2 +- .../packages/components/src/ui/dialog.tsx | 1 - .../packages/components/src/ui/filters.tsx | 54 +- .../packages/components/src/vite/index.mjs | 8 +- frontend/packages/icons/manifest.json | 9087 ++++++++++++++++- frontend/packages/icons/package.json | 7 +- justfile | 2 +- .../examples/case_sensitivity_demo.rs | 26 +- .../examples/group_by_example.rs | 9 +- .../examples/string_contains_demo.rs | 96 + .../clickhouse-user-query/src/builder.rs | 87 +- .../common/clickhouse-user-query/src/lib.rs | 4 +- .../common/clickhouse-user-query/src/query.rs | 38 +- .../tests/builder_tests.rs | 138 +- .../tests/case_sensitivity_tests.rs | 119 +- .../tests/integration_tests.rs | 35 +- .../tests/query_tests.rs | 12 +- .../errors/actor/logs/invalid-actor-ids.md | 9 - .../errors/actor/logs/invalid-query-expr.md | 9 - .../errors/actor/logs/no-actor-ids.md | 9 - .../errors/actor/logs/no-valid-actor-ids.md | 9 - packages/core/api/actor/src/route/actors.rs | 15 +- packages/core/api/actor/src/route/logs.rs | 166 +- packages/core/api/actor/src/route/metrics.rs | 98 +- .../services/guard/src/ops/routes_history.rs | 4 +- packages/core/services/guard/src/schema.rs | 3 - .../services/upload/ops/prepare/src/lib.rs | 17 +- .../core/services/upload/proto/prepare.proto | 9 + .../infra/client/container-runner/src/main.rs | 1 + .../client/isolate-v8-runner/src/isolate.rs | 2 + .../isolate-v8-runner/src/log_shipper.rs | 6 +- .../infra/client/manager/src/actor/mod.rs | 2 +- ...0703195340_fix_actor_logs3_metadata.up.sql | 8 +- .../migrations/20200101000000_init.up.sql | 10 +- .../src/ops/actor/log/read_with_query.rs | 17 +- .../services/pegboard/src/ops/actor/query.rs | 24 +- .../pegboard/src/ops/actor/usage/get.rs | 4 +- .../src/ops/actor/usage/get_aggregated.rs | 4 +- packages/toolchain/cli/src/util/deploy.rs | 9 +- .../js/src/tasks/build/build.ts | 7 - .../js/src/tasks/build/preset.ts | 95 - .../toolchain/src/util/actor/logs.rs | 2 +- .../fern/definition/actors/__package__.yml | 2 +- sdks/api/fern/definition/actors/logs.yml | 4 +- sdks/api/full/go/actors/client/client.go | 4 +- sdks/api/full/go/actors/logs.go | 4 +- sdks/api/full/go/actors/logs/client.go | 4 +- sdks/api/full/go/actors/types.go | 2 +- sdks/api/full/openapi/openapi.yml | 6 +- sdks/api/full/openapi_compat/openapi.yml | 6 +- sdks/api/full/rust/docs/ActorsApi.md | 4 +- sdks/api/full/rust/docs/ActorsLogsApi.md | 4 +- .../full/rust/docs/ActorsLogsExportRequest.md | 2 +- sdks/api/full/rust/src/apis/actors_api.rs | 7 +- .../api/full/rust/src/apis/actors_logs_api.rs | 7 +- .../src/models/actors_logs_export_request.rs | 8 +- .../src/api/resources/actors/client/Client.ts | 7 +- .../requests/QueryActorsRequestQuery.ts | 2 +- .../actors/resources/logs/client/Client.ts | 9 +- .../client/requests/ExportActorLogsRequest.ts | 2 +- .../requests/GetActorLogsRequestQuery.ts | 2 +- .../client/requests/ExportActorLogsRequest.ts | 4 +- sdks/api/runtime/go/actors/client/client.go | 4 +- sdks/api/runtime/go/actors/logs.go | 4 +- sdks/api/runtime/go/actors/logs/client.go | 4 +- sdks/api/runtime/go/actors/types.go | 2 +- sdks/api/runtime/openapi/openapi.yml | 6 +- sdks/api/runtime/openapi_compat/openapi.yml | 6 +- sdks/api/runtime/rust/docs/ActorsApi.md | 4 +- sdks/api/runtime/rust/docs/ActorsLogsApi.md | 4 +- .../rust/docs/ActorsLogsExportRequest.md | 2 +- sdks/api/runtime/rust/src/apis/actors_api.rs | 6 +- .../runtime/rust/src/apis/actors_logs_api.rs | 6 +- .../src/models/actors_logs_export_request.rs | 8 +- .../src/api/resources/actors/client/Client.ts | 7 +- .../requests/QueryActorsRequestQuery.ts | 2 +- .../actors/resources/logs/client/Client.ts | 9 +- .../client/requests/ExportActorLogsRequest.ts | 2 +- .../requests/GetActorLogsRequestQuery.ts | 2 +- .../client/requests/ExportActorLogsRequest.ts | 4 +- shell.nix | 1 + site/next.config.mjs | 69 +- site/package.json | 188 +- site/public/giscus.css | 204 +- .../app/(v2)/(blog)/blog/[...slug]/page.tsx | 3 +- .../(index)/components/CopyCommand.tsx | 5 +- .../(index)/components/HeroBackground.tsx | 65 +- .../(marketing)/(index)/components/Icon.tsx | 166 +- .../(index)/components/LibrariesGrid.tsx | 110 +- .../(index)/components/MarketingButton.tsx | 46 +- .../(index)/components/PlatformIcons.tsx | 271 +- .../src/app/(v2)/(marketing)/(index)/page.tsx | 8 +- .../(index)/sections/CTASection.tsx | 6 +- .../(index)/sections/CodeSnippetsSection.tsx | 303 +- .../(index)/sections/CommunitySection.tsx | 16 +- .../(index)/sections/FeaturesSection.tsx | 22 +- .../(index)/sections/HeroSection.tsx | 39 +- .../(index)/sections/QuotesSection.tsx | 309 +- .../(index)/sections/StudioSection.tsx | 62 +- .../(index)/sections/TechSection.tsx | 81 +- .../cloud/CommandCenterSection.tsx | 29 +- .../(marketing)/cloud/CommunitySection.tsx | 13 +- .../(v2)/(marketing)/cloud/CopyCommand.tsx | 66 +- .../app/(v2)/(marketing)/cloud/CtaButtons.tsx | 32 +- .../app/(v2)/(marketing)/cloud/CtaSection.tsx | 31 +- .../(v2)/(marketing)/cloud/FeaturesGrid.tsx | 15 +- .../(marketing)/cloud/FrameworksSection.tsx | 24 +- .../(marketing)/cloud/MarketingButton.tsx | 40 +- .../(marketing)/cloud/PerformanceSection.tsx | 130 +- .../cloud/PowerfulPrimitivesSection.tsx | 60 +- .../(marketing)/cloud/RivetCloudSection.tsx | 6 +- .../cloud/ServerlessLimitationsSection.tsx | 41 +- .../(marketing)/cloud/TutorialsSection.tsx | 224 +- .../(marketing)/cloud/components/Feature.tsx | 4 +- site/src/app/(v2)/(marketing)/cloud/page.tsx | 32 +- .../(marketing)/pricing/PricingPageClient.tsx | 369 +- .../pricing/components/MobilePricingTabs.tsx | 270 +- .../pricing/components/UsagePricingModal.tsx | 262 +- .../src/app/(v2)/(marketing)/pricing/page.tsx | 3 +- .../rivet-vs-cloudflare-workers/page.tsx | 152 +- site/src/app/(v2)/(marketing)/sales/page.tsx | 5 +- .../src/app/(v2)/(marketing)/support/page.tsx | 4 +- .../app/(v2)/(other)/meme/wired-in/page.jsx | 25 +- .../app/(v2)/[section]/[[...page]]/page.tsx | 2 +- site/src/app/(v2)/oss-friends/page.tsx | 12 +- site/src/app/layout.tsx | 5 +- .../src/components/CollapsibleSidebarItem.tsx | 4 +- site/src/components/Footer.jsx | 10 +- site/src/components/GitHubStars.tsx | 5 +- site/src/components/GitHubStarsDropdown.tsx | 39 +- site/src/components/Heading.jsx | 119 +- site/src/components/PricingCalculator.tsx | 516 +- site/src/components/mdx.jsx | 46 +- site/src/components/v2/FancyHeader.tsx | 51 +- site/src/components/v2/GitHubDropdown.tsx | 98 +- site/src/components/v2/Header.tsx | 3 +- .../docs/cloud/api/actors/logs/export.mdx | 2 +- .../docs/cloud/api/actors/logs/get.mdx | 2 +- .../content/docs/cloud/api/actors/query.mdx | 2 +- site/src/content/docs/cloud/api/errors.mdx | 28 - site/src/sitemap/mod.ts | 57 +- site/src/styles/v2.css | 88 +- site/tailwind.v2.config.js | 106 +- site/typography.js | 622 +- tests/load/actor-lifecycle/actor.ts | 10 +- tests/load/actor-lifecycle/index.ts | 2 +- 274 files changed, 14320 insertions(+), 4105 deletions(-) create mode 100644 docker/dev-full/frontend-hub/Dockerfile create mode 100644 frontend/apps/hub/ARCHITECTURE.md create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actors-actor-details-wrapper.tsx create mode 100644 packages/common/clickhouse-user-query/examples/string_contains_demo.rs delete mode 100644 packages/common/formatted-error/errors/actor/logs/invalid-actor-ids.md delete mode 100644 packages/common/formatted-error/errors/actor/logs/invalid-query-expr.md delete mode 100644 packages/common/formatted-error/errors/actor/logs/no-actor-ids.md delete mode 100644 packages/common/formatted-error/errors/actor/logs/no-valid-actor-ids.md delete mode 100644 packages/toolchain/js-utils-embed/js/src/tasks/build/preset.ts diff --git a/Cargo.lock b/Cargo.lock index 98c0e80bd6..799e73dc4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3400,7 +3400,7 @@ dependencies = [ [[package]] name = "clickhouse-user-query" -version = "25.5.1" +version = "25.5.2" dependencies = [ "clickhouse", "serde", diff --git a/cloud/packages/ci-manager/src/build-store.ts b/cloud/packages/ci-manager/src/build-store.ts index ec97718dfe..d5715ef3bb 100644 --- a/cloud/packages/ci-manager/src/build-store.ts +++ b/cloud/packages/ci-manager/src/build-store.ts @@ -1,8 +1,8 @@ -import { BuildInfo, BuildEvent, Status } from "./types"; import { randomUUID } from "crypto"; +import { dirname, join } from "path"; import { mkdir, rm } from "fs/promises"; -import { join, dirname } from "path"; import { createNanoEvents } from "nanoevents"; +import type { BuildEvent, BuildInfo, Status } from "./types"; export class BuildStore { private builds = new Map(); @@ -12,7 +12,7 @@ export class BuildStore { "status-change": (buildId: string, status: Status) => void; }>(); - constructor(tempDir: string = "/tmp/ci-builds") { + constructor(tempDir = "/tmp/ci-builds") { this.tempDir = tempDir; } @@ -25,7 +25,7 @@ export class BuildStore { dockerfilePath: string, environmentId: string, buildArgs: Record, - buildTarget: string | undefined + buildTarget: string | undefined, ): string { const id = randomUUID(); const contextPath = join(this.tempDir, id, "context.tar.gz"); @@ -94,7 +94,7 @@ export class BuildStore { } } - getContextPath(id: string): string | undefined{ + getContextPath(id: string): string | undefined { return this.builds.get(id)?.contextPath; } diff --git a/cloud/packages/ci-manager/src/common.ts b/cloud/packages/ci-manager/src/common.ts index a86bff5265..56494820bc 100644 --- a/cloud/packages/ci-manager/src/common.ts +++ b/cloud/packages/ci-manager/src/common.ts @@ -12,9 +12,7 @@ interface KanikoArguments { } // SAFETY: buildArgs keys never have equal signs or spaces -function convertBuildArgsToArgs( - buildArgs: Record, -): string[] { +function convertBuildArgsToArgs(buildArgs: Record): string[] { return Object.entries(buildArgs).flatMap(([key, value]) => [ `--build-arg`, `${key}=${value}`, @@ -34,11 +32,11 @@ export function serializeKanikoArguments(args: KanikoArguments): string { "--no-push", "--single-snapshot", "--verbosity=info", - ].map(arg => { + ].map((arg) => { // Args should never contain UNIT_SEP_CHAR, but we can // escape it if they do. - return arg.replaceAll(UNIT_SEP_CHAR, "\\" + UNIT_SEP_CHAR) + return arg.replaceAll(UNIT_SEP_CHAR, "\\" + UNIT_SEP_CHAR); }); return preparedArgs.join(UNIT_SEP_CHAR); -} \ No newline at end of file +} diff --git a/cloud/packages/ci-manager/src/executors/docker.ts b/cloud/packages/ci-manager/src/executors/docker.ts index 670ff49190..11b37f800b 100644 --- a/cloud/packages/ci-manager/src/executors/docker.ts +++ b/cloud/packages/ci-manager/src/executors/docker.ts @@ -1,6 +1,6 @@ import { spawn } from "node:child_process"; -import { BuildStore } from "../build-store"; -import { serializeKanikoArguments, UNIT_SEP_CHAR } from "../common"; +import type { BuildStore } from "../build-store"; +import { serializeKanikoArguments } from "../common"; export async function runDockerBuild( buildStore: BuildStore, @@ -20,16 +20,14 @@ export async function runDockerBuild( "--rm", "--network=host", "-e", - `KANIKO_ARGS=${ - serializeKanikoArguments({ - contextUrl, - outputUrl, - destination: `${buildId}:latest`, - dockerfilePath: build.dockerfilePath, - buildArgs: build.buildArgs, - buildTarget: build.buildTarget, - }) - }`, + `KANIKO_ARGS=${serializeKanikoArguments({ + contextUrl, + outputUrl, + destination: `${buildId}:latest`, + dockerfilePath: build.dockerfilePath, + buildArgs: build.buildArgs, + buildTarget: build.buildTarget, + })}`, "ci-runner", ]; @@ -40,12 +38,12 @@ export async function runDockerBuild( buildStore.updateStatus(buildId, { type: "running", - data: { docker: {} } + data: { docker: {} }, }); return new Promise((resolve, reject) => { const dockerProcess = spawn("docker", kanikoArgs, { - stdio: ["pipe", "pipe", "pipe"] + stdio: ["pipe", "pipe", "pipe"], }); buildStore.setContainerProcess(buildId, dockerProcess); @@ -71,7 +69,10 @@ export async function runDockerBuild( }); dockerProcess.on("close", (code) => { - buildStore.addLog(buildId, `Docker process closed with exit code: ${code}`); + buildStore.addLog( + buildId, + `Docker process closed with exit code: ${code}`, + ); buildStore.updateStatus(buildId, { type: "finishing", data: {} }); if (code === 0) { @@ -90,7 +91,10 @@ export async function runDockerBuild( }); dockerProcess.on("error", (error) => { - buildStore.addLog(buildId, `Docker process error: ${error.message}`); + buildStore.addLog( + buildId, + `Docker process error: ${error.message}`, + ); buildStore.updateStatus(buildId, { type: "failure", data: { reason: `Failed to start kaniko: ${error.message}` }, @@ -99,5 +103,3 @@ export async function runDockerBuild( }); }); } - - diff --git a/cloud/packages/ci-manager/src/executors/rivet.ts b/cloud/packages/ci-manager/src/executors/rivet.ts index 5e8d5c7655..7cf54b8812 100644 --- a/cloud/packages/ci-manager/src/executors/rivet.ts +++ b/cloud/packages/ci-manager/src/executors/rivet.ts @@ -1,5 +1,5 @@ import { RivetClient } from "@rivet-gg/api"; -import { BuildStore } from "../build-store"; +import type { BuildStore } from "../build-store"; import { serializeKanikoArguments } from "../common"; export async function runRivetBuild( @@ -57,7 +57,7 @@ export async function runRivetBuild( dockerfilePath: build.dockerfilePath, buildArgs: build.buildArgs, buildTarget: build.buildTarget, - }) + }), }, }, network: { @@ -81,8 +81,8 @@ export async function runRivetBuild( buildStore.updateStatus(buildId, { type: "running", data: { - rivet: { actorId } - } + rivet: { actorId }, + }, }); await pollActorStatus( diff --git a/cloud/packages/ci-manager/src/kaniko-runner.ts b/cloud/packages/ci-manager/src/kaniko-runner.ts index cb95147454..0cd4358034 100644 --- a/cloud/packages/ci-manager/src/kaniko-runner.ts +++ b/cloud/packages/ci-manager/src/kaniko-runner.ts @@ -1,7 +1,7 @@ -import { BuildStore } from "./build-store"; -import { mkdir } from "node:fs/promises"; import { existsSync } from "node:fs"; +import { mkdir } from "node:fs/promises"; import { dirname } from "node:path"; +import type { BuildStore } from "./build-store"; import { runDockerBuild } from "./executors/docker"; import { runRivetBuild } from "./executors/rivet"; @@ -20,8 +20,8 @@ export async function runKanikoBuild( buildStore.updateStatus(buildId, { type: "running", data: { - noRunner: {} - } + noRunner: {}, + }, }); const executionMode = process.env.KANIKO_EXECUTION_MODE || "docker"; diff --git a/cloud/packages/ci-manager/src/oci-converter.ts b/cloud/packages/ci-manager/src/oci-converter.ts index 5b4992877c..0f97e7220e 100644 --- a/cloud/packages/ci-manager/src/oci-converter.ts +++ b/cloud/packages/ci-manager/src/oci-converter.ts @@ -1,8 +1,6 @@ import { execSync } from "child_process"; -import { mkdir, rm, writeFile, readFile, readdir, stat } from "fs/promises"; -import { join, dirname } from "path"; -import { createReadStream, createWriteStream } from "fs"; -import { pipeline } from "stream/promises"; +import { join } from "path"; +import { mkdir, readFile, rm, writeFile } from "fs/promises"; import * as tar from "tar"; export interface OCIConversionResult { @@ -12,14 +10,14 @@ export interface OCIConversionResult { export async function convertDockerTarToOCIBundle( dockerTarPath: string, - tempDir: string = "/tmp/oci-conversion" + tempDir = "/tmp/oci-conversion", ): Promise { const conversionId = Math.random().toString(36).substring(7); const workDir = join(tempDir, conversionId); - + try { await mkdir(workDir, { recursive: true }); - + const dockerImagePath = join(workDir, "docker-image.tar"); const ociImagePath = join(workDir, "oci-image"); const ociBundlePath = join(workDir, "oci-bundle"); @@ -30,54 +28,71 @@ export async function convertDockerTarToOCIBundle( await writeFile(dockerImagePath, dockerTarData); // Convert Docker image to OCI image using skopeo - console.log(`Converting Docker image to OCI image: ${dockerImagePath} -> ${ociImagePath}`); - execSync(`skopeo copy docker-archive:${dockerImagePath} oci:${ociImagePath}:default`, { - stdio: "pipe" - }); + console.log( + `Converting Docker image to OCI image: ${dockerImagePath} -> ${ociImagePath}`, + ); + execSync( + `skopeo copy docker-archive:${dockerImagePath} oci:${ociImagePath}:default`, + { + stdio: "pipe", + }, + ); // Convert OCI image to OCI bundle using umoci - console.log(`Converting OCI image to OCI bundle: ${ociImagePath} -> ${ociBundlePath}`); - execSync(`umoci unpack --rootless --image ${ociImagePath}:default ${ociBundlePath}`, { - stdio: "pipe" - }); + console.log( + `Converting OCI image to OCI bundle: ${ociImagePath} -> ${ociBundlePath}`, + ); + execSync( + `umoci unpack --rootless --image ${ociImagePath}:default ${ociBundlePath}`, + { + stdio: "pipe", + }, + ); // Create tar from OCI bundle - console.log(`Creating tar from OCI bundle: ${ociBundlePath} -> ${bundleTarPath}`); + console.log( + `Creating tar from OCI bundle: ${ociBundlePath} -> ${bundleTarPath}`, + ); await tar.create( { file: bundleTarPath, cwd: ociBundlePath, }, - ["."] + ["."], ); // Clean up intermediate files await Promise.all([ rm(dockerImagePath, { force: true }), rm(ociImagePath, { recursive: true, force: true }), - rm(ociBundlePath, { recursive: true, force: true }) + rm(ociBundlePath, { recursive: true, force: true }), ]); const cleanup = async () => { try { await rm(workDir, { recursive: true, force: true }); } catch (error) { - console.warn(`Failed to cleanup OCI conversion directory ${workDir}:`, error); + console.warn( + `Failed to cleanup OCI conversion directory ${workDir}:`, + error, + ); } }; return { bundleTarPath, - cleanup + cleanup, }; } catch (error) { // Cleanup on error try { await rm(workDir, { recursive: true, force: true }); } catch (cleanupError) { - console.warn(`Failed to cleanup after error in ${workDir}:`, cleanupError); + console.warn( + `Failed to cleanup after error in ${workDir}:`, + cleanupError, + ); } throw new Error(`OCI conversion failed: ${error}`); } } - diff --git a/cloud/packages/ci-manager/src/rivet-uploader.ts b/cloud/packages/ci-manager/src/rivet-uploader.ts index 6107c7d4ea..d9c8c6ed1e 100644 --- a/cloud/packages/ci-manager/src/rivet-uploader.ts +++ b/cloud/packages/ci-manager/src/rivet-uploader.ts @@ -1,7 +1,5 @@ +import { statSync } from "fs"; import { RivetClient } from "@rivet-gg/api"; -import { warn } from "console"; -import { createReadStream, statSync } from "fs"; -import { readFile } from "fs/promises"; export interface RivetUploadConfig { token: string; @@ -113,7 +111,7 @@ export async function uploadOCIBundleToRivet( async function uploadChunkWithRetry( url: string, buffer: Buffer, - maxRetries: number = 3, + maxRetries = 3, ): Promise { let lastError: Error | null = null; @@ -232,4 +230,3 @@ async function patchBuildTags( // Don't throw here to avoid failing the entire upload process } } - diff --git a/cloud/packages/ci-manager/src/server.ts b/cloud/packages/ci-manager/src/server.ts index ae8b6e5c20..384a352a7e 100644 --- a/cloud/packages/ci-manager/src/server.ts +++ b/cloud/packages/ci-manager/src/server.ts @@ -1,23 +1,20 @@ -import { Hono } from "hono"; -import { streamSSE } from "hono/streaming"; -import { logger } from "hono/logger"; -import { BuildStore } from "./build-store"; -import { runKanikoBuild } from "./kaniko-runner"; -import { createWriteStream, createReadStream } from "node:fs"; +import { createReadStream, createWriteStream } from "node:fs"; import { mkdir, stat } from "node:fs/promises"; -import { dirname } from "path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; +import { dirname } from "path"; import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { logger } from "hono/logger"; +import { streamSSE } from "hono/streaming"; import type { ReadableStream as WebReadableStream } from "stream/web"; +import { BuildStore } from "./build-store"; +import { runKanikoBuild } from "./kaniko-runner"; +import { convertDockerTarToOCIBundle } from "./oci-converter"; import { - convertDockerTarToOCIBundle, -} from "./oci-converter"; -import { - uploadOCIBundleToRivet, type RivetUploadConfig, + uploadOCIBundleToRivet, } from "./rivet-uploader"; -import { UNIT_SEP_CHAR } from "./common"; import { BuildRequestSchema } from "./types"; async function processRivetUpload( @@ -92,7 +89,7 @@ async function processRivetUpload( } } -export async function createServer(port: number = 3000) { +export async function createServer(port = 3000) { const app = new Hono(); app.use(logger()); @@ -105,10 +102,7 @@ export async function createServer(port: number = 3000) { const body = await c.req.parseBody(); const parseResult = BuildRequestSchema.safeParse(body); if (!parseResult.success) { - return c.json( - { error: "Invalid build request format" }, - 400, - ); + return c.json({ error: "Invalid build request format" }, 400); } const { buildName, @@ -116,16 +110,16 @@ export async function createServer(port: number = 3000) { environmentId, buildArgs, buildTarget, - context: contextFile + context: contextFile, } = parseResult.data; // Create the build const buildId = buildStore.createBuild( - buildName, - dockerfilePath, - environmentId, + buildName, + dockerfilePath, + environmentId, buildArgs, - buildTarget + buildTarget, ); const contextPath = buildStore.getContextPath(buildId); diff --git a/cloud/packages/ci-manager/src/types.ts b/cloud/packages/ci-manager/src/types.ts index d56c6b9d4b..6519ec6099 100644 --- a/cloud/packages/ci-manager/src/types.ts +++ b/cloud/packages/ci-manager/src/types.ts @@ -2,98 +2,124 @@ import { z } from "zod"; import { NO_SEP_CHAR_REGEX, UNIT_SEP_CHAR } from "./common"; export const RunnerSchema = z.union([ - z.object({ - rivet: z.object({ - actorId: z.string(), - }) - }), - z.object({ - docker: z.object({}) - }), - z.object({ - noRunner: z.object({}) - }) + z.object({ + rivet: z.object({ + actorId: z.string(), + }), + }), + z.object({ + docker: z.object({}), + }), + z.object({ + noRunner: z.object({}), + }), ]); export type Runner = z.infer; export const StatusSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal("starting"), data: z.object({}) }), - z.object({ type: z.literal("running"), data: RunnerSchema }), - z.object({ type: z.literal("finishing"), data: z.object({}) }), - z.object({ type: z.literal("converting"), data: z.object({}) }), - z.object({ type: z.literal("uploading"), data: z.object({}) }), - z.object({ type: z.literal("failure"), data: z.object({ reason: z.string() }) }), - z.object({ type: z.literal("success"), data: z.object({ buildId: z.string() }) }), + z.object({ type: z.literal("starting"), data: z.object({}) }), + z.object({ type: z.literal("running"), data: RunnerSchema }), + z.object({ type: z.literal("finishing"), data: z.object({}) }), + z.object({ type: z.literal("converting"), data: z.object({}) }), + z.object({ type: z.literal("uploading"), data: z.object({}) }), + z.object({ + type: z.literal("failure"), + data: z.object({ reason: z.string() }), + }), + z.object({ + type: z.literal("success"), + data: z.object({ buildId: z.string() }), + }), ]); export type Status = z.infer; const ILLEGAL_BUILD_ARG_KEY = /[\s'"\\]/g; -const BuildArgsSchema = z.string() - .transform((str) => JSON.parse(str)) - .pipe(z.array(z.string())) - .refine((arr) => { - // Check each key=value pair to ensure keys have no spaces - return arr.every(item => { - const [key] = item.split('='); - if (!key) return false; - if (ILLEGAL_BUILD_ARG_KEY.test(key)) return false; - if (item.includes(UNIT_SEP_CHAR)) return false; - return true; - }); - }, { message: "Argument key/value contains invalid character" }) - .transform((arr) => { - const result: Record = Object.create(null); - // Convert array of strings to an object - for (const item of arr) { - const [key, ...valueParts] = item.split('='); - const value = valueParts.join('='); - - if (key && value !== undefined) { - result[key] = value; - } - } - - return result; - }); +const BuildArgsSchema = z + .string() + .transform((str) => JSON.parse(str)) + .pipe(z.array(z.string())) + .refine( + (arr) => { + // Check each key=value pair to ensure keys have no spaces + return arr.every((item) => { + const [key] = item.split("="); + if (!key) return false; + if (ILLEGAL_BUILD_ARG_KEY.test(key)) return false; + if (item.includes(UNIT_SEP_CHAR)) return false; + return true; + }); + }, + { message: "Argument key/value contains invalid character" }, + ) + .transform((arr) => { + const result: Record = Object.create(null); + // Convert array of strings to an object + for (const item of arr) { + const [key, ...valueParts] = item.split("="); + const value = valueParts.join("="); + + if (key && value !== undefined) { + result[key] = value; + } + } + + return result; + }); export const BuildRequestSchema = z.object({ - buildName: z.string() - .regex(NO_SEP_CHAR_REGEX, "buildName cannot contain special characters"), - dockerfilePath: z.string() - .regex(NO_SEP_CHAR_REGEX, "dockerfilePath cannot contain special characters"), - environmentId: z.string() - .regex(NO_SEP_CHAR_REGEX, "environmentId cannot contain special characters"), - buildArgs: BuildArgsSchema, - buildTarget: z.string() - .regex(NO_SEP_CHAR_REGEX, "buildTarget cannot contain special characters") - .optional(), - context: z.instanceof(File) + buildName: z + .string() + .regex( + NO_SEP_CHAR_REGEX, + "buildName cannot contain special characters", + ), + dockerfilePath: z + .string() + .regex( + NO_SEP_CHAR_REGEX, + "dockerfilePath cannot contain special characters", + ), + environmentId: z + .string() + .regex( + NO_SEP_CHAR_REGEX, + "environmentId cannot contain special characters", + ), + buildArgs: BuildArgsSchema, + buildTarget: z + .string() + .regex( + NO_SEP_CHAR_REGEX, + "buildTarget cannot contain special characters", + ) + .optional(), + context: z.instanceof(File), }); export type BuildRequest = z.infer; export const BuildEventSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal("status"), data: StatusSchema }), - z.object({ type: z.literal("log"), data: z.object({ line: z.string() }) }), + z.object({ type: z.literal("status"), data: StatusSchema }), + z.object({ type: z.literal("log"), data: z.object({ line: z.string() }) }), ]); export type BuildEvent = z.infer; export interface BuildInfo { - id: string; - status: Status; - buildName: string; - dockerfilePath: string; - environmentId: string; - contextPath: string; - buildArgs: Record; - buildTarget?: string; - outputPath: string; - events: BuildEvent[]; - containerProcess?: any; - createdAt: Date; - downloadedAt?: Date; - cleanupTimeout?: NodeJS.Timeout; + id: string; + status: Status; + buildName: string; + dockerfilePath: string; + environmentId: string; + contextPath: string; + buildArgs: Record; + buildTarget?: string; + outputPath: string; + events: BuildEvent[]; + containerProcess?: any; + createdAt: Date; + downloadedAt?: Date; + cleanupTimeout?: NodeJS.Timeout; } diff --git a/cloud/packages/ci-manager/tests/docker-e2e.test.ts b/cloud/packages/ci-manager/tests/docker-e2e.test.ts index 5245435444..2fbef9f1be 100644 --- a/cloud/packages/ci-manager/tests/docker-e2e.test.ts +++ b/cloud/packages/ci-manager/tests/docker-e2e.test.ts @@ -4,36 +4,28 @@ * Useful for quick iteration on the server. */ -import { describe, it, expect, beforeAll, afterAll, test } from "vitest"; import { execSync } from "node:child_process"; import { join } from "node:path"; -import { writeFile } from "fs/promises"; +import { RivetClient } from "@rivet-gg/api"; import getPort from "get-port"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import type { RivetUploadConfig } from "../src/rivet-uploader"; import { createServer } from "../src/server"; -import { RivetClient } from "@rivet-gg/api"; import { - createSampleDockerContext, + createActorFromBuild, + createBuildWithContext, createFailingDockerContext, createTestWebServerContext, - createBuildWithContext, getBuildStatus, pollBuildStatus, - downloadOutputTar, - waitForActorReady, testActorEndpoint, - createActorFromBuild, } from "./test-utils"; -import { convertDockerTarToOCIBundle } from "../src/oci-converter"; -import { - uploadOCIBundleToRivet, - type RivetUploadConfig, -} from "../src/rivet-uploader"; describe("Docker", () => { let server: any; let baseUrl: string; let rivetConfig: RivetUploadConfig = undefined as any; - let testActorIds: string[] = []; + const testActorIds: string[] = []; beforeAll(async () => { // Build the ci-manager dockerfile before starting tests diff --git a/cloud/packages/ci-manager/tests/oci-converter.test.ts b/cloud/packages/ci-manager/tests/oci-converter.test.ts index 9eeed5b432..d3ac999229 100644 --- a/cloud/packages/ci-manager/tests/oci-converter.test.ts +++ b/cloud/packages/ci-manager/tests/oci-converter.test.ts @@ -1,8 +1,8 @@ -import { test, expect, beforeAll, afterAll } from "vitest"; import { execSync } from "child_process"; -import { mkdir, writeFile, rm, readFile } from "fs/promises"; import { join } from "path"; +import { mkdir, readFile, rm, writeFile } from "fs/promises"; import * as tar from "tar"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { convertDockerTarToOCIBundle } from "../src/oci-converter"; const TEST_DIR = "/tmp/oci-converter-test"; @@ -11,7 +11,7 @@ const TEST_IMAGE_NAME = "oci-converter-test"; async function createTestDockerImage(): Promise { const contextDir = join(TEST_DIR, "docker-context"); await mkdir(contextDir, { recursive: true }); - + // Create a simple Dockerfile const dockerfile = ` FROM alpine:latest @@ -20,89 +20,90 @@ COPY test-script.sh /test-script.sh RUN chmod +x /test-script.sh CMD ["/test-script.sh"] `; - + // Create a test script const testScript = `#!/bin/sh echo "OCI conversion test successful!" cat /hello.txt `; - + await writeFile(join(contextDir, "Dockerfile"), dockerfile.trim()); await writeFile(join(contextDir, "test-script.sh"), testScript.trim()); - + // Build the Docker image console.log(`Building test Docker image: ${TEST_IMAGE_NAME}`); execSync(`docker build -t ${TEST_IMAGE_NAME} .`, { cwd: contextDir, - stdio: "pipe" + stdio: "pipe", }); - + // Save the Docker image to tar const dockerTarPath = join(TEST_DIR, "test-image.tar"); console.log(`Saving Docker image to: ${dockerTarPath}`); execSync(`docker save -o ${dockerTarPath} ${TEST_IMAGE_NAME}`, { - stdio: "pipe" + stdio: "pipe", }); - + return dockerTarPath; } async function createMockKanikoOutput(dockerTarPath: string): Promise { const kanikoOutputPath = join(TEST_DIR, "kaniko-output.tar.gz"); const tempDir = join(TEST_DIR, "kaniko-temp"); - + await mkdir(tempDir, { recursive: true }); - + // Copy docker tar to the expected location inside kaniko output const dockerTarData = await readFile(dockerTarPath); await writeFile(join(tempDir, "image.tar"), dockerTarData); - + // Create kaniko output tar.gz await tar.create( { file: kanikoOutputPath, gzip: true, - cwd: tempDir + cwd: tempDir, }, - ["."] + ["."], ); - + // Cleanup temp directory await rm(tempDir, { recursive: true, force: true }); - + return kanikoOutputPath; } async function validateOCIBundle(bundleTarPath: string): Promise { const validateDir = join(TEST_DIR, "validate"); await mkdir(validateDir, { recursive: true }); - + try { // Extract the OCI bundle tar await tar.extract({ file: bundleTarPath, - cwd: validateDir + cwd: validateDir, }); - + // Check for required OCI bundle files const configJsonPath = join(validateDir, "config.json"); const rootfsPath = join(validateDir, "rootfs"); - + // Verify config.json exists and is valid JSON const configData = await readFile(configJsonPath, "utf8"); const config = JSON.parse(configData); - + expect(config).toBeDefined(); expect(config.ociVersion).toBeDefined(); expect(config.process).toBeDefined(); expect(config.root).toBeDefined(); - + // Verify rootfs directory exists - const rootfsStat = await import("fs/promises").then(fs => fs.stat(rootfsPath)); + const rootfsStat = await import("fs/promises").then((fs) => + fs.stat(rootfsPath), + ); expect(rootfsStat.isDirectory()).toBe(true); - + console.log("OCI bundle validation passed"); - } finally { await rm(validateDir, { recursive: true, force: true }); } @@ -111,33 +112,39 @@ async function validateOCIBundle(bundleTarPath: string): Promise { beforeAll(async () => { // Create test directory await mkdir(TEST_DIR, { recursive: true }); - + // Check if Docker is available try { execSync("docker --version", { stdio: "pipe" }); } catch (error) { - throw new Error("Docker is not available. Please install Docker to run OCI converter tests."); + throw new Error( + "Docker is not available. Please install Docker to run OCI converter tests.", + ); } - + // Check if skopeo is available try { execSync("skopeo --version", { stdio: "pipe" }); } catch (error) { - throw new Error("skopeo is not available. Please install skopeo to run OCI converter tests."); + throw new Error( + "skopeo is not available. Please install skopeo to run OCI converter tests.", + ); } - + // Check if umoci is available try { execSync("umoci --version", { stdio: "pipe" }); } catch (error) { - throw new Error("umoci is not available. Please install umoci to run OCI converter tests."); + throw new Error( + "umoci is not available. Please install umoci to run OCI converter tests.", + ); } }); afterAll(async () => { // Cleanup test directory await rm(TEST_DIR, { recursive: true, force: true }); - + // Remove test Docker image try { execSync(`docker rmi ${TEST_IMAGE_NAME}`, { stdio: "pipe" }); @@ -148,32 +155,38 @@ afterAll(async () => { test("createTestDockerImage builds and saves Docker image", async () => { const dockerTarPath = await createTestDockerImage(); - + // Verify the tar file was created - const stats = await import("fs/promises").then(fs => fs.stat(dockerTarPath)); + const stats = await import("fs/promises").then((fs) => + fs.stat(dockerTarPath), + ); expect(stats.isFile()).toBe(true); expect(stats.size).toBeGreaterThan(0); - - console.log(`Test Docker image created: ${dockerTarPath} (${stats.size} bytes)`); -}, 60000); + console.log( + `Test Docker image created: ${dockerTarPath} (${stats.size} bytes)`, + ); +}, 60000); test("convertDockerTarToOCIBundle converts Docker tar to OCI bundle", async () => { const dockerTarPath = await createTestDockerImage(); - + const result = await convertDockerTarToOCIBundle(dockerTarPath); - + try { // Verify the OCI bundle tar was created - const stats = await import("fs/promises").then(fs => fs.stat(result.bundleTarPath)); + const stats = await import("fs/promises").then((fs) => + fs.stat(result.bundleTarPath), + ); expect(stats.isFile()).toBe(true); expect(stats.size).toBeGreaterThan(0); - - console.log(`OCI bundle created: ${result.bundleTarPath} (${stats.size} bytes)`); - + + console.log( + `OCI bundle created: ${result.bundleTarPath} (${stats.size} bytes)`, + ); + // Validate the OCI bundle structure await validateOCIBundle(result.bundleTarPath); - } finally { await result.cleanup(); } @@ -182,22 +195,25 @@ test("convertDockerTarToOCIBundle converts Docker tar to OCI bundle", async () = test("full workflow: Docker image -> direct OCI bundle conversion", async () => { // Create test Docker image const dockerTarPath = await createTestDockerImage(); - + // Convert directly to OCI bundle (like the new simplified flow) const convertResult = await convertDockerTarToOCIBundle(dockerTarPath); - + try { // Verify the final OCI bundle - const stats = await import("fs/promises").then(fs => fs.stat(convertResult.bundleTarPath)); + const stats = await import("fs/promises").then((fs) => + fs.stat(convertResult.bundleTarPath), + ); expect(stats.isFile()).toBe(true); expect(stats.size).toBeGreaterThan(0); - + // Validate OCI bundle structure await validateOCIBundle(convertResult.bundleTarPath); - - console.log(`Full workflow completed successfully: ${convertResult.bundleTarPath}`); - + + console.log( + `Full workflow completed successfully: ${convertResult.bundleTarPath}`, + ); } finally { await convertResult.cleanup(); } -}, 180000); \ No newline at end of file +}, 180000); diff --git a/cloud/packages/ci-manager/tests/rivet-e2e.test.ts b/cloud/packages/ci-manager/tests/rivet-e2e.test.ts index 5e1477c56b..29dbf914fc 100644 --- a/cloud/packages/ci-manager/tests/rivet-e2e.test.ts +++ b/cloud/packages/ci-manager/tests/rivet-e2e.test.ts @@ -1,20 +1,20 @@ -import { test, expect, beforeAll, afterAll } from "vitest"; import { execSync } from "child_process"; -import { mkdir, rm } from "fs/promises"; import { RivetClient } from "@rivet-gg/api"; +import { mkdir, rm } from "fs/promises"; +import { afterAll, beforeAll, expect, test } from "vitest"; +import type { RivetUploadConfig } from "../src/rivet-uploader"; import { + createActorFromBuild, createTestWebServerContext, pollBuildStatus, - waitForActorReady, testActorEndpoint, - createActorFromBuild, + waitForActorReady, } from "./test-utils"; -import { type RivetUploadConfig } from "../src/rivet-uploader"; const TEST_DIR = "/tmp/rivet-test"; let rivetConfig: RivetUploadConfig; -let testActorIds: string[] = []; +const testActorIds: string[] = []; async function findCIManagerActor( client: RivetClient, diff --git a/cloud/packages/ci-manager/tests/test-utils.ts b/cloud/packages/ci-manager/tests/test-utils.ts index ba4cfb9bfe..969e69b66a 100644 --- a/cloud/packages/ci-manager/tests/test-utils.ts +++ b/cloud/packages/ci-manager/tests/test-utils.ts @@ -1,8 +1,8 @@ -import { mkdir, writeFile, readFile, unlink } from "node:fs/promises"; -import { join } from "node:path"; +import { randomUUID } from "crypto"; import { execSync } from "node:child_process"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; import * as tar from "tar"; -import { randomUUID } from "crypto"; export async function createFailingDockerContext(): Promise { const tempDir = `/tmp/docker-context-fail-${Date.now()}-${Math.random().toString(36).substring(2)}`; @@ -215,7 +215,10 @@ export async function pollBuildStatus( build.status.type === "success" || build.status.type === "failure" ) { - return { status: build.status.type, buildId: build.status.data?.buildId }; + return { + status: build.status.type, + buildId: build.status.data?.buildId, + }; } if (attempt < maxAttempts - 1) { @@ -274,7 +277,6 @@ export async function getBuildStatus( return await response.json(); } - export async function testActorEndpoint(endpoint: string): Promise { const response = await fetch(`${endpoint}`, { method: "GET", @@ -345,7 +347,9 @@ export async function waitForActorReady(endpoint: string): Promise { return; } } catch (error) { - console.log(`Waiting for actor to be ready... (attempt ${attempt + 1})`); + console.log( + `Waiting for actor to be ready... (attempt ${attempt + 1})`, + ); } if (attempt < maxAttempts - 1) { @@ -355,4 +359,3 @@ export async function waitForActorReady(endpoint: string): Promise { throw new Error(`Actor not ready after ${maxAttempts} attempts`); } - diff --git a/cloud/packages/ci-manager/tests/upload-workflow.test.ts b/cloud/packages/ci-manager/tests/upload-workflow.test.ts index f1680517fe..8dafee4812 100644 --- a/cloud/packages/ci-manager/tests/upload-workflow.test.ts +++ b/cloud/packages/ci-manager/tests/upload-workflow.test.ts @@ -1,21 +1,21 @@ -import { test, expect, beforeAll, afterAll } from "vitest"; +import { RivetClient } from "@rivet-gg/api"; import { mkdir, rm } from "fs/promises"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { convertDockerTarToOCIBundle } from "../src/oci-converter"; import { - uploadOCIBundleToRivet, type RivetUploadConfig, + uploadOCIBundleToRivet, } from "../src/rivet-uploader"; -import { RivetClient } from "@rivet-gg/api"; import { createTestWebServerImage, - waitForActorReady, testActorEndpoint, + waitForActorReady, } from "./test-utils"; const TEST_DIR = "/tmp/upload-test"; let rivetConfig: RivetUploadConfig; -let testActorIds: string[] = []; +const testActorIds: string[] = []; async function createRivetActor( buildId: string, @@ -164,4 +164,4 @@ test("upload workflow", async () => { } finally { await conversionResult.cleanup(); } -}, 600000); \ No newline at end of file +}, 600000); diff --git a/cloud/packages/ci-manager/tsconfig.json b/cloud/packages/ci-manager/tsconfig.json index 195a99ceac..c593760ff0 100644 --- a/cloud/packages/ci-manager/tsconfig.json +++ b/cloud/packages/ci-manager/tsconfig.json @@ -1,43 +1,36 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "node", - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "skipLibCheck": true, - "declaration": false, - "outDir": "./dist", - "rootDir": "./src", - "removeComments": true, - "isolatedModules": true, - "allowJs": true, - "checkJs": false, - "incremental": true, - "tsBuildInfoFile": "./dist/.tsbuildinfo", - "types": ["node"], - "lib": ["ES2022", "DOM"] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "dist", - "**/*.test.ts", - "**/*.spec.ts" - ], - "ts-node": { - "esm": true, - "experimentalSpecifierResolution": "node" - } + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "declaration": false, + "outDir": "./dist", + "rootDir": "./src", + "removeComments": true, + "isolatedModules": true, + "allowJs": true, + "checkJs": false, + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "types": ["node"], + "lib": ["ES2022", "DOM"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node" + } } diff --git a/docker/dev-full/docker-compose.yml b/docker/dev-full/docker-compose.yml index ac1ef2676b..04c6a77892 100644 --- a/docker/dev-full/docker-compose.yml +++ b/docker/dev-full/docker-compose.yml @@ -385,7 +385,9 @@ services: - GF_INSTALL_PLUGINS=grafana-clickhouse-datasource frontend-hub: - image: node:22-bullseye + build: + context: ./frontend-hub + dockerfile: Dockerfile restart: unless-stopped working_dir: /app/frontend/apps/hub command: /bin/bash /etc/frontend-hub/entrypoint.sh diff --git a/docker/dev-full/frontend-hub/Dockerfile b/docker/dev-full/frontend-hub/Dockerfile new file mode 100644 index 0000000000..ba13aba3ef --- /dev/null +++ b/docker/dev-full/frontend-hub/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-bullseye + +# Install corepack globally as root, then fix permissions for user 1000:1000 +RUN npm install -g corepack && \ + corepack enable && \ + chown -R 1000:1000 /usr/local/lib/node_modules/corepack* /usr/local/bin/yarn /usr/local/bin/pnpm /usr/local/bin/pnpx || true + +# Switch to user 1000:1000 +USER 1000:1000 + +# Set working directory +WORKDIR /app \ No newline at end of file diff --git a/docker/dev-full/frontend-hub/entrypoint.sh b/docker/dev-full/frontend-hub/entrypoint.sh index f483c6e735..34103d988f 100755 --- a/docker/dev-full/frontend-hub/entrypoint.sh +++ b/docker/dev-full/frontend-hub/entrypoint.sh @@ -1,9 +1,6 @@ #!/bin/bash set -e -npm i -g corepack -corepack enable - # Install packages cd /app yarn install diff --git a/docker/dev-full/vector-server/vector.yaml b/docker/dev-full/vector-server/vector.yaml index 6011391ce0..9fa0090fcd 100644 --- a/docker/dev-full/vector-server/vector.yaml +++ b/docker/dev-full/vector-server/vector.yaml @@ -60,6 +60,14 @@ transforms: type: vrl source: .source == "pegboard_container_runner" + actors_transform: + type: remap + inputs: + - actors + source: | + # Add namespace label to actor logs + .namespace = "rivet" + clickhouse_dynamic_events_filter: type: filter inputs: @@ -107,7 +115,7 @@ sinks: clickhouse_actor_logs: type: clickhouse inputs: - - actors + - actors_transform compression: gzip database: db_pegboard_actor_log endpoint: http://clickhouse:8123 diff --git a/docker/monolith/vector-server/vector.yaml b/docker/monolith/vector-server/vector.yaml index ab93ad6041..fe5bbba465 100644 --- a/docker/monolith/vector-server/vector.yaml +++ b/docker/monolith/vector-server/vector.yaml @@ -59,6 +59,14 @@ transforms: condition: type: vrl source: .source == "pegboard_container_runner" + + actors_transform: + type: remap + inputs: + - actors + source: | + # Add namespace label to actor logs + .namespace = "rivet" sinks: prom_exporter: @@ -78,7 +86,7 @@ sinks: clickhouse_actor_logs: type: clickhouse inputs: - - actors + - actors_transform compression: gzip endpoint: http://clickhouse:9300 database: db_pegboard_actor_log diff --git a/examples/functions-rust/rivet.json b/examples/functions-rust/rivet.json index a9693dc19e..84f6350f41 100644 --- a/examples/functions-rust/rivet.json +++ b/examples/functions-rust/rivet.json @@ -1,7 +1,7 @@ { - "functions": { - "simple": { - "dockerfile": "Dockerfile" - } - } -} \ No newline at end of file + "functions": { + "simple": { + "dockerfile": "Dockerfile" + } + } +} diff --git a/examples/linear-agent-starter/package.json b/examples/linear-agent-starter/package.json index 22bb309767..ffe55a4ea1 100644 --- a/examples/linear-agent-starter/package.json +++ b/examples/linear-agent-starter/package.json @@ -1,36 +1,36 @@ { - "name": "linear-agent-starter", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "npx tsx --watch src/server/index.ts", - "dev:check-types": "npx tsc --noEmit --watch", - "check-types": "tsc --noEmit", - "test": "vitest run", - "deploy": "npx @actor-core/cli deploy rivet actors/app.ts" - }, - "devDependencies": { - "@actor-core/cli": "0.9.0-rc.1", - "@actor-core/rivet": "0.9.0-rc.1", - "@types/deno": "^2.2.0", - "@types/invariant": "^2", - "@types/node": "^22.13.9", - "actor-core": "0.9.0-rc.1", - "tsx": "^3.12.7", - "typescript": "^5.7.3", - "vitest": "^3.1.1" - }, - "stableVersion": "0.8.0", - "dependencies": { - "@actor-core/nodejs": "0.9.0-rc.1", - "@ai-sdk/anthropic": "^1.2.12", - "@ai-sdk/openai": "^1.3.22", - "@linear/sdk": "^40.0.0", - "ai": "^4.3.16", - "dotenv": "^16.5.0", - "hono": "^4.7.0", - "invariant": "^2.2.4", - "openid-client": "^6.5.0" - } + "name": "linear-agent-starter", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "npx tsx --watch src/server/index.ts", + "dev:check-types": "npx tsc --noEmit --watch", + "check-types": "tsc --noEmit", + "test": "vitest run", + "deploy": "npx @actor-core/cli deploy rivet actors/app.ts" + }, + "devDependencies": { + "@actor-core/cli": "0.9.0-rc.1", + "@actor-core/rivet": "0.9.0-rc.1", + "@types/deno": "^2.2.0", + "@types/invariant": "^2", + "@types/node": "^22.13.9", + "actor-core": "0.9.0-rc.1", + "tsx": "^3.12.7", + "typescript": "^5.7.3", + "vitest": "^3.1.1" + }, + "stableVersion": "0.8.0", + "dependencies": { + "@actor-core/nodejs": "0.9.0-rc.1", + "@ai-sdk/anthropic": "^1.2.12", + "@ai-sdk/openai": "^1.3.22", + "@linear/sdk": "^40.0.0", + "ai": "^4.3.16", + "dotenv": "^16.5.0", + "hono": "^4.7.0", + "invariant": "^2.2.4", + "openid-client": "^6.5.0" + } } diff --git a/examples/linear-agent-starter/src/actors/app.ts b/examples/linear-agent-starter/src/actors/app.ts index 0a74e25ec9..980e2f91eb 100644 --- a/examples/linear-agent-starter/src/actors/app.ts +++ b/examples/linear-agent-starter/src/actors/app.ts @@ -1,9 +1,9 @@ import { setup } from "actor-core"; -import { oauthSession } from "./oauth-session"; +import { createClient } from "actor-core/client"; +import { BASE_PATH, PORT } from "../config"; import { issueAgent } from "./issue-agent"; import { linearAppUser } from "./linear-app-user"; -import { createClient } from "actor-core/client"; -import { PORT, BASE_PATH } from "../config"; +import { oauthSession } from "./oauth-session"; export const app = setup({ actors: { issueAgent, oauthSession, linearAppUser }, diff --git a/examples/linear-agent-starter/src/actors/issue-agent.ts b/examples/linear-agent-starter/src/actors/issue-agent.ts index 0b095eafbb..003c924af6 100644 --- a/examples/linear-agent-starter/src/actors/issue-agent.ts +++ b/examples/linear-agent-starter/src/actors/issue-agent.ts @@ -1,9 +1,9 @@ -import { ActionContextOf, actor } from "actor-core"; +import { anthropic } from "@ai-sdk/anthropic"; import { LinearClient } from "@linear/sdk"; +import { type ActionContextOf, actor } from "actor-core"; +import { type CoreMessage, generateText } from "ai"; +import type { WebhookComment, WebhookIssue } from "../linear-types"; import { actorClient } from "./app"; -import { WebhookIssue, WebhookComment } from "../linear-types"; -import { CoreMessage, generateText } from "ai"; -import { anthropic } from "@ai-sdk/anthropic"; interface IssueAgentState { messages: CoreMessage[]; diff --git a/examples/linear-agent-starter/src/server/index.ts b/examples/linear-agent-starter/src/server/index.ts index 72814990ae..f2d982f56e 100644 --- a/examples/linear-agent-starter/src/server/index.ts +++ b/examples/linear-agent-starter/src/server/index.ts @@ -1,11 +1,11 @@ -import { Hono } from "hono"; -import { serve } from "@hono/node-server"; -import { app, type App, actorClient } from "../actors/app"; -import { createRouter } from "@actor-core/nodejs"; +import { atob } from "node:buffer"; import crypto from "node:crypto"; +import { createRouter } from "@actor-core/nodejs"; +import { serve } from "@hono/node-server"; import { LinearClient } from "@linear/sdk"; +import { Hono } from "hono"; import * as openidClient from "openid-client"; -import { atob } from "node:buffer"; +import { actorClient, app } from "../actors/app"; import type { OAuthExpectedState } from "../actors/oauth-session"; import { BASE_PATH, diff --git a/examples/linear-agent-starter/tsconfig.json b/examples/linear-agent-starter/tsconfig.json index 5d08efe600..b9983b24b6 100644 --- a/examples/linear-agent-starter/tsconfig.json +++ b/examples/linear-agent-starter/tsconfig.json @@ -1,15 +1,14 @@ { - "compilerOptions": { - "target": "esnext", - "module": "esnext", - "strict": true, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "stripInternal": true, - "moduleResolution": "bundler", - "lib": ["ESNext", "DOM"], - "types": ["node"] - } + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "stripInternal": true, + "moduleResolution": "bundler", + "lib": ["ESNext", "DOM"], + "types": ["node"] + } } - diff --git a/examples/multitenant-deploys/package.json b/examples/multitenant-deploys/package.json index 001a9c24ea..3e3dc7b98d 100644 --- a/examples/multitenant-deploys/package.json +++ b/examples/multitenant-deploys/package.json @@ -1,22 +1,22 @@ { - "name": "multitenant-deploys", - "packageManager": "yarn@4.6.0", - "scripts": { - "dev": "tsx src/index.ts", - "test": "vitest run", - "build": "tsc" - }, - "dependencies": { - "@hono/node-server": "^1.7.0", - "axios": "^1.6.7", - "hono": "^4.0.5", - "temp": "^0.9.4" - }, - "devDependencies": { - "@types/node": "^20.11.19", - "@types/temp": "^0.9.4", - "tsx": "^4.7.0", - "typescript": "^5.3.3", - "vitest": "^1.2.2" - } + "name": "multitenant-deploys", + "packageManager": "yarn@4.6.0", + "scripts": { + "dev": "tsx src/index.ts", + "test": "vitest run", + "build": "tsc" + }, + "dependencies": { + "@hono/node-server": "^1.7.0", + "axios": "^1.6.7", + "hono": "^4.0.5", + "temp": "^0.9.4" + }, + "devDependencies": { + "@types/node": "^20.11.19", + "@types/temp": "^0.9.4", + "tsx": "^4.7.0", + "typescript": "^5.3.3", + "vitest": "^1.2.2" + } } diff --git a/examples/multitenant-deploys/src/app.ts b/examples/multitenant-deploys/src/app.ts index dc8dc10798..dd4de1f642 100644 --- a/examples/multitenant-deploys/src/app.ts +++ b/examples/multitenant-deploys/src/app.ts @@ -1,8 +1,8 @@ -import { Hono } from "hono"; import { exec } from "node:child_process"; -import { promisify } from "node:util"; import * as fs from "node:fs/promises"; import * as path from "node:path"; +import { promisify } from "node:util"; +import { Hono } from "hono"; import temp from "temp"; const execAsync = promisify(exec); @@ -101,7 +101,7 @@ app.post("/deploy/:appId", async (c) => { build_path: "./project/", dockerfile: "./Dockerfile", unstable: { - build_method: "remote" + build_method: "remote", }, build_args: { // See MY_ENV_VAR build args in Dockerfile diff --git a/examples/multitenant-deploys/src/index.ts b/examples/multitenant-deploys/src/index.ts index 85bd3eb658..767135b8bb 100644 --- a/examples/multitenant-deploys/src/index.ts +++ b/examples/multitenant-deploys/src/index.ts @@ -7,4 +7,3 @@ serve({ fetch: app.fetch, port: Number(PORT), }); - diff --git a/examples/multitenant-deploys/tests/deploy.test.ts b/examples/multitenant-deploys/tests/deploy.test.ts index 6c51868794..e2d1e1f961 100644 --- a/examples/multitenant-deploys/tests/deploy.test.ts +++ b/examples/multitenant-deploys/tests/deploy.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from "vitest"; -import { app } from "../src/app"; import * as fs from "node:fs/promises"; -import * as path from "node:path"; import * as os from "node:os"; +import * as path from "node:path"; +import { describe, expect, it } from "vitest"; +import { app } from "../src/app"; describe("Deploy Endpoint", () => { it("should deploy an application and return endpoint", async () => { diff --git a/examples/multitenant-deploys/tsconfig.json b/examples/multitenant-deploys/tsconfig.json index b59c2086ec..ce03e9da31 100644 --- a/examples/multitenant-deploys/tsconfig.json +++ b/examples/multitenant-deploys/tsconfig.json @@ -1,12 +1,12 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "esModuleInterop": true, - "strict": true, - "outDir": "dist", - "skipLibCheck": true - }, - "include": ["src/**/*"] -} \ No newline at end of file + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/examples/system-test-actor/src/container/main.ts b/examples/system-test-actor/src/container/main.ts index e6f3a38c39..e1157a830a 100644 --- a/examples/system-test-actor/src/container/main.ts +++ b/examples/system-test-actor/src/container/main.ts @@ -1,17 +1,17 @@ +import dgram from "dgram"; +import fs from "fs"; import { serve } from "@hono/node-server"; import { createNodeWebSocket } from "@hono/node-ws"; import { createAndStartServer } from "../shared/server.js"; -import dgram from 'dgram'; -import fs from 'fs'; // Print hosts file contents before starting try { - const hostsContent = fs.readFileSync('/etc/hosts', 'utf8'); - console.log('=== /etc/hosts contents ==='); + const hostsContent = fs.readFileSync("/etc/hosts", "utf8"); + console.log("=== /etc/hosts contents ==="); console.log(hostsContent); - console.log('=== End of /etc/hosts ==='); + console.log("=== End of /etc/hosts ==="); } catch (err) { - console.error('Failed to read /etc/hosts:', err); + console.error("Failed to read /etc/hosts:", err); } let injectWebSocket: any; @@ -25,7 +25,6 @@ const { app, port } = createAndStartServer((app) => { const server = serve({ fetch: app.fetch, port }); injectWebSocket(server); - // async function contactApi() { // console.log('Contacting', process.env.RIVET_API_ENDPOINT); // const res = await fetch(process.env.RIVET_API_ENDPOINT!); @@ -42,25 +41,26 @@ const portEnv = if (portEnv) { // Create a UDP socket - const udpServer = dgram.createSocket('udp4'); + const udpServer = dgram.createSocket("udp4"); // Listen for incoming messages - udpServer.on('message', (msg, rinfo) => { - console.log(`UDP server received: ${msg} from ${rinfo.address}:${rinfo.port}`); + udpServer.on("message", (msg, rinfo) => { + console.log( + `UDP server received: ${msg} from ${rinfo.address}:${rinfo.port}`, + ); // Echo the message back to the sender udpServer.send(msg, rinfo.port, rinfo.address, (err) => { - if (err) console.error('Failed to send UDP response:', err); + if (err) console.error("Failed to send UDP response:", err); }); }); // Handle errors - udpServer.on('error', (err) => { - console.error('UDP server error:', err); + udpServer.on("error", (err) => { + console.error("UDP server error:", err); udpServer.close(); }); - const port2 = Number.parseInt(portEnv); udpServer.bind(port2, () => { diff --git a/examples/system-test-actor/tests/client.ts b/examples/system-test-actor/tests/client.ts index 4a09f8d03b..7521330a31 100644 --- a/examples/system-test-actor/tests/client.ts +++ b/examples/system-test-actor/tests/client.ts @@ -1,6 +1,6 @@ +import dgram from "dgram"; import { RivetClient } from "@rivet-gg/api"; import WebSocket from "ws"; -import dgram from 'dgram'; // Can be opt since they're not required for dev const RIVET_ENDPOINT = process.env.RIVET_ENDPOINT; @@ -63,11 +63,11 @@ async function run() { }, ...(BUILD_NAME === "ws-container" ? { - resources: { - cpu: 100, - memory: 100, - }, - } + resources: { + cpu: 100, + memory: 100, + }, + } : {}), }, }); @@ -154,7 +154,7 @@ async function run() { }); // UDP - let res = await client.actor.get(actor.id, { + const res = await client.actor.get(actor.id, { project: RIVET_PROJECT, environment: RIVET_ENVIRONMENT, }); @@ -165,10 +165,10 @@ async function run() { console.log("UDP server address:", udpServer); // Create a UDP socket - const udpClient = dgram.createSocket('udp4'); + const udpClient = dgram.createSocket("udp4"); // Send a message to the UDP echo server - const message = Buffer.from('Hello UDP server!'); + const message = Buffer.from("Hello UDP server!"); udpClient.send(message, udpPort.port, udpPort.hostname, (err) => { if (err) { console.error("Error sending UDP message:", err); @@ -179,18 +179,18 @@ async function run() { }); // Listen for a response - udpClient.on('message', (msg, rinfo) => { + udpClient.on("message", (msg, rinfo) => { console.log(`UDP message received: ${msg.toString()}`); console.log(`From: ${rinfo.address}:${rinfo.port}`); udpClient.close(); }); - udpClient.on('error', (err) => { + udpClient.on("error", (err) => { console.error("UDP client error:", err); udpClient.close(); }); - udpClient.on('close', () => { + udpClient.on("close", () => { console.log("UDP connection closed"); }); diff --git a/frontend/apps/hub/ARCHITECTURE.md b/frontend/apps/hub/ARCHITECTURE.md new file mode 100644 index 0000000000..234b5b7ea6 --- /dev/null +++ b/frontend/apps/hub/ARCHITECTURE.md @@ -0,0 +1,153 @@ +# Hub Architecture + +## Overview + +The Hub is a React/TypeScript dashboard application built with: +- **TanStack Router** - File-based routing +- **TanStack Query** - Server state management +- **Jotai** - Complex client state +- **React Context** - Global app state +- **Tailwind CSS** - Styling + +## Directory Structure + +``` +src/ +├── domains/ # Feature domains +│ └── {domain}/ +│ ├── components/ # Domain-specific components +│ ├── queries/ # Query options & mutations +│ ├── data/ # Context providers +│ └── forms/ # Form schemas +├── routes/ # Page components (file-based) +├── components/ # Shared UI components +├── queries/ # Global query setup +├── hooks/ # Custom React hooks +├── lib/ # Utilities +└── app.tsx # Root setup +``` + +## State Management + +### Server State (TanStack Query) +```typescript +// Query pattern: src/domains/{domain}/queries/query-options.ts +export const actorLogsQueryOptions = ({ projectNameId, environmentNameId, actorId }) => + queryOptions({ + queryKey: ["project", projectNameId, "environment", environmentNameId, "actor", actorId, "logs"], + queryFn: async ({ signal }) => rivetClient.actors.logs.get(...), + meta: { watch: true }, // Enable real-time updates + }); +``` + +### Client State (Jotai) +Used for complex, modular state (e.g., actor management): +```typescript +// Atoms for fine-grained reactivity +export const currentActorIdAtom = atom(undefined); +export const actorsAtom = atom([]); +``` + +### Global State (React Context) +```typescript +// Context for app-wide concerns +AuthContext // User authentication +ProjectContext // Current project +EnvironmentContext // Current environment +``` + +## Query Patterns + +### Standard Query +```typescript +const { data } = useQuery(projectQueryOptions(projectId)); +``` + +### Infinite Query +```typescript +const { data, fetchNextPage } = useInfiniteQuery( + projectActorsQueryOptions({ projectNameId, environmentNameId }) +); +``` + +### Mutations +```typescript +const mutation = useMutation({ + mutationFn: (data) => rivetClient.actors.destroy(data), + onSuccess: () => queryClient.invalidateQueries(...) +}); +``` + +### Real-time Updates +Queries with `watchIndex` parameter automatically refetch when server data changes: +```typescript +queryFn: ({ meta }) => api.call({ + watchIndex: getMetaWatchIndex(meta) +}) +``` + +## Key Conventions + +### Query Keys +Hierarchical structure matching API resources: +```typescript +["project", projectId, "environment", envId, "actor", actorId, "logs"] +``` + +### File Naming +- Query options: `query-options.ts` +- Mutations: `mutations.ts` +- Context: `{resource}-context.tsx` +- Components: `{feature}-{component}.tsx` + +### Import Aliases +```typescript +@/domains // Domain logic +@/components // Shared components +@/queries // Query utilities +@/hooks // Custom hooks +@/lib // Utilities +``` + +## API Integration + +- **rivetClient**: Main API client (`@rivet-gg/api-full`) +- **rivetEeClient**: Enterprise API (`@rivet-gg/api-ee`) +- Auto token refresh on expiration +- Request deduplication & caching + +## Example: Adding a New Feature + +1. **Create query options**: + ```typescript + // src/domains/project/queries/feature/query-options.ts + export const featureQueryOptions = (id: string) => queryOptions({ + queryKey: ["feature", id], + queryFn: () => rivetClient.feature.get(id), + }); + ``` + +2. **Create mutations**: + ```typescript + // src/domains/project/queries/feature/mutations.ts + export const useCreateFeatureMutation = () => useMutation({ + mutationFn: (data) => rivetClient.feature.create(data), + }); + ``` + +3. **Create route**: + ```typescript + // src/routes/.../feature.tsx + export const Route = createFileRoute('...')({ + component: FeaturePage, + }); + ``` + +4. **Use in component**: + ```typescript + function FeaturePage() { + const { data } = useQuery(featureQueryOptions(id)); + const mutation = useCreateFeatureMutation(); + // ... + } + ``` \ No newline at end of file diff --git a/frontend/apps/hub/src/components/command-panel/command-panel-page/environment-command-panel-page.tsx b/frontend/apps/hub/src/components/command-panel/command-panel-page/environment-command-panel-page.tsx index 355610524c..cd23b8bd88 100644 --- a/frontend/apps/hub/src/components/command-panel/command-panel-page/environment-command-panel-page.tsx +++ b/frontend/apps/hub/src/components/command-panel/command-panel-page/environment-command-panel-page.tsx @@ -21,7 +21,6 @@ import { faFunction, faGear, faGlobe, - faHammer, faJoystick, faKey, faLink, diff --git a/frontend/apps/hub/src/components/error-component.tsx b/frontend/apps/hub/src/components/error-component.tsx index e527b37d3f..05cb8592d9 100644 --- a/frontend/apps/hub/src/components/error-component.tsx +++ b/frontend/apps/hub/src/components/error-component.tsx @@ -22,10 +22,10 @@ import { isNotFound, useRouter, } from "@tanstack/react-router"; +import posthog from "posthog-js"; import { useEffect } from "react"; import { NetworkIssueError } from "./network-issue-error"; import { NotFoundComponent } from "./not-found-component"; -import posthog from "posthog-js"; export const ErrorComponent = ({ error, diff --git a/frontend/apps/hub/src/components/header/header-sub-nav.tsx b/frontend/apps/hub/src/components/header/header-sub-nav.tsx index d450695ad5..13b5b178c2 100644 --- a/frontend/apps/hub/src/components/header/header-sub-nav.tsx +++ b/frontend/apps/hub/src/components/header/header-sub-nav.tsx @@ -1,11 +1,7 @@ import { useAuth } from "@/domains/auth/contexts/auth"; import { Skeleton, cn } from "@rivet-gg/components"; import { ErrorBoundary } from "@sentry/react"; -import { - useMatches, - useMatchRoute, - useRouterState, -} from "@tanstack/react-router"; +import { useMatchRoute, useMatches } from "@tanstack/react-router"; import { Suspense, useContext } from "react"; import { MobileBreadcrumbsContext } from "../breadcrumbs/mobile-breadcrumbs"; import { HeaderEnvironmentLinks } from "./links/header-environment-links"; diff --git a/frontend/apps/hub/src/components/header/header.tsx b/frontend/apps/hub/src/components/header/header.tsx index c5f7e953a5..03489378ec 100644 --- a/frontend/apps/hub/src/components/header/header.tsx +++ b/frontend/apps/hub/src/components/header/header.tsx @@ -13,12 +13,12 @@ import { ErrorBoundary } from "@sentry/react"; import { Link } from "@tanstack/react-router"; import { Breadcrumbs } from "../breadcrumbs/breadcrumbs"; import { MobileBreadcrumbs } from "../breadcrumbs/mobile-breadcrumbs"; +import { CommandPanel } from "../command-panel"; import { Changelog } from "./changelog"; import { HeaderRouteLoader } from "./header-route-loader"; import { HeaderSubNav } from "./header-sub-nav"; import { MobileHeaderSubNav } from "./mobile-header-sub-nav"; import { NavItem } from "./nav-item"; -import { CommandPanel } from "../command-panel"; const UserProfileButton = () => { const { profile } = useAuth(); diff --git a/frontend/apps/hub/src/domains/auth/views/login-view/hooks.ts b/frontend/apps/hub/src/domains/auth/views/login-view/hooks.ts index 7e27e1c3a4..61f101743a 100644 --- a/frontend/apps/hub/src/domains/auth/views/login-view/hooks.ts +++ b/frontend/apps/hub/src/domains/auth/views/login-view/hooks.ts @@ -1,6 +1,5 @@ import type { SubmitHandler as FormSubmitHandler } from "@/domains/auth/forms/otp-form"; import { useCompleteEmailVerificationMutation } from "@/domains/auth/queries"; -import { selfProfileQueryOptions } from "@/domains/user/queries"; import { Rivet } from "@rivet-gg/api-full"; import * as Sentry from "@sentry/react"; import { useQueryClient } from "@tanstack/react-query"; diff --git a/frontend/apps/hub/src/domains/project/components/actors/actors-actor-details-wrapper.tsx b/frontend/apps/hub/src/domains/project/components/actors/actors-actor-details-wrapper.tsx new file mode 100644 index 0000000000..c05228ec7c --- /dev/null +++ b/frontend/apps/hub/src/domains/project/components/actors/actors-actor-details-wrapper.tsx @@ -0,0 +1,52 @@ +import { ActorsActorDetails } from "@rivet-gg/components/actors"; +import type { ActorAtom } from "@rivet-gg/components/actors"; +import { useEnvironment } from "../../data/environment-context"; +import { useProject } from "../../data/project-context"; +import { useExportActorLogsMutation } from "../../queries/actors/mutations"; + +interface ActorsActorDetailsWrapperProps { + tab?: string; + actor: ActorAtom; + onTabChange?: (tab: string) => void; +} + +export function ActorsActorDetailsWrapper({ + tab, + actor, + onTabChange, +}: ActorsActorDetailsWrapperProps) { + const { nameId: projectNameId } = useProject(); + const { nameId: environmentNameId } = useEnvironment(); + const exportMutation = useExportActorLogsMutation(); + + const handleExportLogs = async ( + actorId: string, + _typeFilter?: string, + _filter?: string, + ) => { + // TODO: Add above filters + const result = await exportMutation.mutateAsync({ + projectNameId, + environmentNameId, + queryJson: JSON.stringify({ + string_equal: { + property: "actor_id", + value: actorId, + }, + }), + }); + + // Open the presigned URL in a new tab to download + window.open(result.url, "_blank"); + }; + + return ( + + ); +} diff --git a/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx b/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx index 8562e3f85a..df974d6fe7 100644 --- a/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx +++ b/frontend/apps/hub/src/domains/project/components/actors/actors-provider.tsx @@ -1,43 +1,42 @@ import { router } from "@/app"; -import { queryClient, rivetClient } from "@/queries/global"; +import { queryClient } from "@/queries/global"; +import type { Rivet } from "@rivet-gg/api-full"; import { type FilterValue, toRecord } from "@rivet-gg/components"; import { - currentActorIdAtom, - actorFiltersAtom, - actorsPaginationAtom, - actorsAtom, - getActorStatus, + type Actor, type DestroyActor, - actorRegionsAtom, - actorBuildsAtom, - createActorAtom, type Logs, type Metrics, - actorsQueryAtom, - actorsInternalFilterAtom, - type Actor, + actorBuildsAtom, actorEnvironmentAtom, - exportLogsHandlerAtom, + actorFiltersAtom, + actorRegionsAtom, + actorsAtom, + actorsInternalFilterAtom, + actorsPaginationAtom, + actorsQueryAtom, + createActorAtom, + currentActorIdAtom, + getActorStatus, } from "@rivet-gg/components/actors"; import { InfiniteQueryObserver, - QueryObserver, MutationObserver, + QueryObserver, } from "@tanstack/react-query"; -//import { createClient } from "actor-core/client"; -import { atom, createStore, Provider, type PrimitiveAtom } from "jotai"; import equal from "fast-deep-equal"; +//import { createClient } from "actor-core/client"; +import { type PrimitiveAtom, Provider, atom, createStore } from "jotai"; import { type ReactNode, useEffect, useState } from "react"; import { - projectActorsQueryOptions, - createActorEndpoint, - destroyActorMutationOptions, + actorBuildsQueryOptions, actorLogsQueryOptions, actorMetricsQueryOptions, actorRegionsQueryOptions, - actorBuildsQueryOptions, + createActorEndpoint, + destroyActorMutationOptions, + projectActorsQueryOptions, } from "../../queries"; -import type { Rivet } from "@rivet-gg/api-full"; interface ActorsProviderProps { actorId: string | undefined; @@ -85,17 +84,6 @@ export function ActorsProvider({ store.set(actorEnvironmentAtom, { projectNameId, environmentNameId }); }, [projectNameId, environmentNameId]); - // biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency - useEffect(() => { - store.set(exportLogsHandlerAtom, async ({ projectNameId, environmentNameId, queryJson }) => { - return rivetClient.actors.logs.export({ - project: projectNameId, - environment: environmentNameId, - queryJson, - }); - }); - }, []); - // biome-ignore lint/correctness/useExhaustiveDependencies: store is not a dependency useEffect(() => { store.set(actorFiltersAtom, { @@ -285,11 +273,14 @@ export function ActorsProvider({ metrics.onMount = (set) => { const metricsObserver = new QueryObserver( queryClient, - actorMetricsQueryOptions({ - projectNameId, - environmentNameId, - actorId: actor.id, - }, { refetchInterval: 5000 }), + actorMetricsQueryOptions( + { + projectNameId, + environmentNameId, + actorId: actor.id, + }, + { refetchInterval: 5000 }, + ), ); type MetricsQuery = { diff --git a/frontend/apps/hub/src/domains/project/components/billing/billing-context.tsx b/frontend/apps/hub/src/domains/project/components/billing/billing-context.tsx index 8971e8675c..c2550da7a3 100644 --- a/frontend/apps/hub/src/domains/project/components/billing/billing-context.tsx +++ b/frontend/apps/hub/src/domains/project/components/billing/billing-context.tsx @@ -6,12 +6,12 @@ import { clusterQueryOptions } from "@/domains/auth/queries/bootstrap"; import type { Rivet } from "@rivet-gg/api-full"; import { startOfMonth } from "date-fns"; import { calculateUsedCredits } from "../../data/billing-calculate-usage"; +import { useProject } from "../../data/project-context"; import { projectBillingQueryOptions, projectBillingUsageQueryOptions, projectQueryOptions, } from "../../queries"; -import { useProject } from "../../data/project-context"; interface BillingContextValue { project: Rivet.cloud.GameFull; diff --git a/frontend/apps/hub/src/domains/project/components/billing/billing-portal-button.tsx b/frontend/apps/hub/src/domains/project/components/billing/billing-portal-button.tsx index 4e49d9d0eb..4a437d6fe7 100644 --- a/frontend/apps/hub/src/domains/project/components/billing/billing-portal-button.tsx +++ b/frontend/apps/hub/src/domains/project/components/billing/billing-portal-button.tsx @@ -1,6 +1,6 @@ import { Button, type ButtonProps } from "@rivet-gg/components"; -import { portalBillingSessionQueryOptions } from "../../queries"; import { useQuery } from "@tanstack/react-query"; +import { portalBillingSessionQueryOptions } from "../../queries"; interface BillingPortalButtonProps extends ButtonProps { groupId: string; diff --git a/frontend/apps/hub/src/domains/project/components/dialogs/edit-route-dialog.tsx b/frontend/apps/hub/src/domains/project/components/dialogs/edit-route-dialog.tsx index 716ec43297..ad7393e8a1 100644 --- a/frontend/apps/hub/src/domains/project/components/dialogs/edit-route-dialog.tsx +++ b/frontend/apps/hub/src/domains/project/components/dialogs/edit-route-dialog.tsx @@ -1,5 +1,6 @@ import * as EditRouteForm from "@/domains/project/forms/route-edit-form"; import type { DialogContentProps } from "@/hooks/use-dialog"; +import type { Rivet } from "@rivet-gg/api-full"; import { Button, DialogFooter, @@ -10,7 +11,6 @@ import { import { useSuspenseQuery } from "@tanstack/react-query"; import { useState } from "react"; import { routeQueryOptions, usePatchRouteMutation } from "../../queries"; -import type { Rivet } from "@rivet-gg/api-full"; interface OptionalContentProps extends DialogContentProps { projectNameId: string; diff --git a/frontend/apps/hub/src/domains/project/components/tags-select.tsx b/frontend/apps/hub/src/domains/project/components/tags-select.tsx index 273d48a4e5..29ba2e7c63 100644 --- a/frontend/apps/hub/src/domains/project/components/tags-select.tsx +++ b/frontend/apps/hub/src/domains/project/components/tags-select.tsx @@ -1,7 +1,7 @@ import { Combobox } from "@rivet-gg/components"; +import { ActorTag } from "@rivet-gg/components/actors"; import { useSuspenseQuery } from "@tanstack/react-query"; import { actorBuildTagsQueryOptions } from "../queries"; -import { ActorTag } from "@rivet-gg/components/actors"; interface TagsSelectProps { projectId: string; diff --git a/frontend/apps/hub/src/domains/project/data/environment-context.tsx b/frontend/apps/hub/src/domains/project/data/environment-context.tsx index 4593246e6e..ebca61ab76 100644 --- a/frontend/apps/hub/src/domains/project/data/environment-context.tsx +++ b/frontend/apps/hub/src/domains/project/data/environment-context.tsx @@ -1,7 +1,7 @@ +import type { Rivet } from "@rivet-gg/api-full"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { environmentByIdQueryOptions } from "../queries"; import { type ReactNode, createContext, useContext } from "react"; -import type { Rivet } from "@rivet-gg/api-full"; +import { environmentByIdQueryOptions } from "../queries"; import { useProject } from "./project-context"; export const EnvironmentContext = createContext< diff --git a/frontend/apps/hub/src/domains/project/data/project-context.tsx b/frontend/apps/hub/src/domains/project/data/project-context.tsx index 64e6f822ea..243f21ef34 100644 --- a/frontend/apps/hub/src/domains/project/data/project-context.tsx +++ b/frontend/apps/hub/src/domains/project/data/project-context.tsx @@ -1,9 +1,9 @@ +import { useAuth } from "@/domains/auth/contexts/auth"; +import { ls } from "@/lib/ls"; +import type { Rivet } from "@rivet-gg/api-full"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { projectByIdQueryOptions } from "../queries"; import { type ReactNode, createContext, useContext, useEffect } from "react"; -import type { Rivet } from "@rivet-gg/api-full"; -import { ls } from "@/lib/ls"; -import { useAuth } from "@/domains/auth/contexts/auth"; +import { projectByIdQueryOptions } from "../queries"; export const ProjectContext = createContext( undefined, diff --git a/frontend/apps/hub/src/domains/project/forms/route-edit-form.tsx b/frontend/apps/hub/src/domains/project/forms/route-edit-form.tsx index 9c2dd320ae..fda88e4b66 100644 --- a/frontend/apps/hub/src/domains/project/forms/route-edit-form.tsx +++ b/frontend/apps/hub/src/domains/project/forms/route-edit-form.tsx @@ -2,19 +2,19 @@ import { bootstrapQueryOptions } from "@/domains/auth/queries/bootstrap"; import { queryClient } from "@/queries/global"; import { Button, + Checkbox, + Code, Combobox, - createSchemaForm, + type ComboboxOption, FormControl, + FormDescription, FormField, FormFieldContext, FormItem, FormLabel, FormMessage, Input, - type ComboboxOption, - FormDescription, - Checkbox, - Code, + createSchemaForm, } from "@rivet-gg/components"; import { Icon, faTrash } from "@rivet-gg/icons"; import { diff --git a/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts b/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts index 51b60836c6..a8048e0acd 100644 --- a/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts +++ b/frontend/apps/hub/src/domains/project/queries/actors/mutations.ts @@ -2,13 +2,13 @@ import { mutationOptions, queryClient, rivetClient } from "@/queries/global"; import type { Rivet } from "@rivet-gg/api-full"; import { toast } from "@rivet-gg/components"; import { useMutation } from "@tanstack/react-query"; +import { customAlphabet } from "nanoid"; import { actorBuildsQueryOptions, actorQueryOptions, projectActorsQueryOptions, routesQueryOptions, } from "./query-options"; -import { customAlphabet } from "nanoid"; const nanoid = customAlphabet("0123456789abcdefghijklmnoprstwuxyz", 5); diff --git a/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts b/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts index 5fde819181..37771832fe 100644 --- a/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts +++ b/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts @@ -2,7 +2,7 @@ import { mergeWatchStreams } from "@/lib/watch-utilities"; import { rivetClient } from "@/queries/global"; import { getMetaWatchIndex } from "@/queries/utils"; import type { Rivet } from "@rivet-gg/api-full"; -import { safe, logfmt, type LogFmtValue, toRecord } from "@rivet-gg/components"; +import { type LogFmtValue, logfmt, safe, toRecord } from "@rivet-gg/components"; import { getActorStatus } from "@rivet-gg/components/actors"; import { type InfiniteData, @@ -225,9 +225,13 @@ export const actorLogsQueryOptions = ( { project, environment, - actorIdsJson: JSON.stringify([actorId]), + queryJson: JSON.stringify({ + string_equal: { + property: "actor_id", + value: actorId, + }, + }), watchIndex: getMetaWatchIndex(meta), - stream: "all", }, { abortSignal }, ); @@ -259,10 +263,12 @@ export const actorMetricsQueryOptions = ( projectNameId, environmentNameId, actorId, + timeWindowMs, }: { projectNameId: string; environmentNameId: string; actorId: string; + timeWindowMs?: number; }, opts: { refetchInterval?: number } = {}, ) => { @@ -276,26 +282,25 @@ export const actorMetricsQueryOptions = ( "actor", actorId, "metrics", + timeWindowMs, ] as const, queryFn: async ({ signal: abortSignal, - queryKey: [, project, , environment, , actorId], + queryKey: [, project, , environment, , actorId, , timeWindowMs], }) => { - const pollOffset = 5_000; - const pollInterval = 10_000; - const now = Date.now(); - const start = now - pollInterval * 5 - pollOffset; // Last minute + 2+ data points - const end = now - pollOffset; // Metrics have a minimum a 5 second latency based on poll interval - + const lookbackMs = timeWindowMs || 15 * 60 * 1000; // Default to 15 minutes + // Calculate interval to target approximately 60 datapoints + const targetDatapoints = 60; + const dataInterval = Math.max(15_000, Math.floor(lookbackMs / targetDatapoints)); // Minimum 15 seconds const response = await rivetClient.actors.metrics.get( actorId, { project, environment, - start, - end, - interval: pollInterval, + start: now - lookbackMs, + end: now, + interval: dataInterval, }, { abortSignal }, ); @@ -313,21 +318,21 @@ export const actorMetricsQueryOptions = ( response.metricNames.forEach((metricName, index) => { const metricValues = response.metricValues[index]; const attributes = response.metricAttributes[index] || {}; - + // Create the metric key based on the metric name and attributes let metricKey = metricName; - + // Handle specific attribute mappings to match UI expectations if (attributes.failure_type && attributes.scope) { metricKey = `memory_failures_${attributes.failure_type}_${attributes.scope}`; } else if (attributes.tcp_state) { - if (metricName.includes('tcp6')) { + if (metricName.includes("tcp6")) { metricKey = `network_tcp6_usage_${attributes.tcp_state}`; } else { metricKey = `network_tcp_usage_${attributes.tcp_state}`; } } else if (attributes.udp_state) { - if (metricName.includes('udp6')) { + if (metricName.includes("udp6")) { metricKey = `network_udp6_usage_${attributes.udp_state}`; } else { metricKey = `network_udp_usage_${attributes.udp_state}`; @@ -336,20 +341,26 @@ export const actorMetricsQueryOptions = ( metricKey = `tasks_state_${attributes.state}`; } else if (attributes.interface) { // Handle network interface attributes - const baseMetric = metricName.replace(/^container_/, ''); + const baseMetric = metricName.replace( + /^container_/, + "", + ); metricKey = `${baseMetric}_${attributes.interface}`; } else if (attributes.device) { // Handle filesystem device attributes - const baseMetric = metricName.replace(/^container_/, ''); + const baseMetric = metricName.replace( + /^container_/, + "", + ); metricKey = `${baseMetric}_${attributes.device}`; } else { // Remove "container_" prefix to match UI expectations - metricKey = metricName.replace(/^container_/, ''); + metricKey = metricName.replace(/^container_/, ""); } - + // Store raw time series data for rate calculations rawData[metricKey] = metricValues || []; - + if (metricValues && metricValues.length > 0) { // Get the latest non-zero value (last value is often 0) let value = null; @@ -373,7 +384,7 @@ export const actorMetricsQueryOptions = ( return { metrics, rawData, - interval: pollInterval, + interval: dataInterval, }; }, }); @@ -684,36 +695,73 @@ export const logsAggregatedQueryOptions = ({ client, queryKey: [_, project, __, environment, ___, search], }) => { - const actors = await client.fetchInfiniteQuery({ - ...projectActorsQueryOptions({ - projectNameId: project, - environmentNameId: environment, - includeDestroyed: true, - tags: {}, - }), - pages: 10, - }); - - const allActors = actors.pages.flatMap((page) => page.actors || []); - - const actorsMap = new Map(); - for (const actor of allActors) { - actorsMap.set(actor.id, actor); + let query = undefined; + if (search?.text) { + if (search.enableRegex) { + query = { + string_match_regex: { + property: "message", + pattern: search.enableRegex, + case_insensitive: !search.enableRegex, + }, + }; + } else { + query = { + string_contains: { + property: "message", + pattern: search.enableRegex, + case_insensitive: !search.enableRegex, + }, + }; + } } const logs = await rivetClient.actors.logs.get( { - stream: "all", project, environment, - searchText: search?.text, - searchCaseSensitive: search?.caseSensitive, - searchEnableRegex: search?.enableRegex, - actorIdsJson: JSON.stringify(allActors.map((a) => a.id)), + queryJson: query ? JSON.stringify(query) : undefined, + }, + { + abortSignal, }, - { abortSignal }, ); + // Fetch all actors that appear in the logs + const actorsMap = new Map(); + + // Get unique actor IDs from logs + const uniqueActorIds = [...new Set(logs.actorIds)]; + + // Fetch actor details in parallel using TanStack Query for caching + const actorPromises = uniqueActorIds.map(async (actorId) => { + try { + // Use fetchQuery to leverage TanStack Query's caching + const data = await client.fetchQuery({ + ...actorQueryOptions({ + projectNameId: project, + environmentNameId: environment, + actorId, + }), + staleTime: 60_000, + }); + return data; + } catch (error) { + // If actor not found or error, return null + console.warn(`Failed to fetch actor ${actorId}:`, error); + return null; + } + }); + + const actors = await Promise.all(actorPromises); + + // Populate the actors map + for (const actor of actors) { + if (actor) { + actorsMap.set(actor.actor.id, actor.actor); + } + } + const parsed = logs.lines.map((line, idx) => { const actorIdx = logs.actorIndices[idx]; const actorId = logs.actorIds[actorIdx]; diff --git a/frontend/apps/hub/src/domains/project/queries/billing/mutations.ts b/frontend/apps/hub/src/domains/project/queries/billing/mutations.ts index b33e2347d7..77ffb82fc5 100644 --- a/frontend/apps/hub/src/domains/project/queries/billing/mutations.ts +++ b/frontend/apps/hub/src/domains/project/queries/billing/mutations.ts @@ -25,4 +25,4 @@ export const useUpdateProjectBillingMutation = ({ onSuccess?.(); }, }); -}; \ No newline at end of file +}; diff --git a/frontend/apps/hub/src/domains/project/queries/billing/query-options.ts b/frontend/apps/hub/src/domains/project/queries/billing/query-options.ts index 071fe6d544..125ac2ebc9 100644 --- a/frontend/apps/hub/src/domains/project/queries/billing/query-options.ts +++ b/frontend/apps/hub/src/domains/project/queries/billing/query-options.ts @@ -74,7 +74,6 @@ export const projectBillingQueryOptions = ( }); }; - export const portalBillingSessionQueryOptions = ( groupId: string, intent: RivetEe.ee.cloud.groups.billing.CreateStripePortalSessionRequest["intent"], @@ -89,4 +88,4 @@ export const portalBillingSessionQueryOptions = ( { intent }, { abortSignal: signal }, ), - }); \ No newline at end of file + }); diff --git a/frontend/apps/hub/src/domains/project/queries/environment/query-options.ts b/frontend/apps/hub/src/domains/project/queries/environment/query-options.ts index cd18cb340a..5a765f6510 100644 --- a/frontend/apps/hub/src/domains/project/queries/environment/query-options.ts +++ b/frontend/apps/hub/src/domains/project/queries/environment/query-options.ts @@ -1,9 +1,9 @@ import { rivetClient } from "@/queries/global"; import type { Rivet } from "@rivet-gg/api-full"; import { queryOptions } from "@tanstack/react-query"; +import stripAnsi from "strip-ansi"; import { getLiveLobbyStatus, getLobbyStatus } from "../../data/lobby-status"; import { projectQueryOptions } from "../query-options"; -import stripAnsi from "strip-ansi"; export const projectEnvironmentsQueryOptions = (projectId: string) => { return queryOptions({ @@ -252,7 +252,9 @@ export const projectEnvironmentLogsLobbyLogsQueryOptions = ( ); return { ...response, - lines: response.lines.map((line) => stripAnsi(window.atob(line))), + lines: response.lines.map((line) => + stripAnsi(window.atob(line)), + ), }; }, }); diff --git a/frontend/apps/hub/src/domains/user/queries/type.ts b/frontend/apps/hub/src/domains/user/queries/type.ts index 7396cb7765..a66fa2627c 100644 --- a/frontend/apps/hub/src/domains/user/queries/type.ts +++ b/frontend/apps/hub/src/domains/user/queries/type.ts @@ -15,7 +15,7 @@ export const ChangelogItem = z.object({ twitter: z.string().optional(), github: z.string().optional(), bluesky: z.string().optional(), - }) + }), }), ), }); diff --git a/frontend/apps/hub/src/hooks/use-dialog.tsx b/frontend/apps/hub/src/hooks/use-dialog.tsx index da912182d8..a1d7dc307b 100644 --- a/frontend/apps/hub/src/hooks/use-dialog.tsx +++ b/frontend/apps/hub/src/hooks/use-dialog.tsx @@ -9,8 +9,8 @@ import { import { type ComponentProps, type ComponentType, - lazy, Suspense, + lazy, useCallback, useMemo, useState, diff --git a/frontend/apps/hub/src/layouts/root.tsx b/frontend/apps/hub/src/layouts/root.tsx index e606426195..eeb6cfc6ab 100644 --- a/frontend/apps/hub/src/layouts/root.tsx +++ b/frontend/apps/hub/src/layouts/root.tsx @@ -1,4 +1,3 @@ -import { CommandPanel } from "@/components/command-panel"; import { NavItem } from "@/components/header/nav-item"; import { usePageLayout } from "@/lib/compute-page-layout"; import { publicUrl } from "@/lib/utils"; diff --git a/frontend/apps/hub/src/lib/guards.tsx b/frontend/apps/hub/src/lib/guards.tsx index eb7567f802..ae7db5afc0 100644 --- a/frontend/apps/hub/src/lib/guards.tsx +++ b/frontend/apps/hub/src/lib/guards.tsx @@ -11,7 +11,6 @@ import { } from "@/domains/project/queries"; import { type QueryClient, - QueryErrorResetBoundary, useSuspenseQueries, useSuspenseQuery, } from "@tanstack/react-query"; diff --git a/frontend/apps/hub/src/lib/utils.ts b/frontend/apps/hub/src/lib/utils.ts index de44e6cb8a..fcf232f022 100644 --- a/frontend/apps/hub/src/lib/utils.ts +++ b/frontend/apps/hub/src/lib/utils.ts @@ -1,5 +1,5 @@ -import { RivetError } from "@rivet-gg/api-full"; import { RivetError as RivetEeError } from "@rivet-gg/api-ee"; +import { RivetError } from "@rivet-gg/api-full"; import { type ErrorComponentProps, useRouter } from "@tanstack/react-router"; import { useEffect } from "react"; import { z } from "zod"; diff --git a/frontend/apps/hub/src/main.tsx b/frontend/apps/hub/src/main.tsx index 2538cfefc3..7c8684fb30 100644 --- a/frontend/apps/hub/src/main.tsx +++ b/frontend/apps/hub/src/main.tsx @@ -2,8 +2,8 @@ import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import { App, router } from "./app"; import "./index.css"; -import { rivetClient } from "./queries/global"; import { initThirdPartyProviders } from "@rivet-gg/components"; +import { rivetClient } from "./queries/global"; initThirdPartyProviders(router, import.meta.env.DEV); diff --git a/frontend/apps/hub/src/queries/global.ts b/frontend/apps/hub/src/queries/global.ts index 3a107eb231..b6402fb5b3 100644 --- a/frontend/apps/hub/src/queries/global.ts +++ b/frontend/apps/hub/src/queries/global.ts @@ -1,7 +1,7 @@ import { ls } from "@/lib/ls"; import { isRivetError } from "@/lib/utils"; -import { RivetClient } from "@rivet-gg/api-full"; import { RivetClient as RivetEeClient } from "@rivet-gg/api-ee"; +import { RivetClient } from "@rivet-gg/api-full"; import { type APIResponse, type Fetcher, diff --git a/frontend/apps/hub/src/queries/watch.ts b/frontend/apps/hub/src/queries/watch.ts index 3219c29aad..491ea9a8a3 100644 --- a/frontend/apps/hub/src/queries/watch.ts +++ b/frontend/apps/hub/src/queries/watch.ts @@ -72,7 +72,7 @@ async function stopWatching(query: Query) { .getQueryCache() .find({ queryKey: [...query.queryKey, "watch"] }); if (watchQuery) { - watchQuery.cancel({silent: true}); + watchQuery.cancel({ silent: true }); queryClient.getQueryCache().remove(watchQuery); watchQuery.destroy(); } diff --git a/frontend/apps/hub/src/routes/_authenticated.tsx b/frontend/apps/hub/src/routes/_authenticated.tsx index f65336dcff..7ea528e23c 100644 --- a/frontend/apps/hub/src/routes/_authenticated.tsx +++ b/frontend/apps/hub/src/routes/_authenticated.tsx @@ -1,7 +1,8 @@ import { useAuth } from "@/domains/auth/contexts/auth"; +import { LoginView } from "@/domains/auth/views/login-view/login-view"; import { useDialog } from "@/hooks/use-dialog"; -import { FullscreenLoading } from "@rivet-gg/components"; import * as Layout from "@/layouts/page-centered"; +import { FullscreenLoading } from "@rivet-gg/components"; import { Outlet, createFileRoute, @@ -10,7 +11,6 @@ import { } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; -import { LoginView } from "@/domains/auth/views/login-view/login-view"; function Authenticated() { const auth = useAuth(); diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/billing.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/billing.tsx index 69d6d84039..eedd394704 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/billing.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/billing.tsx @@ -1,8 +1,8 @@ import { createFileRoute } from "@tanstack/react-router"; +import { useProject } from "@/domains/project/data/project-context"; import { BillingView } from "@/domains/project/views/billing-view"; import { guardEnterprise } from "@/lib/guards"; -import { useProject } from "@/domains/project/data/project-context"; function ProjectBillingRoute() { const { gameId: projectId } = useProject(); diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actor-versions.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actor-versions.tsx index a05486de20..32e924bee7 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actor-versions.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actor-versions.tsx @@ -29,8 +29,8 @@ import { ActorTags } from "@rivet-gg/components/actors"; import { Icon, faCheckCircle, faInfoCircle, faRefresh } from "@rivet-gg/icons"; import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { - createFileRoute, type ErrorComponentProps, + createFileRoute, } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actors.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actors.tsx index 463d07b72e..0365e5bb59 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actors.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/actors.tsx @@ -1,30 +1,30 @@ +import { ErrorComponent } from "@/components/error-component"; +import { ActorsActorDetailsWrapper } from "@/domains/project/components/actors/actors-actor-details-wrapper"; +import { ActorsProvider } from "@/domains/project/components/actors/actors-provider"; +import { useEnvironment } from "@/domains/project/data/environment-context"; +import { useProject } from "@/domains/project/data/project-context"; +import * as Layout from "@/domains/project/layouts/servers-layout"; +import { actorBuildsCountQueryOptions } from "@/domains/project/queries"; +import { useDialog } from "@/hooks/use-dialog"; import { ActorFeature, ActorNotFound, - ActorsActorDetails, ActorsActorEmptyDetails, ActorsListFiltersSchema, ActorsListPreview, currentActorAtom, pickActorListFilters, } from "@rivet-gg/components/actors"; -import { useEnvironment } from "@/domains/project/data/environment-context"; -import { useProject } from "@/domains/project/data/project-context"; -import * as Layout from "@/domains/project/layouts/servers-layout"; -import { actorBuildsCountQueryOptions } from "@/domains/project/queries"; +import { GettingStarted } from "@rivet-gg/components/actors"; import { useSuspenseQuery } from "@tanstack/react-query"; import { - createFileRoute, type ErrorComponentProps, + createFileRoute, useRouter, } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; -import { z } from "zod"; -import { GettingStarted } from "@rivet-gg/components/actors"; import { useAtomValue } from "jotai"; -import { useDialog } from "@/hooks/use-dialog"; -import { ErrorComponent } from "@/components/error-component"; -import { ActorsProvider } from "@/domains/project/components/actors/actors-provider"; +import { z } from "zod"; function Actor() { const navigate = Route.useNavigate(); const { tab } = Route.useSearch(); @@ -46,7 +46,7 @@ function Actor() { } return ( - { diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/containers.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/containers.tsx index a9f28f56be..70234a06ed 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/containers.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/containers.tsx @@ -1,34 +1,34 @@ +import { ErrorComponent } from "@/components/error-component"; +import { ActorsActorDetailsWrapper } from "@/domains/project/components/actors/actors-actor-details-wrapper"; +import { ActorsProvider } from "@/domains/project/components/actors/actors-provider"; +import { useEnvironment } from "@/domains/project/data/environment-context"; +import { useProject } from "@/domains/project/data/project-context"; +import * as Layout from "@/domains/project/layouts/servers-layout"; +import { actorBuildsCountQueryOptions } from "@/domains/project/queries"; +import { useDialog } from "@/hooks/use-dialog"; +import type { Rivet } from "@rivet-gg/api-full"; +import { toRecord } from "@rivet-gg/components"; import { - type Actor as StateActor, ActorFeature, ActorNotFound, - ActorsActorDetails, ActorsActorEmptyDetails, ActorsListFiltersSchema, ActorsListPreview, ActorsViewContext, + type Actor as StateActor, currentActorAtom, pickActorListFilters, } from "@rivet-gg/components/actors"; -import { useEnvironment } from "@/domains/project/data/environment-context"; -import { useProject } from "@/domains/project/data/project-context"; -import * as Layout from "@/domains/project/layouts/servers-layout"; -import { actorBuildsCountQueryOptions } from "@/domains/project/queries"; +import { GettingStarted } from "@rivet-gg/components/actors"; import { useSuspenseQuery } from "@tanstack/react-query"; import { - createFileRoute, type ErrorComponentProps, + createFileRoute, useRouter, } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; -import { z } from "zod"; -import { GettingStarted } from "@rivet-gg/components/actors"; import { useAtomValue } from "jotai"; -import { useDialog } from "@/hooks/use-dialog"; -import { ErrorComponent } from "@/components/error-component"; -import { ActorsProvider } from "@/domains/project/components/actors/actors-provider"; -import type { Rivet } from "@rivet-gg/api-full"; -import { toRecord } from "@rivet-gg/components"; +import { z } from "zod"; function Actor() { const navigate = Route.useNavigate(); @@ -49,7 +49,7 @@ function Actor() { } return ( - { diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/functions.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/functions.tsx index 975e7c46e7..6fa9bf1fcb 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/functions.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/functions.tsx @@ -1,42 +1,42 @@ +import { ErrorComponent } from "@/components/error-component"; +import { useEnvironment } from "@/domains/project/data/environment-context"; +import { useProject } from "@/domains/project/data/project-context"; import * as Layout from "@/domains/project/layouts/servers-layout"; import { projectActorsQueryOptions, routesQueryOptions, useDeleteRouteMutation, } from "@/domains/project/queries"; +import { useDialog } from "@/hooks/use-dialog"; +import { + Button, + DiscreteCopyButton, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + H1, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Text, +} from "@rivet-gg/components"; +import { Icon, faEllipsisH, faPlus } from "@rivet-gg/icons"; import { useInfiniteQuery, usePrefetchInfiniteQuery, useSuspenseQuery, } from "@tanstack/react-query"; import { - createFileRoute, type ErrorComponentProps, Link, + createFileRoute, } from "@tanstack/react-router"; import { zodValidator } from "@tanstack/zod-adapter"; import { z } from "zod"; -import { ErrorComponent } from "@/components/error-component"; -import { - Button, - Table, - TableHeader, - TableRow, - TableHead, - TableBody, - TableCell, - DiscreteCopyButton, - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - Text, - H1, -} from "@rivet-gg/components"; -import { Icon, faPlus, faEllipsisH } from "@rivet-gg/icons"; -import { useEnvironment } from "@/domains/project/data/environment-context"; -import { useProject } from "@/domains/project/data/project-context"; -import { useDialog } from "@/hooks/use-dialog"; function ProjectFunctionsRoute() { const { projectNameId, environmentNameId } = Route.useParams(); diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/logs.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/logs.tsx index fab7ae06dd..1de076ee58 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/logs.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId._v2/logs.tsx @@ -1,3 +1,4 @@ +import { ErrorComponent } from "@/components/error-component"; import * as Layout from "@/domains/project/layouts/servers-layout"; import { type FunctionInvoke, @@ -6,45 +7,37 @@ import { routesQueryOptions, } from "@/domains/project/queries"; import { - cn, - VirtualScrollArea, - WithTooltip, - FilterCreator, Button, - toRecord, - ToggleGroup, - ToggleGroupItem, - ShimmerLine, - type FilterDefinitions, - type OnFiltersChange, - FilterValueSchema, - FilterOp, - createFiltersPicker, - createFiltersSchema, - OptionsProviderProps, - FilterValue, Checkbox, CommandGroup, CommandItem, - SmallText, + FilterCreator, + type FilterDefinitions, + FilterOp, + type FilterValue, LiveBadge, + type OnFiltersChange, + type OptionsProviderProps, + ShimmerLine, + SmallText, + ToggleGroup, + ToggleGroupItem, + VirtualScrollArea, + WithTooltip, + cn, + createFiltersPicker, + createFiltersSchema, + toRecord, } from "@rivet-gg/components"; import { - useInfiniteQuery, - usePrefetchInfiniteQuery, - useQuery, -} from "@tanstack/react-query"; -import { - createFileRoute, - Link, - type ErrorComponentProps, -} from "@tanstack/react-router"; -import { zodValidator } from "@tanstack/zod-adapter"; -import { format } from "date-fns"; -import { forwardRef, useCallback, useMemo, useRef, useState } from "react"; -import { z } from "zod"; -import type { Virtualizer } from "@tanstack/react-virtual"; + ActorObjectInspector, + ActorRegion, + ConsoleMessageVariantIcon, + getConsoleMessageVariant, + useActorsView, +} from "@rivet-gg/components/actors"; import { + Icon, faAngleDown, faAngleUp, faFontCase, @@ -52,18 +45,23 @@ import { faRegex, faSignal, faSwap, - Icon, } from "@rivet-gg/icons"; import { - ActorObjectInspector, - ActorRegion, - ConsoleMessageVariantIcon, - getConsoleMessageVariant, - useActorsView, -} from "@rivet-gg/components/actors"; -import { ErrorComponent } from "@/components/error-component"; + useInfiniteQuery, + usePrefetchInfiniteQuery, + useQuery, +} from "@tanstack/react-query"; +import { + type ErrorComponentProps, + Link, + createFileRoute, +} from "@tanstack/react-router"; +import type { Virtualizer } from "@tanstack/react-virtual"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { format } from "date-fns"; +import { forwardRef, useCallback, useRef, useState } from "react"; import { useDebounceCallback } from "usehooks-ts"; -import { actors } from "@rivet-gg/api-full/serialization"; +import { z } from "zod"; function ProjectFunctionsRoute() { const { environmentNameId, projectNameId } = Route.useParams(); diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/_v2.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/_v2.tsx index c3661a4ae9..3a4be9a5cc 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/_v2.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/_v2.tsx @@ -1,7 +1,8 @@ -import { CommandPanel } from "@/components/command-panel"; import { Button, cn } from "@rivet-gg/components"; import { ActorsLayout, useActorsLayout } from "@rivet-gg/components/actors"; import { + Icon, + type IconProp, faActorsBorderless, faBarsStaggered, faCodeBranch, @@ -9,11 +10,9 @@ import { faFunction, faServer, faSidebar, - Icon, - type IconProp, } from "@rivet-gg/icons"; -import { createFileRoute, Link, Outlet } from "@tanstack/react-router"; -import { AnimatePresence, motion } from "framer-motion"; +import { Link, Outlet, createFileRoute } from "@tanstack/react-router"; +import { motion } from "framer-motion"; const SIDEBAR: { label: string; diff --git a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/tokens.tsx b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/tokens.tsx index ae06047d74..1cdb2edc64 100644 --- a/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/tokens.tsx +++ b/frontend/apps/hub/src/routes/_authenticated/_layout/projects/$projectNameId/environments/$environmentNameId/tokens.tsx @@ -1,4 +1,3 @@ -import { BillingPlans } from "@/domains/project/components/billing/billing-plans"; import { useEnvironment } from "@/domains/project/data/environment-context"; import { useProject } from "@/domains/project/data/project-context"; import * as Layout from "@/domains/project/layouts/project-layout"; @@ -45,10 +44,7 @@ function EnvironmentTokensRoute() { function DevelopmentTokenCard() { const environment = useEnvironment(); return ( - + Development tokens are built to let you develop your project on your local machine with access to production APIs. diff --git a/frontend/apps/studio/src/app.tsx b/frontend/apps/studio/src/app.tsx index 248da09a8d..29c6f48791 100644 --- a/frontend/apps/studio/src/app.tsx +++ b/frontend/apps/studio/src/app.tsx @@ -6,18 +6,18 @@ import { TooltipProvider, getConfig, } from "@rivet-gg/components"; -import { PageLayout } from "@rivet-gg/components/layout"; -import * as Sentry from "@sentry/react"; -import { RouterProvider, createRouter } from "@tanstack/react-router"; -import { Suspense } from "react"; -import { routeTree } from "./routeTree.gen"; -import { withAtomEffect } from "jotai-effect"; import { actorFiltersAtom, currentActorIdAtom, pickActorListFilters, } from "@rivet-gg/components/actors"; +import { PageLayout } from "@rivet-gg/components/layout"; +import * as Sentry from "@sentry/react"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; import { useAtom } from "jotai"; +import { withAtomEffect } from "jotai-effect"; +import { Suspense } from "react"; +import { routeTree } from "./routeTree.gen"; declare module "@tanstack/react-router" { interface Register { diff --git a/frontend/apps/studio/src/components/layout.tsx b/frontend/apps/studio/src/components/layout.tsx index 14f6d40ca4..8fbf3fe559 100644 --- a/frontend/apps/studio/src/components/layout.tsx +++ b/frontend/apps/studio/src/components/layout.tsx @@ -1,7 +1,7 @@ import { connectionStateAtom } from "@/stores/manager"; -import { cn, DocsSheet, ShimmerLine } from "@rivet-gg/components"; -import { Header as RivetHeader, NavItem } from "@rivet-gg/components/header"; -import { faGithub, Icon } from "@rivet-gg/icons"; +import { DocsSheet, ShimmerLine, cn } from "@rivet-gg/components"; +import { NavItem, Header as RivetHeader } from "@rivet-gg/components/header"; +import { Icon, faGithub } from "@rivet-gg/icons"; import { Link } from "@tanstack/react-router"; import { useAtomValue } from "jotai"; import type { PropsWithChildren, ReactNode } from "react"; diff --git a/frontend/apps/studio/src/queries/global.ts b/frontend/apps/studio/src/queries/global.ts index 27336afba4..96ea5f93d5 100644 --- a/frontend/apps/studio/src/queries/global.ts +++ b/frontend/apps/studio/src/queries/global.ts @@ -1,12 +1,7 @@ -import { getConfig, timing, toast } from "@rivet-gg/components"; +import { toast } from "@rivet-gg/components"; import { broadcastQueryClient } from "@tanstack/query-broadcast-client-experimental"; import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; -import { - MutationCache, - MutationObserver, - QueryCache, - QueryClient, -} from "@tanstack/react-query"; +import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; import superjson from "superjson"; const queryCache = new QueryCache(); diff --git a/frontend/apps/studio/src/routes/__root.tsx b/frontend/apps/studio/src/routes/__root.tsx index 791e4a567d..8e36fa8ba6 100644 --- a/frontend/apps/studio/src/routes/__root.tsx +++ b/frontend/apps/studio/src/routes/__root.tsx @@ -1,13 +1,13 @@ import { FEEDBACK_FORM_ID, FullscreenLoading } from "@rivet-gg/components"; +import * as Layout from "@/components/layout"; +import { useDialog } from "@rivet-gg/components/actors"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import { zodValidator } from "@tanstack/zod-adapter"; import { usePostHog } from "posthog-js/react"; -import { z } from "zod"; -import * as Layout from "@/components/layout"; import { Suspense } from "react"; -import { useDialog } from "@rivet-gg/components/actors"; +import { z } from "zod"; function Modals() { const search = Route.useSearch(); diff --git a/frontend/apps/studio/src/routes/_layout.tsx b/frontend/apps/studio/src/routes/_layout.tsx index d5f7f864d0..d40b51ba1e 100644 --- a/frontend/apps/studio/src/routes/_layout.tsx +++ b/frontend/apps/studio/src/routes/_layout.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { Outlet, createFileRoute } from "@tanstack/react-router"; export const Route = createFileRoute("/_layout")({ component: RouteComponent, diff --git a/frontend/apps/studio/src/routes/_layout/index.tsx b/frontend/apps/studio/src/routes/_layout/index.tsx index 696d49018e..d606a91a8a 100644 --- a/frontend/apps/studio/src/routes/_layout/index.tsx +++ b/frontend/apps/studio/src/routes/_layout/index.tsx @@ -25,10 +25,9 @@ import { } from "@rivet-gg/components/actors"; import { Icon, - faChrome, faBrave, + faChrome, faReact, - faRust, faSafari, faTs, } from "@rivet-gg/icons"; @@ -40,14 +39,14 @@ import { useEffect } from "react"; import { z } from "zod"; // @ts-expect-error types are missing import devNpm, { source as devNpmSource } from "../../content/dev-npm.sh?shiki"; -import devYarn, { - source as devYarnSource, - // @ts-expect-error types are missing -} from "../../content/dev-yarn.sh?shiki"; import devPnpm, { source as devPnpmSource, // @ts-expect-error types are missing } from "../../content/dev-pnpm.sh?shiki"; +import devYarn, { + source as devYarnSource, + // @ts-expect-error types are missing +} from "../../content/dev-yarn.sh?shiki"; import devBun, { source as devBunSource, diff --git a/frontend/apps/studio/src/stores/manager.tsx b/frontend/apps/studio/src/stores/manager.tsx index c355e82548..1a5d863b64 100644 --- a/frontend/apps/studio/src/stores/manager.tsx +++ b/frontend/apps/studio/src/stores/manager.tsx @@ -1,20 +1,20 @@ -import { - ToClientSchema, - type ToClient, - type ToServer, - type Actor as InspectorActor, -} from "actor-core/inspector/protocol/manager"; import { toast } from "@rivet-gg/components"; -import { atom } from "jotai"; -import { atomEffect } from "jotai-effect"; import { type Actor, - actorBuildsAtom, ActorFeature, + actorBuildsAtom, actorsAtom, createActorAtom, } from "@rivet-gg/components/actors"; import { createClient } from "actor-core/client"; +import { + type Actor as InspectorActor, + type ToClient, + ToClientSchema, + type ToServer, +} from "actor-core/inspector/protocol/manager"; +import { atom } from "jotai"; +import { atomEffect } from "jotai-effect"; const createConnection = ({ onMessage, diff --git a/frontend/apps/studio/vite.config.ts b/frontend/apps/studio/vite.config.ts index 75c06d22d0..b6b701fd90 100644 --- a/frontend/apps/studio/vite.config.ts +++ b/frontend/apps/studio/vite.config.ts @@ -1,11 +1,11 @@ import * as crypto from "node:crypto"; import path from "node:path"; +// @ts-expect-error types are missing +import { viteShikiTransformer } from "@rivet-gg/components/vite"; import { sentryVitePlugin } from "@sentry/vite-plugin"; import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import { visualizer } from "rollup-plugin-visualizer"; -// @ts-expect-error types are missing -import { viteShikiTransformer } from "@rivet-gg/components/vite"; import { defineConfig } from "vite"; // @ts-expect-error types are missing import vitePluginFaviconsInject from "vite-plugin-favicons-inject"; diff --git a/frontend/packages/cli/cli.ts b/frontend/packages/cli/cli.ts index e898e980cd..04277d1244 100644 --- a/frontend/packages/cli/cli.ts +++ b/frontend/packages/cli/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; -import { join } from "node:path"; import os from "node:os"; +import { join } from "node:path"; const platform = os.platform(); @@ -14,9 +14,13 @@ function computeTargetFilename() { } try { - execFileSync(join(__dirname, computeTargetFilename()), process.argv.slice(2), { - stdio: "inherit", - }); + execFileSync( + join(__dirname, computeTargetFilename()), + process.argv.slice(2), + { + stdio: "inherit", + }, + ); } catch (error) { if (error.status) { process.exit(error.status); diff --git a/frontend/packages/cli/package.json b/frontend/packages/cli/package.json index 6c0fdc2a26..43ad40ea5f 100644 --- a/frontend/packages/cli/package.json +++ b/frontend/packages/cli/package.json @@ -11,11 +11,7 @@ "rivet-cli": "dist/cli.js" }, "license": "Apache-2.0", - "files": [ - "dist/cli.js", - "dist/postinstall.js", - "package.json" - ], + "files": ["dist/cli.js", "dist/postinstall.js", "package.json"], "sideEffects": false, "preferGlobal": true, "preferUnplugged": true, diff --git a/frontend/packages/cli/postinstall.ts b/frontend/packages/cli/postinstall.ts index b20278d75d..5a72f6cc65 100644 --- a/frontend/packages/cli/postinstall.ts +++ b/frontend/packages/cli/postinstall.ts @@ -73,11 +73,11 @@ function maybeOptimizePackage(binPath: string, toPath: string): void { async function download(version: string) { const binaryFilename = computeBinaryFilename(); const url = artifactUrl(version, binaryFilename); - + console.log(`Downloading Rivet CLI ${version} for ${platform}-${arch}`); console.log(`Binary: ${binaryFilename}`); console.log(`URL: ${url}`); - + const response = await fetch(url); if (!response.ok) { @@ -112,7 +112,7 @@ async function download(version: string) { async function main() { console.log("Starting Rivet CLI installation..."); - + try { await fs.promises.rm(RIVET_CLI_BINARY_PATH); console.log("Cleaned up existing binary"); diff --git a/frontend/packages/components/package.json b/frontend/packages/components/package.json index 47559e07ee..400f1dfc11 100644 --- a/frontend/packages/components/package.json +++ b/frontend/packages/components/package.json @@ -3,11 +3,7 @@ "private": true, "version": "25.5.2", "type": "module", - "files": [ - "dist", - "src", - "public" - ], + "files": ["dist", "src", "public"], "main": "./dist/index.cjs", "module": "./dist/index.js", "sideEffects": false, diff --git a/frontend/packages/components/src/actors/actor-build.tsx b/frontend/packages/components/src/actors/actor-build.tsx index 9a9470ab1d..8416f0dd4e 100644 --- a/frontend/packages/components/src/actors/actor-build.tsx +++ b/frontend/packages/components/src/actors/actor-build.tsx @@ -1,10 +1,10 @@ -import { Flex, Dt, Dd, Dl, DiscreteCopyButton } from "@rivet-gg/components"; -import { ActorTags } from "./actor-tags"; +import { Dd, DiscreteCopyButton, Dl, Dt, Flex } from "@rivet-gg/components"; import { formatISO } from "date-fns"; -import { actorBuildsAtom, type Actor, type ActorAtom } from "./actor-context"; -import { selectAtom } from "jotai/utils"; import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; import { useCallback } from "react"; +import { type Actor, type ActorAtom, actorBuildsAtom } from "./actor-context"; +import { ActorTags } from "./actor-tags"; const buildIdSelector = (a: Actor) => a.runtime?.build; diff --git a/frontend/packages/components/src/actors/actor-config-tab.tsx b/frontend/packages/components/src/actors/actor-config-tab.tsx index 0ffd054c43..2a05406888 100644 --- a/frontend/packages/components/src/actors/actor-config-tab.tsx +++ b/frontend/packages/components/src/actors/actor-config-tab.tsx @@ -1,9 +1,9 @@ import { Button, DocsSheet, ScrollArea } from "@rivet-gg/components"; import { Icon, faBooks } from "@rivet-gg/icons"; +import type { ActorAtom } from "./actor-context"; import { ActorGeneral } from "./actor-general"; import { ActorNetwork } from "./actor-network"; import { ActorRuntime } from "./actor-runtime"; -import type { ActorAtom } from "./actor-context"; interface ActorConfigTabProps { actor: ActorAtom; diff --git a/frontend/packages/components/src/actors/actor-connections-tab.tsx b/frontend/packages/components/src/actors/actor-connections-tab.tsx index 4f526ef869..42be046a7c 100644 --- a/frontend/packages/components/src/actors/actor-connections-tab.tsx +++ b/frontend/packages/components/src/actors/actor-connections-tab.tsx @@ -1,12 +1,12 @@ import { LiveBadge, ScrollArea } from "@rivet-gg/components"; +import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; +import type { Actor, ActorAtom } from "./actor-context"; import { ActorObjectInspector } from "./console/actor-inspector"; import { useActorConnections, useActorWorkerStatus, } from "./worker/actor-worker-context"; -import type { Actor, ActorAtom } from "./actor-context"; -import { useAtomValue } from "jotai"; -import { selectAtom } from "jotai/utils"; const selector = (a: Actor) => a.destroyedAt; diff --git a/frontend/packages/components/src/actors/actor-context.tsx b/frontend/packages/components/src/actors/actor-context.tsx index 59e5c4ce99..66cb772a1c 100644 --- a/frontend/packages/components/src/actors/actor-context.tsx +++ b/frontend/packages/components/src/actors/actor-context.tsx @@ -1,10 +1,10 @@ +import type { Rivet } from "@rivet-gg/api"; +import { isAfter, isBefore } from "date-fns"; import { type Atom, atom } from "jotai"; import { atomFamily, splitAtom } from "jotai/utils"; -import type { Rivet } from "@rivet-gg/api"; import { toRecord } from "../lib/utils"; -import { ACTOR_FRAMEWORK_TAG_VALUE } from "./actor-tags"; import { FilterOp, type FilterValue } from "../ui/filters"; -import { isAfter, isBefore } from "date-fns"; +import { ACTOR_FRAMEWORK_TAG_VALUE } from "./actor-tags"; export enum ActorFeature { Logs = "logs", @@ -122,15 +122,12 @@ export const actorRegionsAtom = atom([ export const actorBuildsAtom = atom([]); -export const actorEnvironmentAtom = atom<{ projectNameId: string; environmentNameId: string } | null>(null); - -export type ExportLogsHandler = (params: { +export const actorEnvironmentAtom = atom<{ projectNameId: string; environmentNameId: string; - queryJson: string; -}) => Promise<{ url: string }>; +} | null>(null); -export const exportLogsHandlerAtom = atom(null); +export const actorMetricsTimeWindowAtom = atom(15 * 60 * 1000); // Default to 15 minutes export const actorsInternalFilterAtom = atom<{ fn: (actor: Actor) => boolean; diff --git a/frontend/packages/components/src/actors/actor-cpu-stats.tsx b/frontend/packages/components/src/actors/actor-cpu-stats.tsx index 9a0fbdaaaf..2cf694f339 100644 --- a/frontend/packages/components/src/actors/actor-cpu-stats.tsx +++ b/frontend/packages/components/src/actors/actor-cpu-stats.tsx @@ -1,19 +1,20 @@ import { format } from "date-fns"; import { useId } from "react"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import { timing } from "../lib/timing"; import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, } from "../ui/chart"; -import { timing } from "../lib/timing"; interface ActorCpuStatsProps { interval?: number; cpu: number[]; metricsAt: number; syncId?: string; + isRunning?: boolean; } const chartConfig = { @@ -28,16 +29,58 @@ export function ActorCpuStats({ cpu, metricsAt, syncId, + isRunning = true, }: ActorCpuStatsProps) { - const data = cpu.map((value, i) => ({ - x: `${(cpu.length - i) * -interval}`, - value: value / 100, - config: { - label: new Date( - metricsAt - (cpu.length - i) * timing.seconds(interval), - ), - }, - })); + // Filter out trailing zeros in the last 15 seconds only if actor is still running + let filteredCpu = [...cpu]; + if (isRunning) { + const secondsToCheck = 15; + const pointsToCheck = Math.ceil(secondsToCheck / interval); + + // Find the last non-zero value and cut off any zeros after it + for ( + let i = filteredCpu.length - 1; + i >= Math.max(0, filteredCpu.length - pointsToCheck); + i-- + ) { + if (filteredCpu[i] === 0) { + filteredCpu = filteredCpu.slice(0, i); + } else { + break; + } + } + } + + const data = filteredCpu.map((value, i) => { + let cpuPercent = 0; + + // Calculate CPU percentage using delta time between ticks + if (i > 0) { + const currentCpuTime = value; + const previousCpuTime = filteredCpu[i - 1]; + const deltaTime = interval; // seconds between measurements + + // CPU percentage = (cpu_time_delta / time_delta) * 100 + // This gives us the percentage of CPU time used in the interval + if (currentCpuTime >= previousCpuTime) { + cpuPercent = Math.min( + ((currentCpuTime - previousCpuTime) / deltaTime) * 100, + 100, + ); + } + } + + return { + x: `${(filteredCpu.length - i) * -interval}`, + value: cpuPercent / 100, // Convert to 0-1 range for chart + config: { + label: new Date( + metricsAt - + (filteredCpu.length - i) * timing.seconds(interval), + ), + }, + }; + }); const id = useId(); diff --git a/frontend/packages/components/src/actors/actor-download-logs-button.tsx b/frontend/packages/components/src/actors/actor-download-logs-button.tsx index 4cd5878474..a261529c32 100644 --- a/frontend/packages/components/src/actors/actor-download-logs-button.tsx +++ b/frontend/packages/components/src/actors/actor-download-logs-button.tsx @@ -1,89 +1,40 @@ import { Button, WithTooltip } from "@rivet-gg/components"; import { Icon, faSave } from "@rivet-gg/icons"; -import { type LogsTypeFilter } from "./actor-logs"; +import { useAtomValue } from "jotai"; import type { ActorAtom } from "./actor-context"; -import { actorEnvironmentAtom, exportLogsHandlerAtom } from "./actor-context"; -import { atom, useAtom, useAtomValue } from "jotai"; -import { useState } from "react"; - -const downloadLogsAtom = atom( - null, - async ( - get, - _set, - { - actorId, - typeFilter, - filter, - }: { - actorId: string; - typeFilter?: LogsTypeFilter; - filter?: string; - }, - ) => { - const environment = get(actorEnvironmentAtom); - const exportHandler = get(exportLogsHandlerAtom); - - if (!environment || !exportHandler) { - throw new Error("Environment or export handler not available"); - } - - // Build query JSON for the API - // Based on the GET logs endpoint usage, we need to build a query - const query: any = { - actorIds: [actorId], - }; - - // Add stream filter based on typeFilter - if (typeFilter === "output") { - query.stream = 0; // stdout - } else if (typeFilter === "errors") { - query.stream = 1; // stderr - } - - // Add text search if filter is provided - if (filter) { - query.searchText = filter; - } - - const result = await exportHandler({ - projectNameId: environment.projectNameId, - environmentNameId: environment.environmentNameId, - queryJson: JSON.stringify(query), - }); - - // Open the presigned URL in a new tab to download - window.open(result.url, "_blank"); - }, -); +import type { LogsTypeFilter } from "./actor-logs"; interface ActorDownloadLogsButtonProps { actor: ActorAtom; typeFilter?: LogsTypeFilter; filter?: string; + onExportLogs?: ( + actorId: string, + typeFilter?: string, + filter?: string, + ) => Promise; + isExporting?: boolean; } export function ActorDownloadLogsButton({ actor, typeFilter, filter, + onExportLogs, + isExporting = false, }: ActorDownloadLogsButtonProps) { - const [isDownloading, setIsDownloading] = useState(false); - const [, downloadLogs] = useAtom(downloadLogsAtom); const actorData = useAtomValue(actor); const handleDownload = async () => { + if (!onExportLogs) { + console.warn("No export handler provided"); + return; + } + try { - setIsDownloading(true); - await downloadLogs({ - actorId: actorData.id, - typeFilter, - filter, - }); + await onExportLogs(actorData.id, typeFilter, filter); } catch (error) { - console.error("Failed to download logs:", error); - } finally { - setIsDownloading(false); + console.error("Failed to export logs:", error); } }; @@ -97,11 +48,11 @@ export function ActorDownloadLogsButton({ aria-label="Export logs" size="icon-sm" onClick={handleDownload} - disabled={isDownloading} + disabled={isExporting || !onExportLogs} > } diff --git a/frontend/packages/components/src/actors/actor-general.tsx b/frontend/packages/components/src/actors/actor-general.tsx index 1720645dc0..b9269fb39e 100644 --- a/frontend/packages/components/src/actors/actor-general.tsx +++ b/frontend/packages/components/src/actors/actor-general.tsx @@ -1,11 +1,11 @@ import { Dd, DiscreteCopyButton, Dl, Dt, Flex, cn } from "@rivet-gg/components"; import { formatISO } from "date-fns"; -import { ActorRegion } from "./actor-region"; -import { ActorTags } from "./actor-tags"; -import type { Actor, ActorAtom } from "./actor-context"; +import equal from "fast-deep-equal"; import { useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; -import equal from "fast-deep-equal"; +import type { Actor, ActorAtom } from "./actor-context"; +import { ActorRegion } from "./actor-region"; +import { ActorTags } from "./actor-tags"; const selector = (a: Actor) => ({ id: a.id, diff --git a/frontend/packages/components/src/actors/actor-logs-tab.tsx b/frontend/packages/components/src/actors/actor-logs-tab.tsx index 1812effc34..7925e94cc0 100644 --- a/frontend/packages/components/src/actors/actor-logs-tab.tsx +++ b/frontend/packages/components/src/actors/actor-logs-tab.tsx @@ -1,15 +1,25 @@ import { LogsView, ToggleGroup, ToggleGroupItem } from "@rivet-gg/components"; import { startTransition, useState } from "react"; +import type { ActorAtom } from "./actor-context"; import { ActorDetailsSettingsButton } from "./actor-details-settings-button"; import { ActorDownloadLogsButton } from "./actor-download-logs-button"; import { ActorLogs, type LogsTypeFilter } from "./actor-logs"; -import type { ActorAtom } from "./actor-context"; interface ActorLogsTabProps { actor: ActorAtom; + onExportLogs?: ( + actorId: string, + typeFilter?: string, + filter?: string, + ) => Promise; + isExporting?: boolean; } -export function ActorLogsTab({ actor }: ActorLogsTabProps) { +export function ActorLogsTab({ + actor, + onExportLogs, + isExporting, +}: ActorLogsTabProps) { const [search, setSearch] = useState(""); const [logsFilter, setLogsFilter] = useState("all"); @@ -64,6 +74,8 @@ export function ActorLogsTab({ actor }: ActorLogsTabProps) { actor={actor} typeFilter={logsFilter} filter={search} + onExportLogs={onExportLogs} + isExporting={isExporting} /> diff --git a/frontend/packages/components/src/actors/actor-logs.tsx b/frontend/packages/components/src/actors/actor-logs.tsx index abbdef3b4a..11427cab70 100644 --- a/frontend/packages/components/src/actors/actor-logs.tsx +++ b/frontend/packages/components/src/actors/actor-logs.tsx @@ -1,12 +1,12 @@ import { ShimmerLine, VirtualScrollArea } from "@rivet-gg/components"; import type { Virtualizer } from "@tanstack/react-virtual"; +import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; import { memo, useCallback, useEffect, useRef } from "react"; import { useResizeObserver } from "usehooks-ts"; +import type { Actor, ActorAtom, Logs } from "./actor-context"; import { useActorDetailsSettings } from "./actor-details-settings"; import { ActorConsoleMessage } from "./console/actor-console-message"; -import type { Actor, ActorAtom, Logs } from "./actor-context"; -import { useAtomValue } from "jotai"; -import { selectAtom } from "jotai/utils"; export type LogsTypeFilter = "all" | "output" | "errors"; diff --git a/frontend/packages/components/src/actors/actor-memory-stats.tsx b/frontend/packages/components/src/actors/actor-memory-stats.tsx index 5c5ff5e893..f2af24d75d 100644 --- a/frontend/packages/components/src/actors/actor-memory-stats.tsx +++ b/frontend/packages/components/src/actors/actor-memory-stats.tsx @@ -2,13 +2,13 @@ import { format } from "date-fns"; import { filesize } from "filesize"; import { useId } from "react"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; +import { timing } from "../lib/timing"; import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, } from "../ui/chart"; -import { timing } from "../lib/timing"; interface ActorMemoryStatsProps { metricsAt: number; @@ -16,6 +16,7 @@ interface ActorMemoryStatsProps { allocatedMemory?: number; syncId?: string; interval?: number; + isRunning?: boolean; } const chartConfig = { @@ -31,18 +32,40 @@ export function ActorMemoryStats({ allocatedMemory, metricsAt, syncId, + isRunning = true, }: ActorMemoryStatsProps) { - const data = memory.map((value, i) => ({ - x: `${(memory.length - i) * -interval}`, + // Filter out trailing zeros in the last 15 seconds only if actor is still running + let filteredMemory = [...memory]; + if (isRunning) { + const secondsToCheck = 15; + const pointsToCheck = Math.ceil(secondsToCheck / interval); + + // Find the last non-zero value and cut off any zeros after it + for ( + let i = filteredMemory.length - 1; + i >= Math.max(0, filteredMemory.length - pointsToCheck); + i-- + ) { + if (filteredMemory[i] === 0) { + filteredMemory = filteredMemory.slice(0, i); + } else { + break; + } + } + } + + const data = filteredMemory.map((value, i) => ({ + x: `${(filteredMemory.length - i) * -interval}`, value, config: { label: new Date( - metricsAt - (memory.length - i) * timing.seconds(interval), + metricsAt - + (filteredMemory.length - i) * timing.seconds(interval), ), }, })); - const max = allocatedMemory || Math.max(...memory); + const max = allocatedMemory || Math.max(...filteredMemory); const id = useId(); diff --git a/frontend/packages/components/src/actors/actor-metrics-tab.tsx b/frontend/packages/components/src/actors/actor-metrics-tab.tsx index 227fdbac1d..edc11a6618 100644 --- a/frontend/packages/components/src/actors/actor-metrics-tab.tsx +++ b/frontend/packages/components/src/actors/actor-metrics-tab.tsx @@ -1,4 +1,4 @@ -import { Button, DocsSheet, ScrollArea } from "@rivet-gg/components"; +import { Button, ScrollArea } from "@rivet-gg/components"; import { Icon, faBooks } from "@rivet-gg/icons"; import { ActorMetrics } from "./actor-metrics"; import type { ActorAtom } from "./actor-context"; @@ -22,4 +22,4 @@ export function ActorMetricsTab(props: ActorMetricsTabProps) { ); -} +} \ No newline at end of file diff --git a/frontend/packages/components/src/actors/actor-metrics.tsx b/frontend/packages/components/src/actors/actor-metrics.tsx index 114c0ec776..947d1166dc 100644 --- a/frontend/packages/components/src/actors/actor-metrics.tsx +++ b/frontend/packages/components/src/actors/actor-metrics.tsx @@ -1,4 +1,4 @@ -import { useAtomValue } from "jotai"; +import { useAtomValue, useSetAtom } from "jotai"; import { selectAtom } from "jotai/utils"; import equal from "fast-deep-equal"; import { useState, useMemo } from "react"; @@ -8,24 +8,80 @@ import { ActorMemoryStats } from "./actor-memory-stats"; import { Dd, Dl, Dt } from "../ui/typography"; import { Button } from "../ui/button"; import { Flex } from "../ui/flex"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; +import { actorMetricsTimeWindowAtom, actorEnvironmentAtom } from "./actor-context"; +import { useQuery } from "@tanstack/react-query"; +import { actorMetricsQueryOptions } from "@/domains/project/queries/actors/query-options"; const selector = (a: Actor) => ({ metrics: a.metrics, status: a.status, + resources: a.resources, + id: a.id, }); +const timeWindowOptions = [ + { label: "5 minutes", value: "5m", milliseconds: 5 * 60 * 1000 }, + { label: "15 minutes", value: "15m", milliseconds: 15 * 60 * 1000 }, + { label: "30 minutes", value: "30m", milliseconds: 30 * 60 * 1000 }, + { label: "1 hour", value: "1h", milliseconds: 60 * 60 * 1000 }, + { label: "3 hours", value: "3h", milliseconds: 3 * 60 * 60 * 1000 }, + { label: "6 hours", value: "6h", milliseconds: 6 * 60 * 60 * 1000 }, + { label: "12 hours", value: "12h", milliseconds: 12 * 60 * 60 * 1000 }, + { label: "24 hours", value: "24h", milliseconds: 24 * 60 * 60 * 1000 }, + { label: "2 days", value: "2d", milliseconds: 2 * 24 * 60 * 60 * 1000 }, +]; + export interface ActorMetricsProps { actor: ActorAtom; } export function ActorMetrics({ actor }: ActorMetricsProps) { - const { metrics, status } = useAtomValue( + const { metrics, status, resources, id } = useAtomValue( selectAtom(actor, selector, equal), ); - const metricsData = useAtomValue(metrics); + const defaultMetricsData = useAtomValue(metrics); const [showAdvanced, setShowAdvanced] = useState(false); + + const timeWindowMs = useAtomValue(actorMetricsTimeWindowAtom); + const setTimeWindowMs = useSetAtom(actorMetricsTimeWindowAtom); + const environment = useAtomValue(actorEnvironmentAtom); + + const currentTimeWindow = timeWindowOptions.find(option => option.milliseconds === timeWindowMs) || timeWindowOptions[1]; + const [timeWindow, setTimeWindow] = useState(currentTimeWindow.value); const isActorRunning = status === "running"; + + // Create a query for time window-specific metrics + const { data: customMetricsData, status: customMetricsStatus } = useQuery({ + ...actorMetricsQueryOptions( + { + projectNameId: environment?.projectNameId || "", + environmentNameId: environment?.environmentNameId || "", + actorId: id, + timeWindowMs: timeWindowMs, + }, + { refetchInterval: 5000 } + ), + enabled: !!environment && !!id, + }); + + // Use custom metrics if available, otherwise fall back to default + const metricsData = customMetricsData ? { + metrics: customMetricsData.metrics, + rawData: customMetricsData.rawData, + interval: customMetricsData.interval, + status: customMetricsStatus, + updatedAt: Date.now(), + } : defaultMetricsData; + + const handleTimeWindowChange = (value: string) => { + setTimeWindow(value); + const selectedOption = timeWindowOptions.find(option => option.value === value); + if (selectedOption) { + setTimeWindowMs(selectedOption.milliseconds); + } + }; const formatBytes = (bytes: number | null | undefined) => { if (!isActorRunning || bytes === null || bytes === undefined) @@ -57,7 +113,7 @@ export function ActorMetrics({ actor }: ActorMetricsProps) { // Calculate CPU percentage using time series data points const cpuPercentage = useMemo(() => { if (!isActorRunning) { - return "n/a"; + return "Stopped"; } const data = metricsData; @@ -70,7 +126,7 @@ export function ActorMetrics({ actor }: ActorMetricsProps) { return "n/a"; } - // Find two non-zero consecutive data points to calculate rate + // Find the last valid CPU rate from the most recent data points let cpuRate = 0; for (let i = cpuValues.length - 1; i > 0; i--) { const currentCpu = cpuValues[i]; @@ -96,19 +152,20 @@ export function ActorMetrics({ actor }: ActorMetricsProps) { const calculateMemoryPercentage = ( usage: number | null | undefined, - limit: number | null | undefined, ) => { if ( !isActorRunning || usage === null || usage === undefined || - limit === null || - limit === undefined || - limit === 0 + !resources || + !resources.memory || + resources.memory === 0 ) { return null; } - return (usage / limit) * 100; + // Convert usage from bytes to MB and compare with resources.memory (which is in MB) + const usageMB = usage / (1024 * 1024); + return (usageMB / resources.memory) * 100; }; const isLoading = metricsData.status === "pending"; @@ -137,12 +194,25 @@ export function ActorMetrics({ actor }: ActorMetricsProps) { const memoryPercentage = calculateMemoryPercentage( data.memory_usage_bytes, - data.spec_memory_limit_bytes, ); return (
-

Container Metrics

+
+

Container Metrics

+ +
{/* Main Metrics */}
@@ -151,15 +221,17 @@ export function ActorMetrics({ actor }: ActorMetricsProps) {
CPU Usage
{cpuPercentage} - {cpuPercentage !== "n/a" ? ( + {metricsData.rawData?.cpu_usage_seconds_total && + metricsData.rawData.cpu_usage_seconds_total.length > 0 ? ( ) : null}
@@ -175,18 +247,20 @@ export function ActorMetrics({ actor }: ActorMetricsProps) { )} - {memoryPercentage !== null ? ( + {metricsData.rawData?.memory_usage_bytes && + metricsData.rawData.memory_usage_bytes.length > 0 ? ( ) : null} @@ -194,20 +268,8 @@ export function ActorMetrics({ actor }: ActorMetricsProps) {
- {/* Advanced Section Toggle */} -
- -
- {/* Advanced Metrics */} - {showAdvanced && ( + {false && ( {/* CPU & Performance */}
@@ -302,22 +364,14 @@ export function ActorMetrics({ actor }: ActorMetricsProps) {
- {/* Memory Limits */} + {/* Resource Limits */}
-

Memory Limits

+

Resource Limits

Memory Limit
-
{formatBytes(data.spec_memory_limit_bytes)}
-
Memory Reservation Limit
-
- {formatBytes( - data.spec_memory_reservation_limit_bytes, - )} -
-
Memory Swap Limit
-
- {formatBytes(data.spec_memory_swap_limit_bytes)} -
+
{resources?.memory ? `${resources.memory} MB` : "n/a"}
+
CPU Limit
+
{resources?.cpu ? `${resources.cpu / 1000} cores` : "n/a"}
@@ -483,7 +537,9 @@ export function ActorMetrics({ actor }: ActorMetricsProps) {
Close Wait
- {formatNumber(data.network_tcp_usage_closewait)} + {formatNumber( + data.network_tcp_usage_closewait, + )}
Closing
@@ -639,14 +695,10 @@ export function ActorMetrics({ actor }: ActorMetricsProps) {
{formatTimestamp(data.last_seen)}
Start Time
{formatTimestamp(data.start_time_seconds)}
-
CPU Shares
-
{formatNumber(data.spec_cpu_shares)}
-
CPU Period
-
{formatNumber(data.spec_cpu_period)}
)} ); -} +} \ No newline at end of file diff --git a/frontend/packages/components/src/actors/actor-network.tsx b/frontend/packages/components/src/actors/actor-network.tsx index 78feb5d6ff..34ee81ed49 100644 --- a/frontend/packages/components/src/actors/actor-network.tsx +++ b/frontend/packages/components/src/actors/actor-network.tsx @@ -1,19 +1,19 @@ import { Button, - cn, Dd, DiscreteCopyButton, Dl, DocsSheet, Dt, Flex, + cn, } from "@rivet-gg/components"; import { Icon, faBooks } from "@rivet-gg/icons"; -import { ActorObjectInspector } from "./console/actor-inspector"; -import { Fragment } from "react"; -import type { Actor, ActorAtom } from "./actor-context"; import { useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; +import { Fragment } from "react"; +import type { Actor, ActorAtom } from "./actor-context"; +import { ActorObjectInspector } from "./console/actor-inspector"; const selector = (a: Actor) => a.network?.ports; diff --git a/frontend/packages/components/src/actors/actor-not-found.tsx b/frontend/packages/components/src/actors/actor-not-found.tsx index e0596cd673..06ede521c1 100644 --- a/frontend/packages/components/src/actors/actor-not-found.tsx +++ b/frontend/packages/components/src/actors/actor-not-found.tsx @@ -1,12 +1,12 @@ import { Icon, faQuestionSquare } from "@rivet-gg/icons"; -import { useActorsView } from "./actors-view-context-provider"; -import { actorFiltersAtom, type ActorFeature } from "./actor-context"; -import { ActorTabs } from "./actors-actor-details"; import { useAtomValue, useSetAtom } from "jotai"; -import { useCallback } from "react"; import { selectAtom } from "jotai/utils"; +import { useCallback } from "react"; import { Button } from "../ui/button"; import { FilterOp } from "../ui/filters"; +import { type ActorFeature, actorFiltersAtom } from "./actor-context"; +import { ActorTabs } from "./actors-actor-details"; +import { useActorsView } from "./actors-view-context-provider"; export function ActorNotFound({ features = [], diff --git a/frontend/packages/components/src/actors/actor-region.tsx b/frontend/packages/components/src/actors/actor-region.tsx index fe5422dd60..12b7eb3e2e 100644 --- a/frontend/packages/components/src/actors/actor-region.tsx +++ b/frontend/packages/components/src/actors/actor-region.tsx @@ -1,13 +1,13 @@ -import { cn, Flex, WithTooltip } from "@rivet-gg/components"; +import { Flex, WithTooltip } from "@rivet-gg/components"; +import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; +import { useCallback } from "react"; import { REGION_LABEL, RegionIcon, getRegionKey, } from "../matchmaker/lobby-region"; -import { actorRegionsAtom, type Actor } from "./actor-context"; -import { selectAtom } from "jotai/utils"; -import { useAtomValue } from "jotai"; -import { useCallback } from "react"; +import { actorRegionsAtom } from "./actor-context"; interface ActorRegionProps { regionId?: string; diff --git a/frontend/packages/components/src/actors/actor-runtime.tsx b/frontend/packages/components/src/actors/actor-runtime.tsx index 5348820bd1..e8c0f181a8 100644 --- a/frontend/packages/components/src/actors/actor-runtime.tsx +++ b/frontend/packages/components/src/actors/actor-runtime.tsx @@ -1,21 +1,21 @@ +import equal from "fast-deep-equal"; +import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; import { Suspense } from "react"; +import { formatDuration } from "../lib/formatter"; +import { toRecord } from "../lib/utils"; +import { Flex } from "../ui/flex"; +import { Skeleton } from "../ui/skeleton"; +import { Dd, Dl, Dt } from "../ui/typography"; import { ActorBuild } from "./actor-build"; -import { ActorObjectInspector } from "./console/actor-inspector"; -import { ACTOR_FRAMEWORK_TAG_VALUE } from "./actor-tags"; import { - ActorFeature, - currentActorFeaturesAtom, type Actor, type ActorAtom, + ActorFeature, + currentActorFeaturesAtom, } from "./actor-context"; -import { selectAtom } from "jotai/utils"; -import { useAtomValue } from "jotai"; -import { Dd, Dl, Dt } from "../ui/typography"; -import { Flex } from "../ui/flex"; -import { formatDuration } from "../lib/formatter"; -import { toRecord } from "../lib/utils"; -import { Skeleton } from "../ui/skeleton"; -import equal from "fast-deep-equal"; +import { ACTOR_FRAMEWORK_TAG_VALUE } from "./actor-tags"; +import { ActorObjectInspector } from "./console/actor-inspector"; const selector = (a: Actor) => ({ lifecycle: a.lifecycle, diff --git a/frontend/packages/components/src/actors/actor-state-tab.tsx b/frontend/packages/components/src/actors/actor-state-tab.tsx index 1a6896a934..634596a9b5 100644 --- a/frontend/packages/components/src/actors/actor-state-tab.tsx +++ b/frontend/packages/components/src/actors/actor-state-tab.tsx @@ -1,11 +1,11 @@ +import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; +import type { Actor, ActorAtom } from "./actor-context"; import { ActorEditableState } from "./actor-editable-state"; import { useActorState, useActorWorkerStatus, } from "./worker/actor-worker-context"; -import type { Actor, ActorAtom } from "./actor-context"; -import { useAtomValue } from "jotai"; -import { selectAtom } from "jotai/utils"; const selector = (a: Actor) => a.destroyedAt; diff --git a/frontend/packages/components/src/actors/actor-status-indicator.tsx b/frontend/packages/components/src/actors/actor-status-indicator.tsx index 676b4d5a6e..81f9ff34dc 100644 --- a/frontend/packages/components/src/actors/actor-status-indicator.tsx +++ b/frontend/packages/components/src/actors/actor-status-indicator.tsx @@ -1,8 +1,8 @@ import { Ping, cn } from "@rivet-gg/components"; -import type { Actor, ActorAtom } from "./actor-context"; import { useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; import type { ComponentPropsWithRef } from "react"; +import type { Actor, ActorAtom } from "./actor-context"; export type ActorStatus = | "starting" diff --git a/frontend/packages/components/src/actors/actor-status-label.tsx b/frontend/packages/components/src/actors/actor-status-label.tsx index 523cee6244..c6a16ddb7d 100644 --- a/frontend/packages/components/src/actors/actor-status-label.tsx +++ b/frontend/packages/components/src/actors/actor-status-label.tsx @@ -1,6 +1,6 @@ -import type { Actor, ActorAtom } from "./actor-context"; import { useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; +import type { Actor, ActorAtom } from "./actor-context"; import type { ActorStatus } from "./actor-status-indicator"; export const ACTOR_STATUS_LABEL_MAP = { diff --git a/frontend/packages/components/src/actors/actor-status.tsx b/frontend/packages/components/src/actors/actor-status.tsx index 23857f2114..0f587ed7cb 100644 --- a/frontend/packages/components/src/actors/actor-status.tsx +++ b/frontend/packages/components/src/actors/actor-status.tsx @@ -1,4 +1,5 @@ import { cn } from "@rivet-gg/components"; +import type { ActorAtom } from "./actor-context"; import { ActorStatusIndicator, type ActorStatus as ActorStatusType, @@ -8,7 +9,6 @@ import { ActorStatusLabel, AtomizedActorStatusLabel, } from "./actor-status-label"; -import type { ActorAtom } from "./actor-context"; interface ActorStatusProps { className?: string; diff --git a/frontend/packages/components/src/actors/actor-stop-button.tsx b/frontend/packages/components/src/actors/actor-stop-button.tsx index 824e71ad26..cbc67ea89f 100644 --- a/frontend/packages/components/src/actors/actor-stop-button.tsx +++ b/frontend/packages/components/src/actors/actor-stop-button.tsx @@ -1,10 +1,10 @@ import { Button, WithTooltip } from "@rivet-gg/components"; import { Icon, faXmark } from "@rivet-gg/icons"; -import type { Actor, ActorAtom, DestroyActorAtom } from "./actor-context"; +import equal from "fast-deep-equal"; import { useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; -import equal from "fast-deep-equal"; +import type { Actor, ActorAtom, DestroyActorAtom } from "./actor-context"; const selector = (a: Actor) => ({ destroyedAt: a.destroyedAt, diff --git a/frontend/packages/components/src/actors/actor-tags-select.tsx b/frontend/packages/components/src/actors/actor-tags-select.tsx index a4a93878c8..17eedbba3e 100644 --- a/frontend/packages/components/src/actors/actor-tags-select.tsx +++ b/frontend/packages/components/src/actors/actor-tags-select.tsx @@ -1,8 +1,8 @@ import { Combobox } from "@rivet-gg/components"; -import { ActorTag } from "./actor-tags"; import { useAtomValue } from "jotai"; -import { actorTagsAtom } from "./actor-context"; import { useMemo } from "react"; +import { actorTagsAtom } from "./actor-context"; +import { ActorTag } from "./actor-tags"; interface ActorTagsSelectProps { value: Record; diff --git a/frontend/packages/components/src/actors/actors-actor-details.tsx b/frontend/packages/components/src/actors/actors-actor-details.tsx index 26ed422932..e45a7bc48e 100644 --- a/frontend/packages/components/src/actors/actors-actor-details.tsx +++ b/frontend/packages/components/src/actors/actors-actor-details.tsx @@ -6,9 +6,16 @@ import { TabsTrigger, cn, } from "@rivet-gg/components"; -import { memo, type ReactNode, Suspense } from "react"; +import { Icon, faQuestionSquare } from "@rivet-gg/icons"; +import { useAtomValue } from "jotai"; +import { type ReactNode, Suspense, memo } from "react"; import { ActorConfigTab } from "./actor-config-tab"; import { ActorConnectionsTab } from "./actor-connections-tab"; +import { + type ActorAtom, + ActorFeature, + currentActorFeaturesAtom, +} from "./actor-context"; import { ActorDetailsSettingsProvider } from "./actor-details-settings"; import { ActorLogsTab } from "./actor-logs-tab"; import { ActorMetricsTab } from "./actor-metrics-tab"; @@ -16,25 +23,30 @@ import { ActorStateTab } from "./actor-state-tab"; import { AtomizedActorStatus } from "./actor-status"; import { ActorStopButton } from "./actor-stop-button"; import { ActorsSidebarToggleButton } from "./actors-sidebar-toggle-button"; +import { useActorsView } from "./actors-view-context-provider"; import { ActorConsole } from "./console/actor-console"; import { ActorWorkerContextProvider } from "./worker/actor-worker-context"; -import { - ActorFeature, - currentActorFeaturesAtom, - type ActorAtom, -} from "./actor-context"; -import { useAtomValue } from "jotai"; -import { useActorsView } from "./actors-view-context-provider"; -import { faQuestionSquare, Icon } from "@rivet-gg/icons"; interface ActorsActorDetailsProps { tab?: string; actor: ActorAtom; onTabChange?: (tab: string) => void; + onExportLogs?: ( + actorId: string, + typeFilter?: string, + filter?: string, + ) => Promise; + isExportingLogs?: boolean; } export const ActorsActorDetails = memo( - ({ tab, onTabChange, actor }: ActorsActorDetailsProps) => { + ({ + tab, + onTabChange, + actor, + onExportLogs, + isExportingLogs, + }: ActorsActorDetailsProps) => { const actorFeatures = useAtomValue(currentActorFeaturesAtom); const supportsConsole = actorFeatures?.includes(ActorFeature.Console); @@ -52,6 +64,8 @@ export const ActorsActorDetails = memo( actor={actor} tab={tab} onTabChange={onTabChange} + onExportLogs={onExportLogs} + isExportingLogs={isExportingLogs} /> {supportsConsole ? : null} @@ -88,6 +102,8 @@ export function ActorTabs({ className, disabled, children, + onExportLogs, + isExportingLogs, }: { disabled?: boolean; tab?: string; @@ -96,6 +112,12 @@ export function ActorTabs({ actor?: ActorAtom; className?: string; children?: ReactNode; + onExportLogs?: ( + actorId: string, + typeFilter?: string, + filter?: string, + ) => Promise; + isExportingLogs?: boolean; }) { const supportsState = features?.includes(ActorFeature.State); const supportsLogs = features?.includes(ActorFeature.Logs); @@ -170,7 +192,11 @@ export function ActorTabs({ className="min-h-0 flex-1 mt-0 h-full" > }> - + ) : null} diff --git a/frontend/packages/components/src/actors/actors-actor-not-found.tsx b/frontend/packages/components/src/actors/actors-actor-not-found.tsx index bb4bf88c3c..96fed3be76 100644 --- a/frontend/packages/components/src/actors/actors-actor-not-found.tsx +++ b/frontend/packages/components/src/actors/actors-actor-not-found.tsx @@ -1,6 +1,6 @@ // import { isRivetError } from "@/lib/utils"; // import { RivetError } from "@rivet-gg/api"; -import { Icon, faCircleExclamation, faNotdef } from "@rivet-gg/icons"; +import { Icon, faCircleExclamation } from "@rivet-gg/icons"; import type { ErrorComponentProps } from "@tanstack/react-router"; import { ActorsSidebarToggleButton } from "./actors-sidebar-toggle-button"; diff --git a/frontend/packages/components/src/actors/actors-layout.tsx b/frontend/packages/components/src/actors/actors-layout.tsx index 396df15628..df2a8d4b32 100644 --- a/frontend/packages/components/src/actors/actors-layout.tsx +++ b/frontend/packages/components/src/actors/actors-layout.tsx @@ -1,5 +1,5 @@ -import { cn, ls } from "../lib/utils"; import { type ReactNode, memo, useState } from "react"; +import { cn, ls } from "../lib/utils"; import { ActorsLayoutContextProvider } from "./actors-layout-context"; interface ActorsListPreviewProps { diff --git a/frontend/packages/components/src/actors/actors-list-preview.tsx b/frontend/packages/components/src/actors/actors-list-preview.tsx index a74799a667..d26a22acab 100644 --- a/frontend/packages/components/src/actors/actors-list-preview.tsx +++ b/frontend/packages/components/src/actors/actors-list-preview.tsx @@ -1,4 +1,3 @@ -import { cn, ls } from "../lib/utils"; import { Icon, faGripDotsVertical } from "@rivet-gg/icons"; import { animate, @@ -16,6 +15,7 @@ import { useLayoutEffect, useState, } from "react"; +import { cn, ls } from "../lib/utils"; import { ActorsLayoutContextProvider } from "./actors-layout-context"; import { ActorsListPanel } from "./actors-list-panel"; diff --git a/frontend/packages/components/src/actors/actors-list-row.tsx b/frontend/packages/components/src/actors/actors-list-row.tsx index 5935490720..6108b4df0c 100644 --- a/frontend/packages/components/src/actors/actors-list-row.tsx +++ b/frontend/packages/components/src/actors/actors-list-row.tsx @@ -6,20 +6,20 @@ import { cn, toRecord, } from "@rivet-gg/components"; +import { Icon, faTag, faTags } from "@rivet-gg/icons"; import { Link } from "@tanstack/react-router"; +import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; import { memo } from "react"; -import { ActorRegion } from "./actor-region"; -import { AtomizedActorStatusIndicator } from "./actor-status-indicator"; -import { ActorTags } from "./actor-tags"; import { - isCurrentActorAtom, type Actor, type ActorAtom, + isCurrentActorAtom, } from "./actor-context"; -import { useAtomValue } from "jotai"; -import { selectAtom } from "jotai/utils"; -import { faTag, faTags, Icon } from "@rivet-gg/icons"; +import { ActorRegion } from "./actor-region"; +import { AtomizedActorStatusIndicator } from "./actor-status-indicator"; import { AtomizedActorStatusLabel } from "./actor-status-label"; +import { ActorTags } from "./actor-tags"; interface ActorsListRowProps { className?: string; diff --git a/frontend/packages/components/src/actors/actors-list.tsx b/frontend/packages/components/src/actors/actors-list.tsx index af5128f19d..2ca0e8242d 100644 --- a/frontend/packages/components/src/actors/actors-list.tsx +++ b/frontend/packages/components/src/actors/actors-list.tsx @@ -1,11 +1,8 @@ import { Button, Checkbox, - cn, CommandGroup, CommandItem, - createFiltersPicker, - createFiltersSchema, DocsSheet, FilterCreator, type FilterDefinitions, @@ -15,23 +12,12 @@ import { ScrollArea, ShimmerLine, SmallText, + cn, + createFiltersPicker, + createFiltersSchema, } from "@rivet-gg/components"; -import { ActorsListRow } from "./actors-list-row"; -import { CreateActorButton } from "./create-actor-button"; -import { GoToActorButton } from "./go-to-actor-button"; -import { useSearch, useNavigate } from "@tanstack/react-router"; -import { useAtomValue, useSetAtom } from "jotai"; -import { - actorFiltersAtom, - actorFiltersCountAtom, - actorRegionsAtom, - actorsAtomsAtom, - actorsPaginationAtom, - actorsQueryAtom, - actorTagsAtom, - filteredActorsCountAtom, -} from "./actor-context"; import { + Icon, faActors, faCalendarCircleMinus, faCalendarCirclePlus, @@ -40,17 +26,30 @@ import { faCode, faGlobe, faReact, - faRust, faSignalBars, faTag, faTs, - Icon, } from "@rivet-gg/icons"; -import { useActorsView } from "./actors-view-context-provider"; +import { useNavigate, useSearch } from "@tanstack/react-router"; +import { useAtomValue, useSetAtom } from "jotai"; import { useCallback, useMemo } from "react"; -import { ActorTag } from "./actor-tags"; -import type { ActorStatus as ActorStatusType } from "./actor-status-indicator"; +import { + actorFiltersAtom, + actorFiltersCountAtom, + actorRegionsAtom, + actorTagsAtom, + actorsAtomsAtom, + actorsPaginationAtom, + actorsQueryAtom, + filteredActorsCountAtom, +} from "./actor-context"; import { ActorStatus } from "./actor-status"; +import type { ActorStatus as ActorStatusType } from "./actor-status-indicator"; +import { ActorTag } from "./actor-tags"; +import { ActorsListRow } from "./actors-list-row"; +import { useActorsView } from "./actors-view-context-provider"; +import { CreateActorButton } from "./create-actor-button"; +import { GoToActorButton } from "./go-to-actor-button"; export function ActorsList() { return ( diff --git a/frontend/packages/components/src/actors/build-select.tsx b/frontend/packages/components/src/actors/build-select.tsx index e5768dd6ff..ac5ed0eecf 100644 --- a/frontend/packages/components/src/actors/build-select.tsx +++ b/frontend/packages/components/src/actors/build-select.tsx @@ -1,7 +1,7 @@ import { Badge, Combobox } from "@rivet-gg/components"; import { useAtomValue } from "jotai"; -import { actorBuildsAtom } from "./actor-context"; import { useMemo } from "react"; +import { actorBuildsAtom } from "./actor-context"; interface BuildSelectProps { onValueChange: (value: string) => void; diff --git a/frontend/packages/components/src/actors/console/actor-console-logs.tsx b/frontend/packages/components/src/actors/console/actor-console-logs.tsx index fd0750ddc4..bd3593a6ba 100644 --- a/frontend/packages/components/src/actors/console/actor-console-logs.tsx +++ b/frontend/packages/components/src/actors/console/actor-console-logs.tsx @@ -1,8 +1,8 @@ import { ScrollArea } from "@rivet-gg/components"; +import { useLayoutEffect, useRef } from "react"; import { useActorDetailsSettings } from "../actor-details-settings"; import { useActorReplCommands } from "../worker/actor-worker-context"; import { ActorConsoleLog } from "./actor-console-log"; -import { useLayoutEffect, useRef } from "react"; export function ActorConsoleLogs() { const isScrolledToBottom = useRef(true); diff --git a/frontend/packages/components/src/actors/console/actor-console-message.tsx b/frontend/packages/components/src/actors/console/actor-console-message.tsx index a1da3ba837..eaba9b1e3d 100644 --- a/frontend/packages/components/src/actors/console/actor-console-message.tsx +++ b/frontend/packages/components/src/actors/console/actor-console-message.tsx @@ -6,7 +6,6 @@ import { faExclamationCircle, faSpinnerThird, faWarning, - faXmark, } from "@rivet-gg/icons"; import { format } from "date-fns"; import { type ReactNode, forwardRef } from "react"; diff --git a/frontend/packages/components/src/actors/create-actor-button.tsx b/frontend/packages/components/src/actors/create-actor-button.tsx index bf8abdd057..3748de59bb 100644 --- a/frontend/packages/components/src/actors/create-actor-button.tsx +++ b/frontend/packages/components/src/actors/create-actor-button.tsx @@ -1,4 +1,4 @@ -import { Button, WithTooltip, type ButtonProps } from "@rivet-gg/components"; +import { Button, type ButtonProps, WithTooltip } from "@rivet-gg/components"; import { Icon, faPlus } from "@rivet-gg/icons"; import { useNavigate } from "@tanstack/react-router"; import { useAtomValue } from "jotai"; diff --git a/frontend/packages/components/src/actors/dialogs/create-actor-dialog.tsx b/frontend/packages/components/src/actors/dialogs/create-actor-dialog.tsx index ca9c353b37..9dc8b0745f 100644 --- a/frontend/packages/components/src/actors/dialogs/create-actor-dialog.tsx +++ b/frontend/packages/components/src/actors/dialogs/create-actor-dialog.tsx @@ -1,4 +1,4 @@ -import * as ActorCreateForm from "../form/actor-create-form"; +import { useAtomValue } from "jotai"; import { DialogDescription, DialogFooter, @@ -6,10 +6,10 @@ import { DialogTitle, } from "../../ui/dialog"; import { Flex } from "../../ui/flex"; -import { useAtomValue } from "jotai"; import { createActorAtom } from "../actor-context"; -import type { DialogContentProps } from "../hooks"; import { useActorsView } from "../actors-view-context-provider"; +import * as ActorCreateForm from "../form/actor-create-form"; +import type { DialogContentProps } from "../hooks"; interface ContentProps extends DialogContentProps {} diff --git a/frontend/packages/components/src/actors/dialogs/go-to-actor-dialog.tsx b/frontend/packages/components/src/actors/dialogs/go-to-actor-dialog.tsx index 0bd117930a..3baf32d185 100644 --- a/frontend/packages/components/src/actors/dialogs/go-to-actor-dialog.tsx +++ b/frontend/packages/components/src/actors/dialogs/go-to-actor-dialog.tsx @@ -1,8 +1,8 @@ -import * as GoToActorForm from "../form/go-to-actor-form"; -import type { DialogContentProps } from "../hooks"; -import { DialogFooter, DialogHeader, DialogTitle } from "../../ui/dialog"; import { Button } from "../../ui/button"; +import { DialogFooter, DialogHeader, DialogTitle } from "../../ui/dialog"; import { useActorsView } from "../actors-view-context-provider"; +import * as GoToActorForm from "../form/go-to-actor-form"; +import type { DialogContentProps } from "../hooks"; interface ContentProps extends DialogContentProps { onSubmit?: (actorId: string) => void; diff --git a/frontend/packages/components/src/actors/form/actor-create-form.tsx b/frontend/packages/components/src/actors/form/actor-create-form.tsx index 31b438a22a..bf92b99b46 100644 --- a/frontend/packages/components/src/actors/form/actor-create-form.tsx +++ b/frontend/packages/components/src/actors/form/actor-create-form.tsx @@ -11,11 +11,6 @@ import { JsonCode } from "@rivet-gg/components/code-mirror"; import { type UseFormReturn, useFormContext } from "react-hook-form"; import z from "zod"; -import { - Tags as TagsInput, - formSchema as tagsFormSchema, -} from "./build-tags-form"; -import { RegionSelect } from "../region-select"; import { useAtomValue, useSetAtom } from "jotai"; import { actorCustomTagKeys, @@ -24,6 +19,11 @@ import { actorTagValuesAtom, } from "../actor-context"; import { BuildSelect } from "../build-select"; +import { RegionSelect } from "../region-select"; +import { + Tags as TagsInput, + formSchema as tagsFormSchema, +} from "./build-tags-form"; const jsonValid = z.custom((value) => { try { diff --git a/frontend/packages/components/src/actors/form/build-tags-form.tsx b/frontend/packages/components/src/actors/form/build-tags-form.tsx index 1d46b1a446..b431baf7cd 100644 --- a/frontend/packages/components/src/actors/form/build-tags-form.tsx +++ b/frontend/packages/components/src/actors/form/build-tags-form.tsx @@ -6,6 +6,8 @@ import { } from "react-hook-form"; import z from "zod"; import { createSchemaForm } from "../../lib/create-schema-form"; +import { Button } from "../../ui/button"; +import { Combobox, type ComboboxOption as Option } from "../../ui/combobox"; import { FormControl, FormFieldContext, @@ -14,8 +16,6 @@ import { FormMessage, } from "../../ui/form"; import { Text } from "../../ui/typography"; -import { Button } from "../../ui/button"; -import { Combobox, type ComboboxOption as Option } from "../../ui/combobox"; export const formSchema = z.object({ tags: z diff --git a/frontend/packages/components/src/actors/get-started.tsx b/frontend/packages/components/src/actors/get-started.tsx index fbe7981e6a..8039cb19f9 100644 --- a/frontend/packages/components/src/actors/get-started.tsx +++ b/frontend/packages/components/src/actors/get-started.tsx @@ -1,9 +1,9 @@ -import { Icon, faServer, faActors, faFunction } from "@rivet-gg/icons"; +import { Icon, faActors, faFunction, faServer } from "@rivet-gg/icons"; import { motion } from "framer-motion"; import type { ComponentProps } from "react"; -import { Button } from "../ui/button"; import { DocsSheet } from "../docs-sheet"; import { cn } from "../lib/utils"; +import { Button } from "../ui/button"; export function ActorsResources() { return ( diff --git a/frontend/packages/components/src/actors/getting-started.tsx b/frontend/packages/components/src/actors/getting-started.tsx index 15bab1cafc..e64f8ac502 100644 --- a/frontend/packages/components/src/actors/getting-started.tsx +++ b/frontend/packages/components/src/actors/getting-started.tsx @@ -1,6 +1,6 @@ -import { faActors, Icon } from "@rivet-gg/icons"; -import { ActorsResources } from "./get-started"; +import { Icon, faActors } from "@rivet-gg/icons"; import { useActorsView } from "./actors-view-context-provider"; +import { ActorsResources } from "./get-started"; export function GettingStarted() { const { copy } = useActorsView(); diff --git a/frontend/packages/components/src/actors/worker/actor-repl.worker.ts b/frontend/packages/components/src/actors/worker/actor-repl.worker.ts index 1344a53929..9d879c989f 100644 --- a/frontend/packages/components/src/actors/worker/actor-repl.worker.ts +++ b/frontend/packages/components/src/actors/worker/actor-repl.worker.ts @@ -1,25 +1,25 @@ -import { fromJs } from "esast-util-from-js"; -import { toJs } from "estree-util-to-js"; import { + type InspectData, type ToClient, ToClientSchema, - type InspectData, type ToServer, } from "actor-core/inspector/protocol/actor"; +import { fromJs } from "esast-util-from-js"; +import { toJs } from "estree-util-to-js"; -import type { ResponseOk, Request } from "actor-core/protocol/http"; +import type { Request, ResponseOk } from "actor-core/protocol/http"; import { type HighlighterCore, createHighlighterCore, createOnigurumaEngine, } from "shiki"; +import { endWithSlash } from "../../lib/utils"; import { MessageSchema, type ReplErrorCode, type Response, ResponseSchema, } from "./actor-worker-schema"; -import { endWithSlash } from "../../lib/utils"; class ReplError extends Error { constructor( diff --git a/frontend/packages/components/src/actors/worker/actor-worker-container.ts b/frontend/packages/components/src/actors/worker/actor-worker-container.ts index 5640951ead..7352e8e6bc 100644 --- a/frontend/packages/components/src/actors/worker/actor-worker-container.ts +++ b/frontend/packages/components/src/actors/worker/actor-worker-container.ts @@ -1,16 +1,16 @@ import { CancelledError } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { ls } from "../../lib/utils"; import ActorWorker from "./actor-repl.worker?worker"; import { type CodeMessage, type FormattedCode, type InitMessage, - type Log, type InspectData, + type Log, ResponseSchema, type SetStateMessage, } from "./actor-worker-schema"; -import { toast } from "sonner"; -import { ls } from "../../lib/utils"; export type ReplCommand = { logs: Log[]; diff --git a/frontend/packages/components/src/actors/worker/actor-worker-context.tsx b/frontend/packages/components/src/actors/worker/actor-worker-context.tsx index fbed4f143c..0d731caae6 100644 --- a/frontend/packages/components/src/actors/worker/actor-worker-context.tsx +++ b/frontend/packages/components/src/actors/worker/actor-worker-context.tsx @@ -1,3 +1,5 @@ +import { useAtomValue } from "jotai"; +import { selectAtom } from "jotai/utils"; import { type ReactNode, createContext, @@ -7,12 +9,10 @@ import { useState, useSyncExternalStore, } from "react"; -import { ActorWorkerContainer } from "./actor-worker-container"; -import { assertNonNullable } from "../../lib/utils"; -import { ActorFeature, type Actor, type ActorAtom } from "../actor-context"; -import { selectAtom } from "jotai/utils"; -import { useAtomValue } from "jotai"; import { toast } from "sonner"; +import { assertNonNullable } from "../../lib/utils"; +import { type Actor, type ActorAtom, ActorFeature } from "../actor-context"; +import { ActorWorkerContainer } from "./actor-worker-container"; export const ActorWorkerContext = createContext( null, diff --git a/frontend/packages/components/src/copy-area.tsx b/frontend/packages/components/src/copy-area.tsx index f1484cf4c5..d9e34f7de6 100644 --- a/frontend/packages/components/src/copy-area.tsx +++ b/frontend/packages/components/src/copy-area.tsx @@ -1,6 +1,6 @@ "use client"; -import { Slot, Slottable } from "@radix-ui/react-slot"; +import { Slot } from "@radix-ui/react-slot"; import { Icon, faCopy } from "@rivet-gg/icons"; import { type ComponentProps, diff --git a/frontend/packages/components/src/dialogs/feedback-dialog.tsx b/frontend/packages/components/src/dialogs/feedback-dialog.tsx index e6038e2db5..3e298a3060 100644 --- a/frontend/packages/components/src/dialogs/feedback-dialog.tsx +++ b/frontend/packages/components/src/dialogs/feedback-dialog.tsx @@ -2,14 +2,14 @@ import * as FeedbackForm from "../forms/feedback-form"; import type { DialogContentProps } from "../hooks/use-dialog"; import { FEEDBACK_FORM_ID } from "../lib/constants"; +import { DialogDescription } from "@radix-ui/react-dialog"; +import { Icon, faDiscord } from "@rivet-gg/icons"; import { usePostHog } from "posthog-js/react"; import { useState } from "react"; +import { Button } from "../ui/button"; import { DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; import { Flex } from "../ui/flex"; -import { Button } from "../ui/button"; import { Link, Text } from "../ui/typography"; -import { DialogDescription } from "@radix-ui/react-dialog"; -import { faDiscord, Icon } from "@rivet-gg/icons"; interface ContentProps extends DialogContentProps { source?: string; diff --git a/frontend/packages/components/src/docs-sheet.tsx b/frontend/packages/components/src/docs-sheet.tsx index 1ef766da0d..bf403d3748 100644 --- a/frontend/packages/components/src/docs-sheet.tsx +++ b/frontend/packages/components/src/docs-sheet.tsx @@ -41,7 +41,11 @@ export function DocsSheet({ {title} @@ -52,7 +56,11 @@ export function DocsSheet({
-
- ); + return ( +
+ +
+ ); } diff --git a/site/src/app/(v2)/[section]/[[...page]]/page.tsx b/site/src/app/(v2)/[section]/[[...page]]/page.tsx index 8978cfe9fc..96cc400444 100644 --- a/site/src/app/(v2)/[section]/[[...page]]/page.tsx +++ b/site/src/app/(v2)/[section]/[[...page]]/page.tsx @@ -8,6 +8,7 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { Comments } from "@/components/Comments"; import { DocsNavigation } from "@/components/DocsNavigation"; import { DocsTableOfContents } from "@/components/DocsTableOfContents"; import { Prose } from "@/components/Prose"; @@ -18,7 +19,6 @@ import { Icon, faPencil } from "@rivet-gg/icons"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { VALID_SECTIONS, buildFullPath, buildPathComponents } from "./util"; -import { Comments } from "@/components/Comments"; interface Param { section: string; diff --git a/site/src/app/(v2)/oss-friends/page.tsx b/site/src/app/(v2)/oss-friends/page.tsx index af13c1e4f0..b633f4234a 100644 --- a/site/src/app/(v2)/oss-friends/page.tsx +++ b/site/src/app/(v2)/oss-friends/page.tsx @@ -1,9 +1,8 @@ -import { Button } from "@rivet-gg/components"; import { FancyHeader } from "@/components/v2/FancyHeader"; async function getOssFriends() { const res = await fetch("https://formbricks.com/api/oss-friends", { - next: { revalidate: 3600 } // Revalidate every hour + next: { revalidate: 3600 }, // Revalidate every hour }); const data = await res.json(); return data.data; @@ -20,13 +19,12 @@ export default async function OssFriendsPage() {

{"Rivet's"}{" "} - - Open Source - {" "} + Open Source{" "} Friends

- Other companies whose code & culture mirrors that at Rivet. + Other companies whose code & culture mirrors that at + Rivet.

@@ -52,4 +50,4 @@ export default async function OssFriendsPage() { ); -} \ No newline at end of file +} diff --git a/site/src/app/layout.tsx b/site/src/app/layout.tsx index c1d896eed8..3dc6206fff 100644 --- a/site/src/app/layout.tsx +++ b/site/src/app/layout.tsx @@ -73,7 +73,10 @@ export default function Layout({ children }) { - + diff --git a/site/src/components/CollapsibleSidebarItem.tsx b/site/src/components/CollapsibleSidebarItem.tsx index 9a3b45cc17..599ca62938 100644 --- a/site/src/components/CollapsibleSidebarItem.tsx +++ b/site/src/components/CollapsibleSidebarItem.tsx @@ -29,9 +29,7 @@ export function CollapsibleSidebarItem({ {item.icon ? ( ) : null} - - {item.title} - + {item.title} {item.name} @@ -252,7 +250,9 @@ function SmallPrint() { rights reserved.

- Cloudflare® and Durable Objects™ are trademarks of Cloudflare, Inc. No affiliation or endorsement implied. References used for comparison purposes only. + Cloudflare® and Durable Objects™ are trademarks of + Cloudflare, Inc. No affiliation or endorsement implied. + References used for comparison purposes only.

diff --git a/site/src/components/GitHubStars.tsx b/site/src/components/GitHubStars.tsx index c46b2917b3..c01a68432b 100644 --- a/site/src/components/GitHubStars.tsx +++ b/site/src/components/GitHubStars.tsx @@ -61,10 +61,7 @@ export function GitHubStars({ href={`https://github.com/${repo}`} target="_blank" rel="noreferrer" - className={cn( - "flex items-center gap-2", - className, - )} + className={cn("flex items-center gap-2", className)} {...props} > diff --git a/site/src/components/GitHubStarsDropdown.tsx b/site/src/components/GitHubStarsDropdown.tsx index fd3d5cc9d8..1b6d9b9321 100644 --- a/site/src/components/GitHubStarsDropdown.tsx +++ b/site/src/components/GitHubStarsDropdown.tsx @@ -1,6 +1,6 @@ "use client"; import { cn } from "@rivet-gg/components"; -import { Icon, faGithub, faArrowRight } from "@rivet-gg/icons"; +import { Icon, faArrowRight, faGithub } from "@rivet-gg/icons"; import { useEffect, useState } from "react"; interface GitHubStarsDropdownProps @@ -22,11 +22,20 @@ export function GitHubStarsDropdown({ className, ...props }: GitHubStarsDropdownProps) { - const [rivetStars, setRivetStars] = useState({ stars: 0, loading: true }); - const [rivetKitStars, setRivetKitStars] = useState({ stars: 0, loading: true }); + const [rivetStars, setRivetStars] = useState({ + stars: 0, + loading: true, + }); + const [rivetKitStars, setRivetKitStars] = useState({ + stars: 0, + loading: true, + }); const [isOpen, setIsOpen] = useState(false); - const fetchStars = async (repo: string, setter: (data: RepoData) => void) => { + const fetchStars = async ( + repo: string, + setter: (data: RepoData) => void, + ) => { const cacheKey = `github-stars-${repo}`; const cachedData = sessionStorage.getItem(cacheKey); @@ -39,7 +48,9 @@ export function GitHubStarsDropdown({ } try { - const response = await fetch(`https://api.github.com/repos/${repo}`); + const response = await fetch( + `https://api.github.com/repos/${repo}`, + ); if (!response.ok) throw new Error("Failed to fetch"); const data = await response.json(); const newStars = data.stargazers_count; @@ -96,10 +107,15 @@ export function GitHubStarsDropdown({
Rivet Actors - {rivetKitStars.loading ? "..." : `${formatNumber(rivetKitStars.stars)} stars`} + {rivetKitStars.loading + ? "..." + : `${formatNumber(rivetKitStars.stars)} stars`}
- + Rivet Cloud - {rivetStars.loading ? "..." : `${formatNumber(rivetStars.stars)} stars`} + {rivetStars.loading + ? "..." + : `${formatNumber(rivetStars.stars)} stars`} - + diff --git a/site/src/components/Heading.jsx b/site/src/components/Heading.jsx index fdf4a3f21f..70db0f1517 100644 --- a/site/src/components/Heading.jsx +++ b/site/src/components/Heading.jsx @@ -1,58 +1,83 @@ -'use client'; -import { Button } from '@rivet-gg/components'; -import Link from 'next/link'; +"use client"; +import { Button } from "@rivet-gg/components"; +import Link from "next/link"; -import { Tag } from '@/components/Tag'; -import { Icon, faLink } from '@rivet-gg/icons'; +import { Tag } from "@/components/Tag"; +import { Icon, faLink } from "@rivet-gg/icons"; function Eyebrow({ tag, label }) { - if (!tag && !label) { - return null; - } + if (!tag && !label) { + return null; + } - return ( -
- {tag && {tag}} - {tag && label && } - {label && {label}} -
- ); + return ( +
+ {tag && {tag}} + {tag && label && ( + + )} + {label && ( + + {label} + + )} +
+ ); } function Anchor({ id, children }) { - return ( -
- -
- ); + return ( +
+ +
+ ); } -export function Heading({ level = 2, children, id, tag, label, anchor = true, ...props }) { - const Component = `h${level}`; +export function Heading({ + level = 2, + children, + id, + tag, + label, + anchor = true, + ...props +}) { + const Component = `h${level}`; - return ( - <> - {level == 2 &&
} - - - {anchor ? : null} - {anchor ? ( - - {children} - - ) : ( - children - )} - - - ); + return ( + <> + {level == 2 &&
} + + + {anchor ? : null} + {anchor ? ( + + {children} + + ) : ( + children + )} + + + ); } diff --git a/site/src/components/PricingCalculator.tsx b/site/src/components/PricingCalculator.tsx index de99ca42ce..ac8aa822f8 100644 --- a/site/src/components/PricingCalculator.tsx +++ b/site/src/components/PricingCalculator.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import { useState } from "react"; const PRIMITIVES = [ - { label: 'Function', value: 'function' }, - { label: 'Container', value: 'container' }, - { label: 'Actor', value: 'actor' }, + { label: "Function", value: "function" }, + { label: "Container", value: "container" }, + { label: "Actor", value: "actor" }, ]; const MEMORY_OPTIONS = [128, 256, 512, 1024, 2048]; // in MB @@ -11,204 +11,358 @@ const MEMORY_OPTIONS = [128, 256, 512, 1024, 2048]; // in MB const MS_IN_HOUR = 3600 * 1000; const COST_PER_MS = 0.00000000163; const COST_PER_GB_BW = 0.05; -const COST_PER_MILLION_READS = 0.20; -const COST_PER_MILLION_WRITES = 1.00; -const COST_PER_GB_MONTH = 0.20; +const COST_PER_MILLION_READS = 0.2; +const COST_PER_MILLION_WRITES = 1.0; +const COST_PER_GB_MONTH = 0.2; function calculateCost({ - memory, - requests, - durationMs, - bandwidthGB, - reads, - writes, - storedGB, + memory, + requests, + durationMs, + bandwidthGB, + reads, + writes, + storedGB, }) { - const totalMs = requests * durationMs; - const computeCost = totalMs * COST_PER_MS * (memory / 128); - const bandwidthCost = Math.max(0, bandwidthGB - 10) * COST_PER_GB_BW; - const readCost = Math.max(0, reads - 1_000_000) / 1_000_000 * COST_PER_MILLION_READS; - const writeCost = Math.max(0, writes - 1_000_000) / 1_000_000 * COST_PER_MILLION_WRITES; - const storageCost = Math.max(0, storedGB) * COST_PER_GB_MONTH; - return computeCost + bandwidthCost + readCost + writeCost + storageCost; + const totalMs = requests * durationMs; + const computeCost = totalMs * COST_PER_MS * (memory / 128); + const bandwidthCost = Math.max(0, bandwidthGB - 10) * COST_PER_GB_BW; + const readCost = + (Math.max(0, reads - 1_000_000) / 1_000_000) * COST_PER_MILLION_READS; + const writeCost = + (Math.max(0, writes - 1_000_000) / 1_000_000) * COST_PER_MILLION_WRITES; + const storageCost = Math.max(0, storedGB) * COST_PER_GB_MONTH; + return computeCost + bandwidthCost + readCost + writeCost + storageCost; } const inputStyle = { - background: '#222', - color: '#fff', - border: '1px solid #444', - borderRadius: 6, - padding: '6px 10px', - fontSize: 16, - marginLeft: 8, - marginTop: 2, - marginBottom: 2, - width: 180, + background: "#222", + color: "#fff", + border: "1px solid #444", + borderRadius: 6, + padding: "6px 10px", + fontSize: 16, + marginLeft: 8, + marginTop: 2, + marginBottom: 2, + width: 180, }; function PrimitiveEntry({ entry, onChange, onRemove, index }) { - return ( -
-
- Entry {index + 1} - -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- Entry Cost: ${calculateCost(entry).toFixed(2)} -
-
- ); + return ( +
+
+ Entry {index + 1} + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ Entry Cost:{" "} + + ${calculateCost(entry).toFixed(2)} + +
+
+ ); } function CollapsibleSection({ title, children, open, onToggle }) { - return ( -
- - {open &&
{children}
} -
- ); + return ( +
+ + {open &&
{children}
} +
+ ); } const defaultEntry = { - memory: 128, - requests: 1000000, - durationMs: 50, - bandwidthGB: 0, - reads: 1000000, - writes: 1000000, - storedGB: 0, + memory: 128, + requests: 1000000, + durationMs: 50, + bandwidthGB: 0, + reads: 1000000, + writes: 1000000, + storedGB: 0, }; export default function PricingCalculator() { - const [sections, setSections] = useState({ - function: { open: true, entries: [{ ...defaultEntry }] }, - container: { open: false, entries: [] }, - actor: { open: false, entries: [] }, - }); + const [sections, setSections] = useState({ + function: { open: true, entries: [{ ...defaultEntry }] }, + container: { open: false, entries: [] }, + actor: { open: false, entries: [] }, + }); - const handleAdd = (type) => { - setSections(s => ({ - ...s, - [type]: { ...s[type], entries: [...s[type].entries, { ...defaultEntry }] }, - })); - }; + const handleAdd = (type) => { + setSections((s) => ({ + ...s, + [type]: { + ...s[type], + entries: [...s[type].entries, { ...defaultEntry }], + }, + })); + }; - const handleRemove = (type, idx) => { - setSections(s => ({ - ...s, - [type]: { ...s[type], entries: s[type].entries.filter((_, i) => i !== idx) }, - })); - }; + const handleRemove = (type, idx) => { + setSections((s) => ({ + ...s, + [type]: { + ...s[type], + entries: s[type].entries.filter((_, i) => i !== idx), + }, + })); + }; - const handleChange = (type, idx, entry) => { - setSections(s => ({ - ...s, - [type]: { - ...s[type], - entries: s[type].entries.map((e, i) => (i === idx ? entry : e)), - }, - })); - }; + const handleChange = (type, idx, entry) => { + setSections((s) => ({ + ...s, + [type]: { + ...s[type], + entries: s[type].entries.map((e, i) => (i === idx ? entry : e)), + }, + })); + }; - const handleToggle = (type) => { - setSections(s => ({ - ...s, - [type]: { ...s[type], open: !s[type].open }, - })); - }; + const handleToggle = (type) => { + setSections((s) => ({ + ...s, + [type]: { ...s[type], open: !s[type].open }, + })); + }; - const totalCost = Object.values(sections).flatMap(s => s.entries).reduce((sum, entry) => sum + calculateCost(entry), 0); + const totalCost = Object.values(sections) + .flatMap((s) => s.entries) + .reduce((sum, entry) => sum + calculateCost(entry), 0); - return ( -
-

Estimate Your Monthly Cost

- {PRIMITIVES.map(p => ( - handleToggle(p.value)} - > - {sections[p.value].entries.map((entry, idx) => ( - handleChange(p.value, idx, e)} - onRemove={() => handleRemove(p.value, idx)} - /> - ))} - - - ))} -
- Estimated Monthly Cost: ${totalCost.toFixed(2)} -
-
- ); -} \ No newline at end of file + return ( +
+

+ Estimate Your Monthly Cost +

+ {PRIMITIVES.map((p) => ( + handleToggle(p.value)} + > + {sections[p.value].entries.map((entry, idx) => ( + handleChange(p.value, idx, e)} + onRemove={() => handleRemove(p.value, idx)} + /> + ))} + + + ))} +
+ Estimated Monthly Cost:{" "} + + ${totalCost.toFixed(2)} + +
+
+ ); +} diff --git a/site/src/components/mdx.jsx b/site/src/components/mdx.jsx index 00b6e77c65..160be0a512 100644 --- a/site/src/components/mdx.jsx +++ b/site/src/components/mdx.jsx @@ -71,8 +71,9 @@ export function EnvTokenClient() { return (

- In development, RIVET_TOKEN will use the development - token generated by rivet run. In production,  + In development, RIVET_TOKEN will use the + development token generated by rivet run. In + production,  RIVET_TOKEN will be automatically added by the CDN.

@@ -103,10 +104,11 @@ export function EnvTokenServer() { return (

- In development, RIVET_TOKEN will use the development token - generated by rivet run. In production,  - RIVET_TOKEN is automatically added to your environment by - Rivet. + In development, RIVET_TOKEN will use the + development token generated by rivet run. In + production,  + RIVET_TOKEN is automatically added to your + environment by Rivet.

More info:

@@ -131,10 +133,10 @@ export function PreRivetBranch() { return (

- The pre-rivet branch contains the source code of this - project without Rivet implemented, in contrast to the main{" "} - branch. View these side by side to get a good picture of what it takes - to integrate Rivet for your game. + The pre-rivet branch contains the source code of + this project without Rivet implemented, in contrast to the{" "} + main branch. View these side by side to get a good + picture of what it takes to integrate Rivet for your game.

); @@ -198,7 +200,10 @@ export function InstallCli() { return ( Make sure you have installed the Rivet CLI{" "} - + here . @@ -211,7 +216,9 @@ export function AutomateWithApi() {

Rivet's Cloud API can be managed with your{" "} - cloud token + + cloud token + . This is the same API we use internally and{" "} in the CLI @@ -220,8 +227,8 @@ export function AutomateWithApi() {

- The Cloud REST API is documented here. - You can also use the{" "} + The Cloud REST API is documented{" "} + here. You can also use the{" "} Dockerfile Crash Course {" "} - will teach you how to write your own Dockerfile quickly + will teach you how to write your own Dockerfile{" "} + quickly

  • - Join our Discord and - we'll write your Dockerfile for you! + Join our Discord{" "} + and we'll write your Dockerfile for you!
  • @@ -306,7 +314,9 @@ export const Card = ({ href, ...props }) => { }; export const CardGroup = ({ children }) => { - return
    {children}
    ; + return ( +
    {children}
    + ); }; export const SchemaPreview = ({ schema }) => { diff --git a/site/src/components/v2/FancyHeader.tsx b/site/src/components/v2/FancyHeader.tsx index 41db399638..e9237f5145 100644 --- a/site/src/components/v2/FancyHeader.tsx +++ b/site/src/components/v2/FancyHeader.tsx @@ -1,13 +1,13 @@ "use client"; import { DocsMobileNavigation } from "@/components/DocsMobileNavigation"; import logoUrl from "@/images/rivet-logos/icon-text-white.svg"; -import { Button, cn } from "@rivet-gg/components"; +import { cn } from "@rivet-gg/components"; import { Header as RivetHeader } from "@rivet-gg/components/header"; import { Icon, faDiscord } from "@rivet-gg/icons"; +import { AnimatePresence, motion } from "framer-motion"; import Image from "next/image"; import Link from "next/link"; import { type ReactNode, useEffect, useRef, useState } from "react"; -import { AnimatePresence, motion } from "framer-motion"; import { HeaderPopupProductMenu } from "../HeaderPopupProductMenu"; import { GitHubDropdown } from "./GitHubDropdown"; @@ -34,7 +34,11 @@ function TextNavItem({ )} > - + {children} @@ -124,7 +128,9 @@ export function FancyHeader({ support={
    - Sign In + + Sign In + Discord @@ -136,9 +142,18 @@ export function FancyHeader({ } links={
    - - - + + + @@ -180,21 +195,27 @@ export function FancyHeader({ setIsSubnavOpen(false)} - ariaCurrent={active === "docs" ? "page" : undefined} + ariaCurrent={ + active === "docs" ? "page" : undefined + } > Documentation setIsSubnavOpen(false)} - ariaCurrent={active === "cloud" ? "page" : undefined} + ariaCurrent={ + active === "cloud" ? "page" : undefined + } > Cloud setIsSubnavOpen(false)} - ariaCurrent={active === "blog" ? "page" : undefined} + ariaCurrent={ + active === "blog" ? "page" : undefined + } > Changelog @@ -217,13 +238,19 @@ export function FancyHeader({ {isSubnavOpen === "product" ? ( setIsSubnavOpen(false)} + onMouseLeave={() => + setIsSubnavOpen(false) + } className=" absolute inset-0" > {} @@ -17,15 +17,21 @@ function formatNumber(num: number): string { return num.toString(); } -export function GitHubDropdown({ - className, - ...props -}: GitHubDropdownProps) { - const [rivetStars, setRivetStars] = useState({ stars: 0, loading: true }); - const [rivetKitStars, setRivetKitStars] = useState({ stars: 0, loading: true }); +export function GitHubDropdown({ className, ...props }: GitHubDropdownProps) { + const [rivetStars, setRivetStars] = useState({ + stars: 0, + loading: true, + }); + const [rivetKitStars, setRivetKitStars] = useState({ + stars: 0, + loading: true, + }); const [isOpen, setIsOpen] = useState(false); - const fetchStars = async (repo: string, setter: (data: RepoData) => void) => { + const fetchStars = async ( + repo: string, + setter: (data: RepoData) => void, + ) => { const cacheKey = `github-stars-${repo}`; const cachedData = sessionStorage.getItem(cacheKey); @@ -38,7 +44,9 @@ export function GitHubDropdown({ } try { - const response = await fetch(`https://api.github.com/repos/${repo}`); + const response = await fetch( + `https://api.github.com/repos/${repo}`, + ); if (!response.ok) throw new Error("Failed to fetch"); const data = await response.json(); const newStars = data.stargazers_count; @@ -74,9 +82,9 @@ export function GitHubDropdown({