diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index 95590998590..728290838b2 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -494,6 +494,80 @@ Keep this plugin even after migration — it's not a Next-related shim, it's general protection against this entire class of bug. Just clear the allowlist when the underlying cycle is gone. +### `@sentry/nextjs` → `@sentry/react` alias + +`resolve.alias` in `vite.config.ts` rewrites the bare `@sentry/nextjs` +import to `compat/sentry-nextjs.ts`, which re-exports `@sentry/react` +(the same-version package `@sentry/nextjs` wraps on the client) plus +explicit stand-ins for the Next-only APIs (`captureRouterTransitionStart`, +`captureRequestError`, `withSentryConfig`). + +Why: `@sentry/nextjs`'s client entry imports +`next/dist/shared/lib/constants`, whose module scope evaluates +`...(process?.features?.typescript ? ['next.config.mts'] : [])`. +Optional chaining does **not** guard an undeclared `process` identifier, +so every built client chunk containing it (table editor was the canary) +threw `ReferenceError: process is not defined` at module load. Dev was +unaffected (dev pipeline shims `process`), so it only surfaced in the +production/test build. + +The alias also made the previous `@sentry/nextjs` SSR workarounds +(`ssr.noExternal` entry + `ssr.optimizeDeps.include`) obsolete — the id +is rewritten before SSR resolution, and `@sentry/react` ships real ESM. +App source keeps importing `@sentry/nextjs` so the Next build +(`build:next`) is untouched; drop the alias + shim together with the +Next build when the migration is done (switch imports to +`@sentry/react` directly). + +### GraphiQL Monaco workers: `setup-workers/webpack` → `setup-workers/vite` + +App source (`GraphiQLTab.tsx`) imports `graphiql/setup-workers/webpack`, +which registers `MonacoEnvironment.getWorker` using +`new Worker(new URL('monaco-editor/...', import.meta.url))` — the URL form +webpack/turbopack rewrites at build time. Vite doesn't rewrite bare module +specifiers inside `new URL(..., import.meta.url)`, so under the TanStack +build the worker URLs 404'd and Monaco fell back to running the `json`, +`editorWorkerService` and `graphql` workers on the main thread ("Could not +create web worker(s). Falling back to loading web worker code in main +thread" in the console). + +The `graphiqlViteWorkers` plugin in `vite.config.ts` resolves that import +to graphiql's own `setup-workers/vite` variant (same three workers via +Vite `?worker` imports) in client builds; SSR resolution is untouched. The +import specifier stays `.../webpack` in app source so the Next build keeps +working. The whole setup-workers chain is also in `optimizeDeps.exclude` — +the Rolldown dep optimizer can't load `?worker` ids +(`UNLOADABLE_DEPENDENCY`), so the modules go through the normal transform +pipeline where Vite's built-in worker plugin handles them. Drop the plugin +and the exclude, and switch the import to `graphiql/setup-workers/vite`, +when the Next build goes away. + +### Raw-text imports: `*.md` + `public/deno/*.d.ts` (`rawTextLoader`) + +Next's raw-loader rules (next.config.ts `turbopack.rules`) serve `*.md` +files and the Deno typings `public/deno/edge-runtime.d.ts` / +`public/deno/lib.deno.d.ts` as JS modules whose default export is the +file's text. The `rawTextLoader` plugin in `vite.config.ts` mirrors that +for the Vite pipeline: + +- `*.md` — plain `transform` (used by + `static-data/integrations/*/overview.md` via the literal-import registry + in `static-data/integrations/overviews.ts`). +- The two Deno `.d.ts` files (used by `components/ui/AIEditor` as Monaco + extra libs for edge-function editors) — an exact-specifier allowlist + resolved to `\0`-virtual ids and served from a `load` hook. They can't go + through `transform`: Rolldown's native dep scanner skips JS plugin hooks + and hard-fails parsing TS _declaration_ syntax (`get stdin(): ...;`) as + runtime TS, which killed dependency pre-bundling wholesale. The previous + `/* @vite-ignore */` hack kept the scanner away but also meant the + imports failed at runtime, silently dropping Deno type hints in the + TanStack build. Do NOT widen the allowlist to `*.d.ts` — hijacking + declaration-file resolution globally would corrupt packages that ship + `.d.ts` next to their JS. The `as string` casts on the import specifiers + in `AIEditor/index.tsx` keep tsc from resolving the `.d.ts` files as + declaration files (TS2846) while erasing to plain literals both bundlers + statically analyze. + ### Other build-side migration changes - `pnpm-workspace.yaml` catalog now includes `@tanstack/react-router`, diff --git a/apps/studio/compat/next/link.tsx b/apps/studio/compat/next/link.tsx index c662e623466..cbb68e892ce 100644 --- a/apps/studio/compat/next/link.tsx +++ b/apps/studio/compat/next/link.tsx @@ -11,6 +11,8 @@ import { type Ref, } from 'react' +import { splitInternalUrl } from '@/lib/internal-url' + // Next's Link accepts either a string `href` or a `UrlObject` // ({pathname, query, hash}). Workspace source does both — flatten // `UrlObject` into `pathname?search#hash` first so the TanStack `to` @@ -62,92 +64,14 @@ function resolveHref(href: string | UrlObject): string { return `${pathname}${search}${hash}` } -// Inlined at build time via Vite's `define`. Must agree with Vite `base` -// and `tanstackStart({ router: { basepath } })`. Empty string when no -// basePath is configured. Used to strip a duplicate prefix in -// `splitInternalUrl` below — see the comment there. -const NEXT_PUBLIC_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '' - // TanStack Link's `to` prop is a route-pattern path; query params and hash -// must be passed separately via `search` / `hash`. Studio code (and Next's -// own contract) routinely passes one of three href shapes: -// 1. a relative path like `/project/abc/editor/123?schema=public` -// 2. a same-origin absolute URL produced by `new URL(...).toString()`, -// e.g. `http://localhost:8082/project/abc/editor/123?schema=public` -// (this is what `buildTableEditorUrl` does) -// 3. a genuinely external URL like `https://supabase.com/docs`. -// -// If we forward any of these straight through to TanStack as `to`, TanStack -// either fails to match a known route pattern (#1 with query) or treats -// the whole thing as external (#2) and falls back to native browser -// navigation — which the user sees as a full page reload. -// -// Split into three parts: pathname, search, hash. Same-origin absolute -// URLs are normalised to a relative path. Cross-origin URLs are left -// alone so TanStack's external-link path handles them. -// -// basePath quirk: TanStack's `to` is **basepath-relative** — given -// `basepath: '/dashboard'` and `to: '/foo'`, TanStack builds the href -// `/dashboard/foo`. Next's contract treats `href` as the **full path -// from app root including basePath**, and studio code routinely -// pre-prefixes BASE_PATH (e.g. `buildTableEditorUrl` calls -// `new URL(`${BASE_PATH}/project/.../editor/...`, location.origin)`). -// Forwarding the BASE_PATH-prefixed pathname as `to` makes TanStack -// double-prefix it (`/dashboard/dashboard/project/...`). Strip the -// basePath when we see it, so what we hand TanStack is always -// basepath-relative. -function splitInternalUrl(url: string): { - to: string - search?: Record - hash?: string -} { - // Try to detect cross-origin absolute URLs cheaply before paying for a - // full parse. Protocol-relative URLs (`//host/...`) are always external. - if (url.startsWith('//')) { - return { to: url } - } - - // Use the document origin as the parse base so relative inputs resolve. - // SSR has no `location`; fall back to a placeholder host that won't ever - // collide with a real one. - const base = - typeof window !== 'undefined' && window.location ? window.location.origin : 'http://_/' - - let parsed: URL - try { - parsed = new URL(url, base) - } catch { - return { to: url } - } - - // Cross-origin → leave for TanStack to handle as external. - if ( - typeof window !== 'undefined' && - window.location && - parsed.origin !== window.location.origin - ) { - return { to: url } - } - - let pathname = parsed.pathname - // Strip a leading basePath segment so we hand TanStack a basepath- - // relative path. Match `/dashboard` exactly OR `/dashboard/...`; don't - // strip a coincidental prefix like `/dashboard-other`. - if ( - NEXT_PUBLIC_BASE_PATH && - (pathname === NEXT_PUBLIC_BASE_PATH || pathname.startsWith(`${NEXT_PUBLIC_BASE_PATH}/`)) - ) { - pathname = pathname.slice(NEXT_PUBLIC_BASE_PATH.length) || '/' - } - - const search = Object.fromEntries(parsed.searchParams) - const hash = parsed.hash || undefined - return { - to: pathname, - search: Object.keys(search).length > 0 ? search : undefined, - hash, - } -} +// must be passed separately via `search` / `hash`. If we forward an href +// straight through to TanStack as `to`, TanStack either mangles the query +// (path interpolation percent-decodes it and strips control characters) or +// treats a same-origin absolute URL as external and falls back to native +// browser navigation — which the user sees as a full page reload. Splitting +// (plus origin/basePath normalisation) lives in `splitInternalUrl` +// (@/lib/internal-url), shared with the next/router shim. // Next: prefetch=true|"auto" → eagerly preload; false → never; default // in production = true. TanStack's preload values are "intent" (on hover/ diff --git a/apps/studio/compat/next/navigation.ts b/apps/studio/compat/next/navigation.ts index 576693a1d9b..6b5da9328aa 100644 --- a/apps/studio/compat/next/navigation.ts +++ b/apps/studio/compat/next/navigation.ts @@ -3,9 +3,12 @@ import { useMatches, useParams as useTanStackParams, useRouter as useTanStackRouter, + type AnyRouter, } from '@tanstack/react-router' import { useMemo } from 'react' +import { splitInternalUrl } from '@/lib/internal-url' + // `next/navigation` is the App Router hook surface (Next 13+). Studio is // still pages-based, so most of these are entry-points that arrive via // stray imports or shared packages — we keep the surface comprehensive @@ -29,13 +32,27 @@ export function useRouter() { return useMemo( () => ({ + // Split query/hash out of the href — embedding them in TanStack's + // `to` runs the whole string through path interpolation, which + // percent-decodes it and strips control characters from query values + // (see @/lib/internal-url). `search: {}` / `hash: ''` clear stale + // state, matching Next's full-href navigation semantics. + // + // The `` type arguments opt out of the registered + // route tree's strict typing: Next-style hrefs are free-form strings + // that can't satisfy the route-path union at compile time. push: (href: string, _options?: NavigateOptions) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - router.navigate({ to: href as any }) + const { to, search, hash } = splitInternalUrl(href) + router.navigate({ to, search: search ?? {}, hash: hash ?? '' }) }, replace: (href: string, _options?: NavigateOptions) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - router.navigate({ to: href as any, replace: true }) + const { to, search, hash } = splitInternalUrl(href) + router.navigate({ + to, + search: search ?? {}, + hash: hash ?? '', + replace: true, + }) }, // App Router's refresh() refetches the current route's data without // re-mounting the React tree. Closest TanStack equivalent is @@ -50,10 +67,14 @@ export function useRouter() { if (typeof window !== 'undefined') window.history.forward() }, prefetch: (href: string, _options?: PrefetchOptions) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - router.preloadRoute({ to: href as any }).catch(() => { - // Match Next's fire-and-forget contract. - }) + const { to, search, hash } = splitInternalUrl(href) + // `` (TFrom, TTo) loosens `to` to a plain string for + // the same free-form-href reason as `navigate` above. + router + .preloadRoute({ to, search: search ?? {}, hash: hash ?? '' }) + .catch(() => { + // Match Next's fire-and-forget contract. + }) }, }), [router] diff --git a/apps/studio/compat/next/router.test.ts b/apps/studio/compat/next/router.test.ts index f5b3aa67323..16c0ad8a56f 100644 --- a/apps/studio/compat/next/router.test.ts +++ b/apps/studio/compat/next/router.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { resolveUrl } from './router' +import { resolveSearchOrHashOnlyTarget, resolveUrl, withDefaultPathname } from './router' describe('resolveUrl (next/router compat shim)', () => { it('returns string URLs untouched', () => { @@ -58,3 +58,79 @@ describe('resolveUrl (next/router compat shim)', () => { ).toBe('/project/x?a=1') }) }) + +describe('resolveSearchOrHashOnlyTarget (next/router compat shim)', () => { + it('prefixes the current pathname on a ?-only target', () => { + // The regression we fixed: `push({ query })` with no pathname produced a + // relative `?...` that TanStack resolved by appending — landing on + // `/advisors/security/?preset=...` (trailing slash injected). + expect(resolveSearchOrHashOnlyTarget('?preset=x', '/advisors/security')).toBe( + '/advisors/security?preset=x' + ) + }) + + it('prefixes the current pathname on a #-only target', () => { + expect(resolveSearchOrHashOnlyTarget('#invoices', '/org/slug/billing')).toBe( + '/org/slug/billing#invoices' + ) + }) + + it('strips a trailing slash from the current pathname, but keeps the root "/"', () => { + expect(resolveSearchOrHashOnlyTarget('?a=1', '/auth/providers/')).toBe('/auth/providers?a=1') + expect(resolveSearchOrHashOnlyTarget('?a=1', '/')).toBe('/?a=1') + expect(resolveSearchOrHashOnlyTarget('?a=1', '')).toBe('/?a=1') + }) + + it('leaves targets with a pathname untouched', () => { + expect(resolveSearchOrHashOnlyTarget('/project/abc?x=1', '/elsewhere')).toBe('/project/abc?x=1') + expect(resolveSearchOrHashOnlyTarget('/project/abc', '/elsewhere')).toBe('/project/abc') + }) +}) + +describe('withDefaultPathname (next/router compat shim)', () => { + it('fills a missing pathname from the current route pattern so path params are consumed', () => { + // The regression we fixed: `push({ query: { ...router.query, filter } })` + // with no pathname leaked `ref`/`id` into the query string + // (`/editor/17597/?ref=...&id=17597&filter=...`) because nothing + // interpolated them back into the path. + const target = withDefaultPathname( + { query: { ref: 'abc', id: '17597', filter: 'note:eq:hi' } }, + '/project/[ref]/editor/[id]', + { ref: 'abc', id: '17597' } + ) + expect(resolveUrl(target)).toBe('/project/abc/editor/17597?filter=note%3Aeq%3Ahi') + }) + + it('backfills params the caller omitted from the current route params', () => { + const target = withDefaultPathname( + { query: { preset: 'WARN' } }, + '/project/[ref]/advisors/security', + { ref: 'abc' } + ) + expect(resolveUrl(target)).toBe('/project/abc/advisors/security?preset=WARN') + }) + + it('caller-provided query values win over backfilled params', () => { + const target = withDefaultPathname({ query: { ref: 'other', page: '2' } }, '/project/[ref]', { + ref: 'abc', + }) + expect(resolveUrl(target)).toBe('/project/other?page=2') + }) + + it('never backfills the TanStack _splat param', () => { + const target = withDefaultPathname({ query: { a: '1' } }, '/org/_/[[...routeSlug]]', { + _splat: 'x/y', + }) + expect(resolveUrl(target)).toBe('/org/_/?a=1') + }) + + it('leaves string URLs, explicit pathnames, and raw-string queries untouched', () => { + expect(withDefaultPathname('/x?a=1', '/p/[ref]', { ref: 'r' })).toBe('/x?a=1') + expect( + withDefaultPathname({ pathname: '/y', query: { a: '1' } }, '/p/[ref]', { ref: 'r' }) + ).toEqual({ pathname: '/y', query: { a: '1' } }) + expect(withDefaultPathname({ query: 'a=1' }, '/p/[ref]', { ref: 'r' })).toEqual({ + query: 'a=1', + }) + }) +}) diff --git a/apps/studio/compat/next/router.ts b/apps/studio/compat/next/router.ts index 479b4234e84..d2ae293151c 100644 --- a/apps/studio/compat/next/router.ts +++ b/apps/studio/compat/next/router.ts @@ -4,10 +4,12 @@ import { useParams, useSearch, useRouter as useTanStackRouter, + type AnyRouter, } from '@tanstack/react-router' import { useMemo } from 'react' import { getRouterEventsProxy } from './_router-events' +import { splitInternalUrl } from '@/lib/internal-url' // Next's pages-router exposes `router.pathname` as the route *pattern* // (e.g. `/project/[ref]/sql/[id]`), not the resolved URL. TanStack's @@ -107,6 +109,45 @@ function interpolatePathname( return { pathname: interpolated, query: rest } } +// TanStack resolves a `?`- or `#`-only relative `to` by *appending* it to the +// current path, injecting a trailing slash: navigating to `?preset=x` from +// `/advisors/security` lands on `/advisors/security/?preset=x`. Next resolved +// these against the current pathname. Prefix it explicitly (trailing slash +// stripped; root stays `/`) so `push({ query })` with no pathname stays on +// the exact current path. +// Exported for unit tests (see router.test.ts) — not part of the Next surface. +export function resolveSearchOrHashOnlyTarget(to: string, currentPathname: string): string { + if (!to.startsWith('?') && !to.startsWith('#')) return to + let base = currentPathname || '/' + if (base.length > 1 && base.endsWith('/')) base = base.slice(0, -1) + return `${base}${to}` +} + +// Next resolves a pathname-less UrlObject against the *current route +// pattern*, re-consuming dynamic params from `query` into the path — e.g. +// `push({ query: { ...router.query, preset } })` on `/project/[ref]/advisors` +// stays on `/project//advisors?preset=…`. Because the shim's +// `router.query` merges path params in (Next shape), skipping this would leak +// `ref`/`id` into the query string. Params the caller didn't include are +// backfilled from the current route's params (minus TanStack's `_splat`, +// which no bracket segment can consume) so partial `{ query }` pushes stay +// on-page instead of producing empty path segments. +// Exported for unit tests (see router.test.ts) — not part of the Next surface. +export function withDefaultPathname( + url: string | UrlObject, + currentPathPattern: string, + currentParams: Record +): string | UrlObject { + if (typeof url === 'string' || url.pathname != null) return url + if (!url.query || typeof url.query !== 'object') return url + const { _splat, ...paramsWithoutSplat } = currentParams + return { + ...url, + pathname: currentPathPattern, + query: { ...paramsWithoutSplat, ...url.query }, + } +} + // Exported for unit tests (see router.test.ts) — not part of the Next surface. export function resolveUrl(url: string | UrlObject): string { if (typeof url === 'string') return url @@ -135,8 +176,8 @@ export function resolveUrl(url: string | UrlObject): string { // `basepath: '/dashboard'` and `to: '/foo'`, it produces `/dashboard/foo`. // So we strip the origin AND the basePath when present; otherwise // `router.push('/dashboard/...')` would double-prefix to -// `/dashboard/dashboard/...`. Mirrors the equivalent logic in the -// next/link shim's `splitInternalUrl`. Cross-origin URLs pass through +// `/dashboard/dashboard/...`. Mirrors the equivalent logic in +// `splitInternalUrl` (@/lib/internal-url). Cross-origin URLs pass through // untouched so TanStack hands them to the browser as external. const NEXT_PUBLIC_BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '' @@ -231,9 +272,33 @@ export function useRouter() { // It's intentionally not part of Next's public TransitionOptions. options?: TransitionOptions & { _replace?: boolean } ): Promise => { - const to = toRelativeSameOrigin(resolveUrl(url)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await router.navigate({ to: to as any, replace: options?._replace }) + // `location.pathname` is already basepath-stripped by TanStack, so the + // prefixed target stays basepath-relative like every other `to`. + const target = resolveSearchOrHashOnlyTarget( + // `useParams({ strict: false })` types as possibly-undefined; treat + // "no params" as an empty record for backfilling. + toRelativeSameOrigin(resolveUrl(withDefaultPathname(url, pathPattern, params ?? {}))), + location.pathname + ) + // Never embed `?query`/`#hash` inside TanStack's `to` — router-core + // runs the whole string through path interpolation, which percent- + // decodes it and strips control characters (dropping `%0A` newlines + // from values like the Logs Explorer's `s` SQL param). Split into + // { to, search, hash } instead; the target is already relative and + // basepath-stripped, so splitInternalUrl's own origin/basePath + // normalisation is a no-op here. `search: {}` clears the query, + // matching Next's push-without-query semantics; same for `hash: ''`. + // + // The `` type arguments opt out of the registered + // route tree's strict typing: Next-style hrefs are free-form strings + // that can't satisfy the route-path union at compile time. + const { to, search, hash } = splitInternalUrl(target) + await router.navigate({ + to, + search: search ?? {}, + hash: hash ?? '', + replace: options?._replace, + }) return true } @@ -308,8 +373,16 @@ export function useRouter() { _options?: PrefetchOptions ): Promise => { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await router.preloadRoute({ to: url as any }) + // Split like navigate() above so a query string in the href + // preloads the real target instead of a path-mangled one. + // `` (TFrom, TTo) loosens `to` to a plain string + // for the same free-form-href reason as `navigate` above. + const { to, search, hash } = splitInternalUrl(toRelativeSameOrigin(url)) + await router.preloadRoute({ + to, + search: search ?? {}, + hash: hash ?? '', + }) } catch { // Next's prefetch is fire-and-forget; swallow resolution errors // (e.g. unknown route) so callers don't have to guard. diff --git a/apps/studio/compat/sentry-nextjs.ts b/apps/studio/compat/sentry-nextjs.ts new file mode 100644 index 00000000000..8666c90b2ac --- /dev/null +++ b/apps/studio/compat/sentry-nextjs.ts @@ -0,0 +1,62 @@ +/** + * TanStack-build replacement for `@sentry/nextjs`. + * + * `@sentry/nextjs`'s client entry imports `next/dist/shared/lib/constants`, + * which evaluates `...(process?.features?.typescript ? ['next.config.mts'] : [])` + * at module scope. Optional chaining does NOT guard an undeclared `process` + * identifier in the browser, so any built chunk containing it throws + * `ReferenceError: process is not defined` at load time (observed on the + * table editor route). The Vite dev server shims `process`, so the crash + * only surfaces in the production/test build. + * + * vite.config.ts aliases `@sentry/nextjs` to this module for the TanStack + * build only — app source keeps importing `@sentry/nextjs`, and the Next + * build (`build:next`) is untouched. + * + * `@sentry/nextjs` re-exports `@sentry/react` (same version, 10.x) and adds + * Next-specific helpers on top. Everything Studio uses at runtime — + * `captureException`, `captureMessage`, `startSpan`, `withScope`, `setTag`, + * `setUser`, `init`, and the `Event` / `Breadcrumb` / `StackFrame` types — + * exists in `@sentry/react`, so re-export the lot. + */ +import { captureException } from '@sentry/react' + +// eslint-disable-next-line barrel-files/avoid-re-export-all -- compat shim: its whole job is to mirror @sentry/react's surface +export * from '@sentry/react' + +// --------------------------------------------------------------------------- +// Next-specific APIs with no `@sentry/react` equivalent. None of them are in +// the TanStack/Vite module graph today (they're only referenced from +// Next-convention files: instrumentation-client.ts, instrumentation.ts and +// next.config.ts), but provide explicit stand-ins so the alias can never +// produce an `undefined is not a function` crash if one leaks in later. +// --------------------------------------------------------------------------- + +/** + * `@sentry/nextjs`'s hook for Next's `onRouterTransitionStart` instrumentation + * event. TanStack Router navigation spans would instead come from + * `Sentry.tanstackRouterBrowserTracingIntegration` — no-op here. + */ +export function captureRouterTransitionStart(_href: string, _navigationType: string): void {} + +/** + * `@sentry/nextjs`'s hook for Next's `onRequestError` server instrumentation. + * Report the error through the regular capture pipeline instead. + */ +export function captureRequestError( + error: unknown, + _request: unknown, + _errorContext: unknown +): void { + // `captureException` from `@sentry/react` is isomorphic (no-op without an + // initialized client), so this is safe on both server and client. + captureException(error) +} + +/** + * Build-time-only wrapper for next.config — meaningless under Vite. Return + * the config unchanged so any accidental usage is inert. + */ +export function withSentryConfig(config: T, ..._options: unknown[]): T { + return config +} diff --git a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx index 98b00272f4c..fdaa39ad42a 100644 --- a/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx +++ b/apps/studio/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel.tsx @@ -1,4 +1,3 @@ -import { basename } from 'path' import { IS_PLATFORM } from 'common' import { Circle, Code, Minus, Plus, Wind } from 'lucide-react' import Link from 'next/link' @@ -7,10 +6,11 @@ import { Card, CardContent, CardHeader, CardTitle, cn, Skeleton } from 'ui' import { DiffEditor } from '@/components/ui/DiffEditor' import type { EdgeFunctionBodyData } from '@/data/edge-functions/edge-function-body-query' -import type { - EdgeFunctionsDiffResult, - FileInfo, - FileStatus, +import { + fileKey, + type EdgeFunctionsDiffResult, + type FileInfo, + type FileStatus, } from '@/hooks/branches/useEdgeFunctionsDiff' import { EMPTY_ARR } from '@/lib/void' @@ -31,9 +31,6 @@ interface FunctionDiffProps { fileInfos: FileInfo[] } -// Helper to canonicalize file identifiers to prevent mismatch due to differing root paths -const fileKey = (fullPath: string) => basename(fullPath) - // Helper to get the status color for file indicators const getStatusColor = (status: FileStatus): string => { switch (status) { diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx index e5fcb83e8e5..15b26ec07df 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react' import { Markdown } from 'ui-patterns/Markdown' +import { loadIntegrationOverview } from '@/static-data/integrations/overviews' + interface MarkdownContentProps { content: string | null | undefined integrationId?: string @@ -20,9 +22,9 @@ export const MarkdownContent = ({ if (!integrationId || remoteContent) return let cancelled = false - import(`@/static-data/integrations/${integrationId}/overview.md`) - .then((module) => { - if (!cancelled) setLocalContent(String(module.default)) + loadIntegrationOverview(integrationId) + .then((markdown) => { + if (!cancelled && markdown !== null) setLocalContent(markdown) }) .catch((error) => console.error('Error loading markdown:', error)) diff --git a/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx b/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx index 45a9c5d1f22..b156d96ad94 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react' import { cn } from 'ui' import { Markdown } from '@/components/interfaces/Markdown' +import { loadIntegrationOverview } from '@/static-data/integrations/overviews' const CHAR_LIMIT = 500 // Adjust this number as needed @@ -17,9 +18,16 @@ export const MarkdownContent = ({ const [isExpanded, setIsExpanded] = useState(initiallyExpanded ?? false) useEffect(() => { - import(`@/static-data/integrations/${integrationId}/overview.md`) - .then((module) => setContent(String(module.default))) + let cancelled = false + loadIntegrationOverview(integrationId) + .then((markdown) => { + if (!cancelled && markdown !== null) setContent(markdown) + }) .catch((error) => console.error('Error loading markdown:', error)) + + return () => { + cancelled = true + } }, [integrationId]) const displayContent = isExpanded ? content : content.slice(0, CHAR_LIMIT) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx index cde130f2aab..fb40475c709 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx @@ -438,7 +438,12 @@ export const InfrastructureActivity = () => { title="Looking for actual disk activity?" description="The chart above shows your remaining burst budget, not real disk throughput. For detailed read/write IOPS and throughput charts, head to the Database Observability page." > -