diff --git a/Tiltfile b/Tiltfile index c4c75965b..306fc0427 100644 --- a/Tiltfile +++ b/Tiltfile @@ -26,10 +26,10 @@ local_resource( local_resource( "frontend-serve", labels=["frontend"], - deps=["frontend/src"], + deps=["studio/src"], resource_deps=["node_modules", "api-dist"], serve_cmd="npm run dev", - serve_dir="frontend", + serve_dir="studio", trigger_mode=TRIGGER_MODE_MANUAL, ) @@ -57,4 +57,4 @@ local_resource( resource_deps=["node_modules", "db-generate", "db-migrate"], serve_cmd="npm run dev", serve_dir="api", -) \ No newline at end of file +) diff --git a/api/src/serve-frontend-build.ts b/api/src/serve-frontend-build.ts index 75b25b033..f1da78a1a 100644 --- a/api/src/serve-frontend-build.ts +++ b/api/src/serve-frontend-build.ts @@ -17,10 +17,10 @@ const __dirname = dirname(__filename); */ const POSSIBLE_FRONTEND_BUILD_PATHS = [ /* For when we `npm run dev` from the api folder */ - path.resolve(__dirname, "..", "..", "frontend", "dist"), + path.resolve(__dirname, "..", "..", "studio", "dist"), /* For when we run via `npx` **NOTE** - This path assumes we are running from the `dist` folder in a compiled version of the api, + This path assumes we are running from the `dist` folder in a compiled version of the api, and that the frontend build has been copy pasted into the selfsame `dist` folder. */ path.resolve(__dirname, "..", "..", "dist"), diff --git a/biome.jsonc b/biome.jsonc index 1d8f5f795..361172bf1 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -41,9 +41,16 @@ } }, { - "include": ["frontend"], + "include": ["studio"], "linter": { - "enabled": false + "enabled": true, + "rules": { + "suspicious": { + "noArrayIndexKey": { + "level": "off" + } + } + } } }, { diff --git a/package.json b/package.json index 79c3b19e7..b6ce1d152 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dev:frontend": "pnpm --filter @fiberplane/studio-frontend dev", "clean:fpx-studio": "pnpm run clean:api && pnpm run clean:frontend", "clean:api": "rimraf api/dist", - "clean:frontend": "rimraf frontend/dist", + "clean:frontend": "rimraf studio/dist", "format": "biome check . --write", "lint": "pnpm --recursive lint" }, diff --git a/packages/client-library-otel/src/async-hooks/AbstractAsyncHooksContextManager.ts b/packages/client-library-otel/src/async-hooks/AbstractAsyncHooksContextManager.ts index a77bb9a40..2ad03b63a 100644 --- a/packages/client-library-otel/src/async-hooks/AbstractAsyncHooksContextManager.ts +++ b/packages/client-library-otel/src/async-hooks/AbstractAsyncHooksContextManager.ts @@ -101,13 +101,17 @@ export abstract class AbstractAsyncHooksContextManager ee: T, ): T { const map = this._getPatchMap(ee); - if (map !== undefined) return ee; + if (map !== undefined) { + return ee; + } this._createPatchMap(ee); // patch methods that add a listener to propagate context // biome-ignore lint/complexity/noForEach: this is from the original code ADD_LISTENER_METHODS.forEach((methodName) => { - if (ee[methodName] === undefined) return; + if (ee[methodName] === undefined) { + return; + } ee[methodName] = this._patchAddListener(ee, ee[methodName], context); }); // patch methods that remove a listener diff --git a/studio/.eslintrc.cjs b/studio/.eslintrc.cjs deleted file mode 100644 index 6e8698b72..000000000 --- a/studio/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - ignorePatterns: ["dist", ".eslintrc.cjs"], - parser: "@typescript-eslint/parser", - plugins: ["react-refresh"], - rules: { - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, -}; diff --git a/studio/biome.jsonc b/studio/biome.jsonc new file mode 100644 index 000000000..873f0a7eb --- /dev/null +++ b/studio/biome.jsonc @@ -0,0 +1,22 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "extends": ["../biome.jsonc"], + "files": { + "ignore": ["dist", "node_modules"] + }, + "overrides": [ + { + "include": ["src"], + "linter": { + "enabled": true, + "rules": { + "suspicious": { + "noArrayIndexKey": { + "level": "off" + } + } + } + } + } + ] +} diff --git a/studio/package.json b/studio/package.json index 9335d37e4..50d8d86bd 100644 --- a/studio/package.json +++ b/studio/package.json @@ -7,9 +7,8 @@ "dev": "vite", "build": "tsc && vite build", "typecheck": "tsc --noEmit", - "lint": "biome lint . && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && tsc", - "lint:ci": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && tsc", "format": "biome check . --write", + "lint": "biome lint . && pnpm run typecheck", "preview": "vite preview", "deploy": "pnpm run build && wrangler pages deploy dist", "test": "vitest --run" diff --git a/studio/src/App.tsx b/studio/src/App.tsx index b3480877d..14c903af3 100644 --- a/studio/src/App.tsx +++ b/studio/src/App.tsx @@ -1,6 +1,6 @@ import { QueryClientProvider, queryClient } from "@/queries"; import { TooltipProvider } from "@radix-ui/react-tooltip"; -import { ReactNode, useEffect } from "react"; +import { type ReactNode, useEffect } from "react"; import { Route, BrowserRouter as Router, diff --git a/studio/src/Layout.tsx b/studio/src/Layout.tsx index 004e30b67..0b708174e 100644 --- a/studio/src/Layout.tsx +++ b/studio/src/Layout.tsx @@ -1,6 +1,6 @@ import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; import type React from "react"; -import { ComponentProps } from "react"; +import type { ComponentProps } from "react"; import { NavLink } from "react-router-dom"; import FpxIcon from "./assets/fpx.svg"; import { WebhoncBadge } from "./components/WebhoncBadge"; diff --git a/studio/src/components/Ping.tsx b/studio/src/components/Ping.tsx index 2409cdbb1..e136d17e2 100644 --- a/studio/src/components/Ping.tsx +++ b/studio/src/components/Ping.tsx @@ -4,8 +4,8 @@ const Ping: React.FC<{ className?: string }> = ({ className }) => { return (
-
-
+
+
Awaiting signal...
diff --git a/studio/src/components/WebhoncBadge/WebhoncBadge.tsx b/studio/src/components/WebhoncBadge/WebhoncBadge.tsx index 45cf6ec6c..a36c02158 100644 --- a/studio/src/components/WebhoncBadge/WebhoncBadge.tsx +++ b/studio/src/components/WebhoncBadge/WebhoncBadge.tsx @@ -23,7 +23,7 @@ export function WebhoncBadge() { "bg-muted/20 text-muted-foreground": !url, }, )} - onClick={() => copyToClipboard(url!)} + onClick={() => url && copyToClipboard(url)} onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} title="Copy public URL to clipboard" diff --git a/studio/src/components/ui/DataTable.tsx b/studio/src/components/ui/DataTable.tsx index 2fdd42f72..b59becad9 100644 --- a/studio/src/components/ui/DataTable.tsx +++ b/studio/src/components/ui/DataTable.tsx @@ -10,11 +10,11 @@ import { isModifierKeyPressed } from "@/utils"; import { useHandler } from "@fiberplane/hooks"; import { type ColumnDef, - PaginationState, + type PaginationState, type Row, type RowData, - RowModel, - Table as TableType, + type RowModel, + type Table as TableType, flexRender, getCoreRowModel, useReactTable, @@ -115,8 +115,13 @@ export function DataTable({ const handleNextRow = useHandler(() => { setSelectedRowIndex((prevIndex) => { - if (prevIndex === null) return 0; - if (prevIndex + 1 >= rows.length) return prevIndex; + if (prevIndex === null) { + return 0; + } + + if (prevIndex + 1 >= rows.length) { + return prevIndex; + } return prevIndex + 1; }); @@ -124,8 +129,14 @@ export function DataTable({ const handlePrevRow = useHandler(() => { setSelectedRowIndex((prevIndex) => { - if (prevIndex === null) return 0; - if (prevIndex - 1 < 0) return prevIndex; + if (prevIndex === null) { + return 0; + } + + if (prevIndex - 1 < 0) { + return prevIndex; + } + return prevIndex - 1; }); }); diff --git a/studio/src/components/ui/button/Button.tsx b/studio/src/components/ui/button/Button.tsx index 526b037c2..4d6428249 100644 --- a/studio/src/components/ui/button/Button.tsx +++ b/studio/src/components/ui/button/Button.tsx @@ -1,5 +1,5 @@ import { Slot } from "@radix-ui/react-slot"; -import { type VariantProps } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import * as React from "react"; import { cn } from "@/utils"; diff --git a/studio/src/components/ui/command.tsx b/studio/src/components/ui/command.tsx index 0d6dd290f..dfbd1b6ef 100644 --- a/studio/src/components/ui/command.tsx +++ b/studio/src/components/ui/command.tsx @@ -1,4 +1,4 @@ -import { type DialogProps } from "@radix-ui/react-dialog"; +import type { DialogProps } from "@radix-ui/react-dialog"; import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; import { Command as CommandPrimitive } from "cmdk"; import * as React from "react"; diff --git a/studio/src/components/ui/form.tsx b/studio/src/components/ui/form.tsx index b70bbc819..f934a0cac 100644 --- a/studio/src/components/ui/form.tsx +++ b/studio/src/components/ui/form.tsx @@ -1,11 +1,11 @@ -import * as LabelPrimitive from "@radix-ui/react-label"; +import type * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; import * as React from "react"; import { Controller, - ControllerProps, - FieldPath, - FieldValues, + type ControllerProps, + type FieldPath, + type FieldValues, FormProvider, useFormContext, } from "react-hook-form"; diff --git a/studio/src/components/ui/pagination.tsx b/studio/src/components/ui/pagination.tsx index d312085ac..30ff8db6e 100644 --- a/studio/src/components/ui/pagination.tsx +++ b/studio/src/components/ui/pagination.tsx @@ -5,7 +5,7 @@ import { } from "@radix-ui/react-icons"; import * as React from "react"; -import { ButtonProps } from "@/components/ui/button"; +import type { ButtonProps } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button/variants"; import { cn } from "@/utils"; diff --git a/studio/src/components/ui/toaster.tsx b/studio/src/components/ui/toaster.tsx index 5ff57090a..aceda5e37 100644 --- a/studio/src/components/ui/toaster.tsx +++ b/studio/src/components/ui/toaster.tsx @@ -13,20 +13,16 @@ export function Toaster() { return ( - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
- {title && {title}} - {description && ( - {description} - )} -
- {action} - -
- ); - })} + {toasts.map(({ id, title, description, action, ...props }) => ( + +
+ {title && {title}} + {description && {description}} +
+ {action} + +
+ ))}
); diff --git a/studio/src/components/ui/use-toast.ts b/studio/src/components/ui/use-toast.ts index 5a03b78a0..32609bb62 100644 --- a/studio/src/components/ui/use-toast.ts +++ b/studio/src/components/ui/use-toast.ts @@ -93,9 +93,9 @@ export const reducer = (state: State, action: Action): State => { if (toastId) { addToRemoveQueue(toastId); } else { - state.toasts.forEach((toast) => { + for (const toast of state.toasts) { addToRemoveQueue(toast.id); - }); + } } return { @@ -130,9 +130,10 @@ let memoryState: State = { toasts: [] }; function dispatch(action: Action) { memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { + + for (const listener of listeners) { listener(memoryState); - }); + } } type Toast = Omit; @@ -154,7 +155,9 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss(); + if (!open) { + dismiss(); + } }, }, }); @@ -177,7 +180,7 @@ function useToast() { listeners.splice(index, 1); } }; - }, [state]); + }, []); return { ...state, diff --git a/studio/src/main.tsx b/studio/src/main.tsx index f25366e5e..d2d4f0666 100644 --- a/studio/src/main.tsx +++ b/studio/src/main.tsx @@ -3,7 +3,12 @@ import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; -ReactDOM.createRoot(document.getElementById("root")!).render( +const root = document.getElementById("root"); +if (!root) { + throw new Error("Application failed to start: missing root element"); +} + +ReactDOM.createRoot(root).render( , diff --git a/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/RequestDetailsPageV2Content.tsx b/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/RequestDetailsPageV2Content.tsx index 755643490..354392748 100644 --- a/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/RequestDetailsPageV2Content.tsx +++ b/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/RequestDetailsPageV2Content.tsx @@ -12,13 +12,13 @@ import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; -import { MizuOrphanLog } from "@/queries"; -import { OtelSpan } from "@/queries/traces-otel"; +import type { MizuOrphanLog } from "@/queries"; +import type { OtelSpan } from "@/queries/traces-otel"; import { cn } from "@/utils"; import { EmptyState } from "../EmptyState"; import { TraceDetailsTimeline, TraceDetailsV2 } from "../v2"; import { HttpSummary, SummaryV2 } from "../v2/SummaryV2"; -import { getVendorInfo } from "../v2/vendorify-traces"; +import type { getVendorInfo } from "../v2/vendorify-traces"; import { useRequestWaterfall } from "./useRequestWaterfall"; export type SpanWithVendorInfo = { diff --git a/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useOrphanLogs.ts b/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useOrphanLogs.ts index c25251f06..14c76eb0f 100644 --- a/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useOrphanLogs.ts +++ b/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useOrphanLogs.ts @@ -1,5 +1,5 @@ -import { MizuOrphanLog, OtelSpan, isMizuOrphanLog } from "@/queries"; -import { OtelEvent } from "@/queries/traces-otel"; +import { type MizuOrphanLog, type OtelSpan, isMizuOrphanLog } from "@/queries"; +import type { OtelEvent } from "@/queries/traces-otel"; import { safeParseJson } from "@/utils"; import { useMemo } from "react"; import { getString } from "../v2/otel-helpers"; diff --git a/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useRequestWaterfall.ts b/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useRequestWaterfall.ts index 5c41d3de5..9f755374b 100644 --- a/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useRequestWaterfall.ts +++ b/studio/src/pages/RequestDetailsPage/RequestDetailsPageV2/useRequestWaterfall.ts @@ -1,7 +1,10 @@ -import { MizuOrphanLog, OtelSpan } from "@/queries"; +import type { MizuOrphanLog, OtelSpan } from "@/queries"; import { useMemo } from "react"; import { getVendorInfo } from "../v2/vendorify-traces"; -import { SpanWithVendorInfo, Waterfall } from "./RequestDetailsPageV2Content"; +import type { + SpanWithVendorInfo, + Waterfall, +} from "./RequestDetailsPageV2Content"; export function useRequestWaterfall( spans: Array, diff --git a/studio/src/pages/RequestDetailsPage/SkeletonLoader.tsx b/studio/src/pages/RequestDetailsPage/SkeletonLoader.tsx index 6a5c91acb..2bbe73b61 100644 --- a/studio/src/pages/RequestDetailsPage/SkeletonLoader.tsx +++ b/studio/src/pages/RequestDetailsPage/SkeletonLoader.tsx @@ -21,14 +21,14 @@ export function SkeletonLoader() { "sm:gap-6 sm:py-8", )} > -
+
-
-
+
+
-
+
-
+
-
+
-
+
diff --git a/studio/src/pages/RequestDetailsPage/hooks/useNavigateToList.ts b/studio/src/pages/RequestDetailsPage/hooks/useNavigateToList.ts index 5464055d5..6857a9297 100644 --- a/studio/src/pages/RequestDetailsPage/hooks/useNavigateToList.ts +++ b/studio/src/pages/RequestDetailsPage/hooks/useNavigateToList.ts @@ -7,17 +7,6 @@ export function useEscapeToList() { const [isInputFocused, setIsInputFocused] = useState(false); - const handleFocus = (event: FocusEvent) => { - if (event.target instanceof HTMLInputElement) { - setIsInputFocused(true); - } - }; - const handleBlur = (event: FocusEvent) => { - if (event.target instanceof HTMLInputElement) { - setIsInputFocused(false); - } - }; - useHotkeys(["Escape"], () => { // catch all the cases where the user is in the input field // and we don't want to exit the page @@ -33,6 +22,17 @@ export function useEscapeToList() { }); useEffect(() => { + const handleFocus = (event: FocusEvent) => { + if (event.target instanceof HTMLInputElement) { + setIsInputFocused(true); + } + }; + const handleBlur = (event: FocusEvent) => { + if (event.target instanceof HTMLInputElement) { + setIsInputFocused(false); + } + }; + // We can use AbortController to remove both event listeners a bit more cleanly // https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/#use-abortcontroller-to-unbind-groups-of-events document.addEventListener("focus", handleFocus, true); diff --git a/studio/src/pages/RequestDetailsPage/hooks/usePagination.ts b/studio/src/pages/RequestDetailsPage/hooks/usePagination.ts index ec3915860..127b45031 100644 --- a/studio/src/pages/RequestDetailsPage/hooks/usePagination.ts +++ b/studio/src/pages/RequestDetailsPage/hooks/usePagination.ts @@ -30,10 +30,14 @@ export function usePagination({ }); const handlePrevTrace = useHandler(() => { - if (currentIndex === undefined) return; + if (currentIndex === undefined) { + return; + } + if (currentIndex === 0) { return; } + const route = getTraceRoute(currentIndex - 1); navigate(route); }); diff --git a/studio/src/pages/RequestDetailsPage/shared.tsx b/studio/src/pages/RequestDetailsPage/shared.tsx index 23becf189..a182dbdee 100644 --- a/studio/src/pages/RequestDetailsPage/shared.tsx +++ b/studio/src/pages/RequestDetailsPage/shared.tsx @@ -1,6 +1,6 @@ import { Card } from "@/components/ui/card"; import { cn } from "@/utils"; -import { ComponentProps } from "react"; +import type { ComponentProps } from "react"; import { getHttpMethodTextColor } from "../RequestorPage/method"; export const FpxCard = ({ diff --git a/studio/src/pages/RequestDetailsPage/v2/FetchSpan.tsx b/studio/src/pages/RequestDetailsPage/v2/FetchSpan.tsx index c9ec1ade7..412b1e9e5 100644 --- a/studio/src/pages/RequestDetailsPage/v2/FetchSpan.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/FetchSpan.tsx @@ -1,7 +1,7 @@ import { Status } from "@/components/ui/status"; import { CodeMirrorSqlEditor } from "@/pages/RequestorPage/Editors/CodeMirrorEditor"; import { getHttpMethodTextColor } from "@/pages/RequestorPage/method"; -import { OtelSpan } from "@/queries"; +import type { OtelSpan } from "@/queries"; import { SENSITIVE_HEADERS, cn, noop } from "@/utils"; import { ClockIcon } from "@radix-ui/react-icons"; import { useMemo } from "react"; @@ -20,8 +20,8 @@ import { } from "./otel-helpers"; import { Divider, SubSection, SubSectionHeading } from "./shared"; import { - NeonVendorInfo, - VendorInfo, + type NeonVendorInfo, + type VendorInfo, isAnthropicVendorInfo, isNeonVendorInfo, isOpenAIVendorInfo, diff --git a/studio/src/pages/RequestDetailsPage/v2/IncomingRequest.tsx b/studio/src/pages/RequestDetailsPage/v2/IncomingRequest.tsx index f570882b2..c6fb9e352 100644 --- a/studio/src/pages/RequestDetailsPage/v2/IncomingRequest.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/IncomingRequest.tsx @@ -1,6 +1,6 @@ import { Status } from "@/components/ui/status"; import { getHttpMethodTextColor } from "@/pages/RequestorPage/method"; -import { OtelSpan } from "@/queries"; +import type { OtelSpan } from "@/queries"; import { SENSITIVE_HEADERS, cn } from "@/utils"; import { ClockIcon } from "@radix-ui/react-icons"; import { useMemo } from "react"; diff --git a/studio/src/pages/RequestDetailsPage/v2/KeyValueTableV2.tsx b/studio/src/pages/RequestDetailsPage/v2/KeyValueTableV2.tsx index e18702cd6..a9bbeee82 100644 --- a/studio/src/pages/RequestDetailsPage/v2/KeyValueTableV2.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/KeyValueTableV2.tsx @@ -18,7 +18,7 @@ import { EyeClosedIcon, EyeOpenIcon, } from "@radix-ui/react-icons"; -import { ReactNode, useState } from "react"; +import { type ReactNode, useState } from "react"; import { SubSectionHeading } from "./shared"; export const KeyValueRow = ({ diff --git a/studio/src/pages/RequestDetailsPage/v2/OrphanLog.tsx b/studio/src/pages/RequestDetailsPage/v2/OrphanLog.tsx index ba3457fe6..0500a302d 100644 --- a/studio/src/pages/RequestDetailsPage/v2/OrphanLog.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/OrphanLog.tsx @@ -1,4 +1,4 @@ -import { MizuOrphanLog } from "@/queries"; +import type { MizuOrphanLog } from "@/queries"; import { cn, objectHasName, diff --git a/studio/src/pages/RequestDetailsPage/v2/SummaryV2.tsx b/studio/src/pages/RequestDetailsPage/v2/SummaryV2.tsx index cbcd76898..13aec67c6 100644 --- a/studio/src/pages/RequestDetailsPage/v2/SummaryV2.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/SummaryV2.tsx @@ -1,14 +1,14 @@ import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { BadgeProps } from "@/components/ui/badge/Badge"; +import type { BadgeProps } from "@/components/ui/badge/Badge"; import { Status } from "@/components/ui/status"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { OtelSpan } from "@/queries/traces-otel"; +import type { OtelSpan } from "@/queries/traces-otel"; import { SEMATTRS_EXCEPTION_MESSAGE, SEMATTRS_EXCEPTION_TYPE, @@ -50,16 +50,13 @@ export function SummaryV2({ requestSpan }: { requestSpan: OtelSpan }) { {hasErrors ? "ERRORS" : "RESPONSE"} {hasErrors ? ( - errors.map((error, idx) => ( + errors.map((error, index) => ( - + {error?.name}: {error?.message} diff --git a/studio/src/pages/RequestDetailsPage/v2/TraceDetailsTimeline.tsx b/studio/src/pages/RequestDetailsPage/v2/TraceDetailsTimeline.tsx index aa049482c..28b571d6e 100644 --- a/studio/src/pages/RequestDetailsPage/v2/TraceDetailsTimeline.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/TraceDetailsTimeline.tsx @@ -6,21 +6,16 @@ import OpenAiLogo from "@/assets/OpenAILogo.svg"; import { Badge } from "@/components/ui/badge"; import { SpanKind } from "@/constants"; -import { MizuOrphanLog, OtelSpan, isMizuOrphanLog } from "@/queries"; +import { type MizuOrphanLog, type OtelSpan, isMizuOrphanLog } from "@/queries"; import { cn, safeParseJson } from "@/utils"; import { CommitIcon, PaperPlaneIcon, TimerIcon } from "@radix-ui/react-icons"; import { formatDistanceStrict } from "date-fns"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { Waterfall } from "../RequestDetailsPageV2/RequestDetailsPageV2Content"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { Waterfall } from "../RequestDetailsPageV2/RequestDetailsPageV2Content"; import { isFetchSpan } from "./otel-helpers"; import { - VendorInfo, + type VendorInfo, isAnthropicVendorInfo, isNeonVendorInfo, isOpenAIVendorInfo, @@ -266,7 +261,7 @@ const useTimelineIcon = ( }, [spanOrLog, vendorInfo, colorOverride]); }; -const getTypeIcon = (type: string, colorOverride: string = "") => { +const getTypeIcon = (type: string, colorOverride = "") => { switch (type) { case "request": case "SERVER": @@ -390,7 +385,7 @@ const WaterfallRowSpan: React.FC<{ style={{ width: lineWidth, marginLeft: lineOffset }} title={`${span.start_time} - ${span.end_time}`} > -
+
@@ -438,7 +433,7 @@ const WaterfallRowLog: React.FC<{ style={{ marginLeft: lineOffset }} title={log.timestamp} > -
+
diff --git a/studio/src/pages/RequestDetailsPage/v2/TraceDetailsV2.tsx b/studio/src/pages/RequestDetailsPage/v2/TraceDetailsV2.tsx index 9686b07b1..7e0b44b2c 100644 --- a/studio/src/pages/RequestDetailsPage/v2/TraceDetailsV2.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/TraceDetailsV2.tsx @@ -1,8 +1,8 @@ import { Badge } from "@/components/ui/badge"; import { SpanStatus } from "@/constants"; -import { OtelSpan, isMizuOrphanLog } from "@/queries"; +import { type OtelSpan, isMizuOrphanLog } from "@/queries"; import { useMemo } from "react"; -import { Waterfall } from "../RequestDetailsPageV2/RequestDetailsPageV2Content"; +import type { Waterfall } from "../RequestDetailsPageV2/RequestDetailsPageV2Content"; import { StackTrace } from "../StackTrace"; import { SectionHeading } from "../shared"; import { FetchSpan } from "./FetchSpan"; @@ -16,7 +16,7 @@ import { isIncomingRequestSpan, } from "./otel-helpers"; import { SubSection, SubSectionHeading } from "./shared"; -import { VendorInfo } from "./vendorify-traces"; +import type { VendorInfo } from "./vendorify-traces"; export function TraceDetailsV2({ waterfall, @@ -105,7 +105,7 @@ function GenericSpan({ span }: { span: OtelSpan }) { if (event.name === "log") { let args: Array = []; try { - args = JSON.parse(getString(event.attributes["args"])); + args = JSON.parse(getString(event.attributes.args)); } catch { // swallow error } @@ -117,8 +117,8 @@ function GenericSpan({ span }: { span: OtelSpan }) { args, id: new Date(event.timestamp).getTime(), timestamp: event.timestamp, - message: getString(event.attributes["message"]), - level: getString(event.attributes["level"]), + message: getString(event.attributes.message), + level: getString(event.attributes.level), traceId: span.trace_id, createdAt: event.timestamp, updatedAt: event.timestamp, diff --git a/studio/src/pages/RequestDetailsPage/v2/otel-helpers.ts b/studio/src/pages/RequestDetailsPage/v2/otel-helpers.ts index bdd750f6a..01aea98c2 100644 --- a/studio/src/pages/RequestDetailsPage/v2/otel-helpers.ts +++ b/studio/src/pages/RequestDetailsPage/v2/otel-helpers.ts @@ -9,8 +9,12 @@ import { FPX_RESPONSE_BODY, SpanKind, } from "@/constants"; -import { OtelSpan } from "@/queries"; -import { OtelAttributes, OtelEvent, OtelTrace } from "@/queries/traces-otel"; +import type { OtelSpan } from "@/queries"; +import type { + OtelAttributes, + OtelEvent, + OtelTrace, +} from "@/queries/traces-otel"; export const isErrorLogEvent = (event: OtelEvent) => { return event.name === "log" && getString(event.attributes.level) === "error"; diff --git a/studio/src/pages/RequestDetailsPage/v2/shared.tsx b/studio/src/pages/RequestDetailsPage/v2/shared.tsx index 1ad353a60..1e7302966 100644 --- a/studio/src/pages/RequestDetailsPage/v2/shared.tsx +++ b/studio/src/pages/RequestDetailsPage/v2/shared.tsx @@ -13,6 +13,11 @@ export const SubSectionHeading = ({
{ + if (e.key === "Enter") { + onClick?.(); + } + }} > {children}
diff --git a/studio/src/pages/RequestDetailsPage/v2/vendorify-traces.ts b/studio/src/pages/RequestDetailsPage/v2/vendorify-traces.ts index e80fd9845..922e060c4 100644 --- a/studio/src/pages/RequestDetailsPage/v2/vendorify-traces.ts +++ b/studio/src/pages/RequestDetailsPage/v2/vendorify-traces.ts @@ -1,4 +1,4 @@ -import { OtelSpan } from "@/queries"; +import type { OtelSpan } from "@/queries"; import { z } from "zod"; import { getRequestBody, getRequestUrl } from "./otel-helpers"; diff --git a/studio/src/pages/RequestorPage/EventsTable.tsx b/studio/src/pages/RequestorPage/EventsTable.tsx index 31278dbd2..471cabde1 100644 --- a/studio/src/pages/RequestorPage/EventsTable.tsx +++ b/studio/src/pages/RequestorPage/EventsTable.tsx @@ -1,5 +1,5 @@ import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; -import { OtelEvent } from "@/queries/traces-otel"; +import type { OtelEvent } from "@/queries/traces-otel"; import { truncateWithEllipsis } from "@/utils"; import { useMemo } from "react"; import { getString } from "../RequestDetailsPage/v2/otel-helpers"; diff --git a/studio/src/pages/RequestorPage/FormDataForm/FormDataForm.tsx b/studio/src/pages/RequestorPage/FormDataForm/FormDataForm.tsx index e60027cb8..2815a153c 100644 --- a/studio/src/pages/RequestorPage/FormDataForm/FormDataForm.tsx +++ b/studio/src/pages/RequestorPage/FormDataForm/FormDataForm.tsx @@ -10,7 +10,10 @@ import { createChangeValue, isDraftParameter, } from "./data"; -import { ChangeFormDataParametersHandler, FormDataParameter } from "./types"; +import type { + ChangeFormDataParametersHandler, + FormDataParameter, +} from "./types"; type Props = { keyValueParameters: FormDataParameter[]; diff --git a/studio/src/pages/RequestorPage/FormDataForm/data.ts b/studio/src/pages/RequestorPage/FormDataForm/data.ts index 1d8647e7a..ce15889f3 100644 --- a/studio/src/pages/RequestorPage/FormDataForm/data.ts +++ b/studio/src/pages/RequestorPage/FormDataForm/data.ts @@ -1,4 +1,4 @@ -import { +import type { ChangeFormDataParametersHandler, DraftFormDataParameter, FormDataParameter, diff --git a/studio/src/pages/RequestorPage/FpxDetails.tsx b/studio/src/pages/RequestorPage/FpxDetails.tsx index 69e5a8940..9d7204c96 100644 --- a/studio/src/pages/RequestorPage/FpxDetails.tsx +++ b/studio/src/pages/RequestorPage/FpxDetails.tsx @@ -11,7 +11,7 @@ import { getString, isErrorLogEvent, } from "../RequestDetailsPage/v2/otel-helpers"; -import { Requestornator } from "./queries"; +import type { Requestornator } from "./queries"; type FpxDetailsProps = { response?: Requestornator; @@ -88,9 +88,13 @@ function TraceDetails({ response, className }: TraceDetailsProps) { const isException = event.name?.toLowerCase() === "exception"; const isError = isErrorLogEvent(event); const name = isException ? ( - Exception + + Exception + ) : isError ? ( - Error + + Error + ) : ( event.name ); @@ -99,7 +103,9 @@ function TraceDetails({ response, className }: TraceDetailsProps) { const message = stringMessage ? ( parseMessage(stringMessage) ) : ( - No message + + No message + ); return [name, message] as [ string | React.ReactNode, diff --git a/studio/src/pages/RequestorPage/KeyValueForm/KeyValueForm.tsx b/studio/src/pages/RequestorPage/KeyValueForm/KeyValueForm.tsx index 8e5fa36e3..5278c884d 100644 --- a/studio/src/pages/RequestorPage/KeyValueForm/KeyValueForm.tsx +++ b/studio/src/pages/RequestorPage/KeyValueForm/KeyValueForm.tsx @@ -9,7 +9,10 @@ import { createChangeValue, isDraftParameter, } from "./data"; -import { ChangeKeyValueParametersHandler, KeyValueParameter } from "./types"; +import type { + ChangeKeyValueParametersHandler, + KeyValueParameter, +} from "./types"; type Props = { keyValueParameters: KeyValueParameter[]; diff --git a/studio/src/pages/RequestorPage/KeyValueForm/data.ts b/studio/src/pages/RequestorPage/KeyValueForm/data.ts index 7badc385c..36d201436 100644 --- a/studio/src/pages/RequestorPage/KeyValueForm/data.ts +++ b/studio/src/pages/RequestorPage/KeyValueForm/data.ts @@ -1,4 +1,4 @@ -import { +import type { ChangeKeyValueParametersHandler, DraftKeyValueParameter, KeyValueParameter, diff --git a/studio/src/pages/RequestorPage/RequestMethodCombobox.tsx b/studio/src/pages/RequestorPage/RequestMethodCombobox.tsx index 757b5254f..ccd69af75 100644 --- a/studio/src/pages/RequestorPage/RequestMethodCombobox.tsx +++ b/studio/src/pages/RequestorPage/RequestMethodCombobox.tsx @@ -15,7 +15,7 @@ import { CheckIcon } from "@radix-ui/react-icons"; import * as React from "react"; import { forwardRef } from "react"; import { getHttpMethodTextColor } from "./method"; -import { type RequestMethodInputValue } from "./types"; +import type { RequestMethodInputValue } from "./types"; import { WEBSOCKETS_ENABLED } from "./webSocketFeatureFlag"; type InputOption = { diff --git a/studio/src/pages/RequestorPage/RequestPanel/AiDropDownMenu.tsx b/studio/src/pages/RequestorPage/RequestPanel/AiDropDownMenu.tsx index c8fd3dbdf..150f9b3f2 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/AiDropDownMenu.tsx +++ b/studio/src/pages/RequestorPage/RequestPanel/AiDropDownMenu.tsx @@ -16,9 +16,10 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn, isMac } from "@/utils"; +import { useHandler } from "@fiberplane/hooks"; import { CaretDownIcon } from "@radix-ui/react-icons"; import { useCallback, useEffect, useState } from "react"; -import { AiTestingPersona, FRIENDLY, HOSTILE } from "../ai"; +import { type AiTestingPersona, FRIENDLY, HOSTILE } from "../ai"; type AiDropDownMenuProps = { isLoadingParameters: boolean; @@ -42,22 +43,23 @@ export function AiDropDownMenu({ [onPersonaChange], ); - const handleGenerateRequest = useCallback(() => { + const handleGenerateRequest = useHandler(() => { fillInRequest(); setOpen(false); - }, [fillInRequest, setOpen]); + }); // When the user shift+clicks of meta+clicks on the trigger, // automatically open the menu // I'm doing this because the caret is kinda hard to press... const { isMetaOrShiftPressed } = useIsMetaOrShiftPressed(); - const handleMagicWandButtonClick = useCallback(() => { + const handleMagicWandButtonClick = useHandler(() => { if (!open && isMetaOrShiftPressed) { setOpen(true); return; } + fillInRequest(); - }, [isMetaOrShiftPressed, setOpen, open, fillInRequest]); + }); return ( @@ -75,7 +77,7 @@ export function AiDropDownMenu({ /> - diff --git a/studio/src/pages/RequestorPage/RequestPanel/AiGeneratedInputsBanner.tsx b/studio/src/pages/RequestorPage/RequestPanel/AiGeneratedInputsBanner.tsx index 221e160a9..11360a58a 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/AiGeneratedInputsBanner.tsx +++ b/studio/src/pages/RequestorPage/RequestPanel/AiGeneratedInputsBanner.tsx @@ -1,6 +1,6 @@ import { Button } from "@/components/ui/button"; import { Cross2Icon, InfoCircledIcon } from "@radix-ui/react-icons"; -import { Dispatch, SetStateAction } from "react"; +import type { Dispatch, SetStateAction } from "react"; type AIGeneratedInputsBannerProps = { showAiGeneratedInputsBanner: boolean; diff --git a/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/BottomToolbar.tsx b/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/BottomToolbar.tsx index e1767c20a..df0fdf520 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/BottomToolbar.tsx +++ b/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/BottomToolbar.tsx @@ -1,7 +1,7 @@ -import { CopyAsCurl, CopyAsCurlProps } from "./CopyAsCurl"; +import { CopyAsCurl, type CopyAsCurlProps } from "./CopyAsCurl"; import { RequestBodyTypeDropdown, - RequestBodyTypeDropdownProps, + type RequestBodyTypeDropdownProps, } from "./RequestBodyCombobox"; type BottomToolbarProps = CopyAsCurlProps & diff --git a/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/CopyAsCurl.tsx b/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/CopyAsCurl.tsx index bb68b605b..8260f001d 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/CopyAsCurl.tsx +++ b/studio/src/pages/RequestorPage/RequestPanel/BottomToolbar/CopyAsCurl.tsx @@ -7,7 +7,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { RequestorState } from "../../reducer"; +import type { RequestorState } from "../../reducer"; import { getBodyValue } from "./utils"; export type CopyAsCurlProps = Pick< diff --git a/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/PathParamForm.tsx b/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/PathParamForm.tsx index 41ca590c2..32f63fd46 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/PathParamForm.tsx +++ b/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/PathParamForm.tsx @@ -1,6 +1,6 @@ import { KeyValueRow } from "../../KeyValueForm"; import { createChangeEnabled } from "../../KeyValueForm/data"; -import { +import type { ChangeKeyValueParametersHandler, KeyValueParameter, } from "../../KeyValueForm/types"; diff --git a/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/data.ts b/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/data.ts index dd1700dad..0df92ffad 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/data.ts +++ b/studio/src/pages/RequestorPage/RequestPanel/PathParamForm/data.ts @@ -1,4 +1,4 @@ -import { +import type { ChangeKeyValueParametersHandler, KeyValueParameter, } from "../../KeyValueForm/types"; diff --git a/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx b/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx index c37fb6654..c41a81827 100644 --- a/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx +++ b/studio/src/pages/RequestorPage/RequestPanel/RequestPanel.tsx @@ -4,22 +4,22 @@ import { useToast } from "@/components/ui/use-toast"; import { useIsSmScreen } from "@/hooks"; import { cn } from "@/utils"; import { EraserIcon, InfoCircledIcon } from "@radix-ui/react-icons"; -import { Dispatch, SetStateAction } from "react"; +import type { Dispatch, SetStateAction } from "react"; import { Resizable } from "react-resizable"; import { CodeMirrorJsonEditor } from "../Editors"; import { FormDataForm } from "../FormDataForm"; -import { KeyValueForm, KeyValueParameter } from "../KeyValueForm"; +import { KeyValueForm, type KeyValueParameter } from "../KeyValueForm"; import { ResizableHandle } from "../Resizable"; import { CustomTabTrigger, CustomTabsContent, CustomTabsList } from "../Tabs"; -import { AiTestingPersona } from "../ai"; +import type { AiTestingPersona } from "../ai"; import { useResizableWidth, useStyleWidth } from "../hooks"; import type { RequestBodyType, RequestorBody, RequestsPanelTab, } from "../reducer"; -import { RequestMethod } from "../types"; -import { WebSocketState } from "../useMakeWebsocketRequest"; +import type { RequestMethod } from "../types"; +import type { WebSocketState } from "../useMakeWebsocketRequest"; import { AiDropDownMenu } from "./AiDropDownMenu"; import { AIGeneratedInputsBanner } from "./AiGeneratedInputsBanner"; import { BottomToolbar } from "./BottomToolbar"; diff --git a/studio/src/pages/RequestorPage/RequestorHistory.tsx b/studio/src/pages/RequestorPage/RequestorHistory.tsx index 002f10958..948514d4b 100644 --- a/studio/src/pages/RequestorPage/RequestorHistory.tsx +++ b/studio/src/pages/RequestorPage/RequestorHistory.tsx @@ -1,6 +1,6 @@ import { cn, parsePathFromRequestUrl, truncatePathWithEllipsis } from "@/utils"; import { getHttpMethodTextColor } from "./method"; -import { Requestornator } from "./queries"; +import type { Requestornator } from "./queries"; type RequestorHistoryProps = { history: Array; @@ -70,6 +70,11 @@ export function HistoryEntry({
{ + if (e.key === "Enter") { + loadHistoricalRequest?.(traceId); + } + }} onClick={() => { loadHistoricalRequest?.(traceId); }} diff --git a/studio/src/pages/RequestorPage/RequestorInput.tsx b/studio/src/pages/RequestorPage/RequestorInput.tsx index b62e61025..2d710ac0d 100644 --- a/studio/src/pages/RequestorPage/RequestorInput.tsx +++ b/studio/src/pages/RequestorPage/RequestorInput.tsx @@ -18,12 +18,12 @@ import { useHotkeys } from "react-hotkeys-hook"; import { RequestMethodCombobox } from "./RequestMethodCombobox"; import { useAddRoutes } from "./queries"; import { - RequestMethod, - RequestMethodInputValue, - RequestType, + type RequestMethod, + type RequestMethodInputValue, + type RequestType, isWsRequest, } from "./types"; -import { WebSocketState } from "./useMakeWebsocketRequest"; +import type { WebSocketState } from "./useMakeWebsocketRequest"; type RequestInputProps = { method: RequestMethod; diff --git a/studio/src/pages/RequestorPage/RequestorPage.tsx b/studio/src/pages/RequestorPage/RequestorPage.tsx index 3952f520a..e78dd3fc3 100644 --- a/studio/src/pages/RequestorPage/RequestorPage.tsx +++ b/studio/src/pages/RequestorPage/RequestorPage.tsx @@ -11,7 +11,7 @@ import { ResponsePanel } from "./ResponsePanel"; import { RoutesCombobox } from "./RoutesCombobox"; import { RoutesPanel } from "./RoutesPanel"; import { AiTestGenerationPanel, useAi } from "./ai"; -import { Requestornator, useMakeProxiedRequest } from "./queries"; +import { type Requestornator, useMakeProxiedRequest } from "./queries"; import { useRequestor } from "./reducer"; import { useRoutes } from "./routes"; import { BACKGROUND_LAYER } from "./styles"; diff --git a/studio/src/pages/RequestorPage/RequestorSessionHistoryContext/RequestorSessionHistoryContext.tsx b/studio/src/pages/RequestorPage/RequestorSessionHistoryContext/RequestorSessionHistoryContext.tsx index d0418cf6a..705790701 100644 --- a/studio/src/pages/RequestorPage/RequestorSessionHistoryContext/RequestorSessionHistoryContext.tsx +++ b/studio/src/pages/RequestorPage/RequestorSessionHistoryContext/RequestorSessionHistoryContext.tsx @@ -10,7 +10,8 @@ * */ -import React, { createContext, useState, ReactNode } from "react"; +import type React from "react"; +import { type ReactNode, createContext, useState } from "react"; type RequestorTraceId = string; diff --git a/studio/src/pages/RequestorPage/ResponsePanel.tsx b/studio/src/pages/RequestorPage/ResponsePanel.tsx deleted file mode 100644 index e471e0ef2..000000000 --- a/studio/src/pages/RequestorPage/ResponsePanel.tsx +++ /dev/null @@ -1,759 +0,0 @@ -import "react-resizable/css/styles.css"; // Import the styles for the resizable component - -import RobotIcon from "@/assets/Robot.svg"; -import { Button } from "@/components/ui/button"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; -import { Tabs } from "@/components/ui/tabs"; -import { - SENSITIVE_HEADERS, - cn, - isJson, - noop, - parsePathFromRequestUrl, - truncateWithEllipsis, -} from "@/utils"; -import { - ArrowDownIcon, - ArrowTopRightIcon, - ArrowUpIcon, - CaretDownIcon, - CaretRightIcon, - LinkBreak2Icon, - QuestionMarkIcon, -} from "@radix-ui/react-icons"; -import { useMemo, useState } from "react"; -import { Link } from "react-router-dom"; -import { Timestamp } from "../RequestDetailsPage/Timestamp"; -import { CollapsibleKeyValueTableV2 } from "../RequestDetailsPage/v2/KeyValueTableV2"; -import { SubSectionHeading } from "../RequestDetailsPage/v2/shared"; -import { CodeMirrorJsonEditor } from "./Editors"; -import { FpxDetails } from "./FpxDetails"; -import { Method, StatusCode } from "./RequestorHistory"; -import { CustomTabTrigger, CustomTabsContent, CustomTabsList } from "./Tabs"; -import { AiTestGenerationDrawer } from "./ai"; -import { Requestornator } from "./queries"; -import type { ResponsePanelTab } from "./reducer"; -import { - RequestorActiveResponse, - isRequestorActiveResponse, -} from "./reducer/state"; -import { type RequestType, isWsRequest } from "./types"; -import { WebSocketState } from "./useMakeWebsocketRequest"; - -type Props = { - activeResponse: RequestorActiveResponse | null; - activeResponsePanelTab: ResponsePanelTab; - setActiveResponsePanelTab: (tab: string) => void; - shouldShowResponseTab: (tab: ResponsePanelTab) => boolean; - tracedResponse?: Requestornator; - isLoading: boolean; - requestType: RequestType; - websocketState: WebSocketState; - openAiTestGenerationPanel: () => void; - isAiTestGenerationPanelOpen: boolean; -}; - -export function ResponsePanel({ - activeResponse, - activeResponsePanelTab, - setActiveResponsePanelTab, - shouldShowResponseTab, - tracedResponse, - isLoading, - requestType, - websocketState, - openAiTestGenerationPanel, - isAiTestGenerationPanelOpen, -}: Props) { - // NOTE - If we have a "raw" response, we want to render that, so we can (e.g.,) show binary data - const responseToRender = activeResponse ?? tracedResponse; - - const isFailure = isRequestorActiveResponse(responseToRender) - ? responseToRender.isFailure - : responseToRender?.app_responses?.isFailure; - - // FIXME - This should actually look if the trace exists in the database - // Since a trace ID will still be created even if we request a non-existent route OR against a service that doesn't exist - const hasTraceId = isRequestorActiveResponse(responseToRender) - ? !!responseToRender?.traceId - : !!responseToRender?.app_responses?.traceId; - - const showBottomToolbar = !!responseToRender; - const disableGoToTraceButton = !hasTraceId; - - const responseHeaders = isRequestorActiveResponse(responseToRender) - ? responseToRender.responseHeaders - : responseToRender?.app_responses?.responseHeaders; - - const shouldShowMessages = shouldShowResponseTab("messages"); - - return ( -
- - - Response - {shouldShowMessages && ( - Messages - )} - Debug -
- -
-
- - } - FailState={} - EmptyState={} - > - - - - - } - FailState={ - isWsRequest(requestType) ? ( - - ) : ( - - ) - } - EmptyState={ - isWsRequest(requestType) ? ( - - ) : ( - - ) - } - > -
- - - } - response={responseToRender} - // HACK - To support absolutely positioned bottom toolbar - className={cn(showBottomToolbar && "pb-16")} - /> - {showBottomToolbar && ( - - )} -
-
-
- - } - EmptyState={} - > -
- - {showBottomToolbar && ( - - )} -
-
-
-
-
- ); -} - -function CollapsibleBodyContainer({ - className, - defaultCollapsed = false, - title = "Body", - children, -}: { - emptyMessage?: string; - className?: string; - defaultCollapsed?: boolean; - title?: string; - children: React.ReactNode; -}) { - const [isOpen, setIsOpen] = useState(!defaultCollapsed); - const toggleIsOpen = () => setIsOpen((o) => !o); - - return ( -
- - - - {isOpen ? ( - - ) : ( - - )} - {title} - - - {children} - -
- ); -} - -// TODO - When there's no matching trace, don't allow the user to go to trace details -const BottomToolbar = ({ - response, - disableGoToTraceButton, -}: { response?: Requestornator | null; disableGoToTraceButton: boolean }) => { - const traceId = response?.app_responses?.traceId; - - return ( -
-
- -
- - - -
- ); -}; - -function WebsocketMessages({ - websocketState, -}: { websocketState: WebSocketState }) { - return ( -
-
Messages
-
- - - {websocketState.messages.map((message, index) => ( - - - {message.type === "received" ? ( - - ) : ( - - )} - - - {truncateWithEllipsis(message?.data, 100)} - - - {message?.timestamp ? ( -
- -
- ) : ( - "—" - )} -
-
- ))} -
-
-
-
- ); -} - -/** - * Helper component for handling loading/failure/empty states in tab content - */ -function TabContentInner({ - isLoading, - isEmpty, - isFailure, - LoadingState = , - EmptyState, - FailState, - children, -}: { - children: React.ReactNode; - EmptyState: JSX.Element; - FailState: JSX.Element; - LoadingState?: JSX.Element; - isFailure: boolean; - isLoading: boolean; - isEmpty: boolean; -}) { - return isLoading ? ( - <>{LoadingState} - ) : isFailure ? ( - <>{FailState} - ) : !isEmpty ? ( - <>{children} - ) : ( - <>{EmptyState} - ); -} - -function ResponseSummary({ - response, -}: { response?: Requestornator | RequestorActiveResponse }) { - const status = isRequestorActiveResponse(response) - ? response?.responseStatusCode - : response?.app_responses?.responseStatusCode; - const method = isRequestorActiveResponse(response) - ? response?.requestMethod - : response?.app_requests?.requestMethod; - const url = isRequestorActiveResponse(response) - ? response?.requestUrl - : parsePathFromRequestUrl( - response?.app_requests?.requestUrl ?? "", - response?.app_requests?.requestQueryParams ?? undefined, - ); - return ( -
- -
- - - {url} - -
-
- ); -} - -function ResponseBody({ - headersSlot, - response, - className, -}: { - headersSlot?: React.ReactNode; - response?: Requestornator | RequestorActiveResponse; - className?: string; -}) { - const isFailure = isRequestorActiveResponse(response) - ? response?.isFailure - : response?.app_responses?.isFailure; - - // This means we couldn't even contact the service - if (isFailure) { - return ; - } - - if (isRequestorActiveResponse(response)) { - const body = response?.responseBody; - if (body?.type === "error") { - return ; - } - - if (body?.type === "text" || body?.type === "html") { - return ( -
- {headersSlot} - - - -
- ); - } - - if (body?.type === "json") { - const prettyBody = JSON.stringify(safeParseJson(body.value), null, 2); - - return ( -
- {headersSlot} - - - -
- ); - } - - // TODO - if (body?.type === "binary") { - return ( -
- {headersSlot} - - ; - -
- ); - } - - // TODO - Stylize - if (body?.type === "unknown") { - return ( - - ); - } - - return ; - } - - if (!isRequestorActiveResponse(response)) { - const body = response?.app_responses?.responseBody; - - // Special rendering for JSON - if (body && isJson(body)) { - const prettyBody = JSON.stringify(JSON.parse(body), null, 2); - - return ( -
- {headersSlot} - - - -
- ); - } - - // For text responses, just split into lines and render with rudimentary line numbers - // TODO - if response is empty, show that in a ux friendly way, with 204 for example - - return ( -
- {headersSlot} - - - -
- ); - } -} - -function UnknownResponse({ - className, - headersSlot, -}: { - headersSlot: React.ReactNode; - className?: string; -}) { - return ( -
- {headersSlot} - -
- - - Unknown response type, cannot render body - -
-
-
- ); -} - -function ResponseBodyBinary({ - body, -}: { - body: { contentType: string; type: "binary"; value: ArrayBuffer }; -}) { - const isImage = body.contentType.startsWith("image/"); - - if (isImage) { - const blob = new Blob([body.value], { type: body.contentType }); - const imageUrl = URL.createObjectURL(blob); - return ( - Response Image URL.revokeObjectURL(imageUrl)} - /> - ); - } - - // TODO - Stylize - return
Binary response {body.contentType}
; -} - -export function ResponseBodyText({ - body, - maxPreviewLength = null, - maxPreviewLines = null, - defaultExpanded = false, - className, -}: { - body: string; - maxPreviewLength?: number | null; - maxPreviewLines?: number | null; - defaultExpanded?: boolean; - className?: string; -}) { - const [isExpanded, setIsExpanded] = useState(!!defaultExpanded); - const toggleIsExpanded = () => setIsExpanded((e) => !e); - - // For text responses, just split into lines and render with rudimentary line numbers - const { lines, hiddenLinesCount, hiddenCharsCount, shouldShowExpandButton } = - useTextPreview(body, isExpanded, maxPreviewLength, maxPreviewLines); - - // TODO - if response is empty, show that in a ux friendly way, with 204 for example - - return ( -
-
-        {lines}
-      
- {shouldShowExpandButton && ( -
- {!isExpanded && ( -
- {hiddenLinesCount > 0 ? ( - <>{hiddenLinesCount} lines hidden - ) : ( - <>{hiddenCharsCount} characters hidden - )} -
- )} - -
- )} -
- ); -} - -function useTextPreview( - body: string, - isExpanded: boolean, - maxPreviewLength: number | null, - maxPreviewLines: number | null, -) { - return useMemo(() => { - let hiddenCharsCount = 0; - let hiddenLinesCount = 0; - const allLinesCount = body.split("\n")?.length; - - const exceedsMaxPreviewLength = maxPreviewLength - ? body.length > maxPreviewLength - : false; - - const exceedsMaxPreviewLines = maxPreviewLines - ? allLinesCount > maxPreviewLines - : false; - - // If we're not expanded, we want to show a preview of the body depending on the maxPreviewLength - let previewBody = body; - if (maxPreviewLength && exceedsMaxPreviewLength && !isExpanded) { - previewBody = body ? body.slice(0, maxPreviewLength) + "..." : ""; - hiddenCharsCount = body.length - maxPreviewLength; - } - - let previewLines = previewBody?.split("\n"); - if ( - maxPreviewLines && - !isExpanded && - previewLines.length > maxPreviewLines - ) { - previewLines = previewLines.slice(0, maxPreviewLines); - previewBody = `${previewLines.join("\n")}...`; - hiddenLinesCount = allLinesCount - previewLines.length; - } - - const lines = (isExpanded ? body : previewBody) - ?.split("\n") - ?.map((line, index) => ( -
- - {index + 1} - - {line} -
- )); - - return { - lines, - shouldShowExpandButton: exceedsMaxPreviewLength || exceedsMaxPreviewLines, - hiddenLinesCount, - hiddenCharsCount, - }; - }, [body, maxPreviewLines, maxPreviewLength, isExpanded]); -} - -function NoResponse() { - return ( -
-
- Enter a URL and hit send to see a response -
-
- Or load a past request from your history -
-
- ); -} - -function NoWebsocketConnection() { - return ( -
-
- Enter a WebSocket URL and click Connect to start receiving messages -
-
- You can send and view messages in the Messages tabs -
-
- ); -} - -function Loading() { - return ( - <> - - - - ); -} - -function LoadingResponseBody() { - return ( - <> -
- - - -
- - - ); -} - -function FailedRequest({ - response, -}: { response?: Requestornator | RequestorActiveResponse }) { - // TODO - Show a more friendly error message - const failureReason = isRequestorActiveResponse(response) - ? null - : response?.app_responses?.failureReason; - const friendlyMessage = - failureReason === "fetch failed" ? "Service unreachable" : null; - // const failureDetails = response?.app_responses?.failureDetails; - return ( -
-
- -
- {friendlyMessage - ? `Request failed: ${friendlyMessage}` - : "Request failed"} -
-
- Make sure your api is up and has FPX Middleware enabled! -
-
-
- ); -} - -function FailedWebsocket() { - return ( -
-
- -
- Websocket connection failed -
-
- Make sure your api is up and running -
-
-
- ); -} - -const safeParseJson = (jsonString: string) => { - try { - const parsed = JSON.parse(jsonString); - return parsed; - } catch (error) { - console.error("Failed to parse JSON:", error); - return jsonString; - } -}; diff --git a/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody.tsx b/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody.tsx index 6e8668bc8..0a88fcccd 100644 --- a/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody.tsx +++ b/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody.tsx @@ -80,7 +80,7 @@ export function ResponseBody({ > {headersSlot} - ; +
); @@ -164,7 +164,7 @@ function ResponseBodyBinary({ return ( Response Image URL.revokeObjectURL(imageUrl)} /> @@ -254,7 +254,7 @@ function useTextPreview( // If we're not expanded, we want to show a preview of the body depending on the maxPreviewLength let previewBody = body; if (maxPreviewLength && exceedsMaxPreviewLength && !isExpanded) { - previewBody = body ? body.slice(0, maxPreviewLength) + "..." : ""; + previewBody = body ? `${body.slice(0, maxPreviewLength)}...` : ""; hiddenCharsCount = body.length - maxPreviewLength; } diff --git a/studio/src/pages/RequestorPage/RoutesCombobox.tsx b/studio/src/pages/RequestorPage/RoutesCombobox.tsx index 668add966..398bee1e5 100644 --- a/studio/src/pages/RequestorPage/RoutesCombobox.tsx +++ b/studio/src/pages/RequestorPage/RoutesCombobox.tsx @@ -21,7 +21,7 @@ import { useIsLgScreen } from "@/hooks"; import { cn } from "@/utils"; import { useHotkeys } from "react-hotkeys-hook"; import { RouteItem } from "./RoutesPanel"; -import { ProbedRoute } from "./queries"; +import type { ProbedRoute } from "./queries"; import { AddRoutesDialog } from "./routes/AddRouteButton"; type RoutesComboboxProps = { diff --git a/studio/src/pages/RequestorPage/RoutesPanel.tsx b/studio/src/pages/RequestorPage/RoutesPanel.tsx index 81fb482c0..8611610f4 100644 --- a/studio/src/pages/RequestorPage/RoutesPanel.tsx +++ b/studio/src/pages/RequestorPage/RoutesPanel.tsx @@ -12,7 +12,11 @@ import { RequestorHistory } from "./RequestorHistory"; import { ResizableHandle } from "./Resizable"; import { useResizableWidth, useStyleWidth } from "./hooks"; import { getHttpMethodTextColor } from "./method"; -import { ProbedRoute, Requestornator, useDeleteRoute } from "./queries"; +import { + type ProbedRoute, + type Requestornator, + useDeleteRoute, +} from "./queries"; import { AddRouteButton } from "./routes"; import { BACKGROUND_LAYER } from "./styles"; import { isWsRequest } from "./types"; @@ -233,6 +237,11 @@ function HistorySection({
{ + if (e.key === "Enter") { + setShowHistorySection((current) => !current); + } + }} onClick={() => { setShowHistorySection((current) => !current); }} @@ -276,6 +285,11 @@ function RoutesSection(props: RoutesSectionProps) { <>
{ + if (e.key === "Enter") { + setShowRoutesSection((current) => !current); + } + }} onClick={() => { setShowRoutesSection((current) => !current); }} @@ -289,6 +303,11 @@ function RoutesSection(props: RoutesSectionProps) {
handleRouteClick(route)} + onKeyUp={(e) => { + if (e.key === "Enter") { + handleRouteClick(route); + } + }} className={cn( "flex items-center py-1 pl-5 pr-2 rounded cursor-pointer font-mono text-sm", { diff --git a/studio/src/pages/RequestorPage/Tabs.tsx b/studio/src/pages/RequestorPage/Tabs.tsx index 98a61bef2..fe907afaf 100644 --- a/studio/src/pages/RequestorPage/Tabs.tsx +++ b/studio/src/pages/RequestorPage/Tabs.tsx @@ -1,8 +1,8 @@ import { TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/utils"; -import * as TabsPrimitive from "@radix-ui/react-tabs"; +import type * as TabsPrimitive from "@radix-ui/react-tabs"; import * as React from "react"; -import { ComponentProps } from "react"; +import type { ComponentProps } from "react"; const TAB_HEIGHT = "h-12"; diff --git a/studio/src/pages/RequestorPage/ai/AiTestGenerationDrawer.tsx b/studio/src/pages/RequestorPage/ai/AiTestGenerationDrawer.tsx index f00c62056..f6aff5cc8 100644 --- a/studio/src/pages/RequestorPage/ai/AiTestGenerationDrawer.tsx +++ b/studio/src/pages/RequestorPage/ai/AiTestGenerationDrawer.tsx @@ -15,7 +15,7 @@ import { cn, parsePathFromRequestUrl, truncatePathWithEllipsis } from "@/utils"; import { CopyIcon } from "@radix-ui/react-icons"; import { useState } from "react"; import { Method, StatusCode } from "../RequestorHistory"; -import { Requestornator } from "../queries"; +import type { Requestornator } from "../queries"; import { usePrompt } from "./ai-test-generation"; export function AiTestGenerationDrawer({ diff --git a/studio/src/pages/RequestorPage/ai/AiTestGenerationPanel.tsx b/studio/src/pages/RequestorPage/ai/AiTestGenerationPanel.tsx index c2a5349c0..418655548 100644 --- a/studio/src/pages/RequestorPage/ai/AiTestGenerationPanel.tsx +++ b/studio/src/pages/RequestorPage/ai/AiTestGenerationPanel.tsx @@ -10,7 +10,7 @@ import { } from "@radix-ui/react-icons"; import { useMemo, useState } from "react"; import { CustomTabTrigger, CustomTabsContent, CustomTabsList } from "../Tabs"; -import { ProbedRoute, Requestornator } from "../queries"; +import type { ProbedRoute, Requestornator } from "../queries"; import { findMatchedRoute } from "../routes"; import { ContextEntry } from "./AiTestGenerationDrawer"; import { usePrompt } from "./ai-test-generation"; diff --git a/studio/src/pages/RequestorPage/ai/ai-test-generation.ts b/studio/src/pages/RequestorPage/ai/ai-test-generation.ts index 9caf746bf..7ef17fa47 100644 --- a/studio/src/pages/RequestorPage/ai/ai-test-generation.ts +++ b/studio/src/pages/RequestorPage/ai/ai-test-generation.ts @@ -7,10 +7,10 @@ import { isErrorLogEvent, isFetchSpan, } from "@/pages/RequestDetailsPage/v2/otel-helpers"; -import { OtelSpans, useOtelTrace } from "@/queries"; +import { type OtelSpans, useOtelTrace } from "@/queries"; import { formatHeaders, redactSensitiveHeaders } from "@/utils"; import { useMemo } from "react"; -import { Requestornator } from "../queries"; +import type { Requestornator } from "../queries"; import { appRequestToHttpRequest, appResponseToHttpRequest } from "./utils"; function createRequestDescription(request: Requestornator | null): string { diff --git a/studio/src/pages/RequestorPage/ai/ai.ts b/studio/src/pages/RequestorPage/ai/ai.ts index 356949b21..986c05676 100644 --- a/studio/src/pages/RequestorPage/ai/ai.ts +++ b/studio/src/pages/RequestorPage/ai/ai.ts @@ -4,9 +4,12 @@ import { errorHasMessage, isJson } from "@/utils"; import { useCallback, useEffect, useMemo, useState } from "react"; import { z } from "zod"; import { createFormDataParameter } from "../FormDataForm/data"; -import { KeyValueParameter, createKeyValueParameters } from "../KeyValueForm"; -import { ProbedRoute, Requestornator } from "../queries"; -import { RequestorBody } from "../reducer"; +import { + type KeyValueParameter, + createKeyValueParameters, +} from "../KeyValueForm"; +import type { ProbedRoute, Requestornator } from "../queries"; +import type { RequestorBody } from "../reducer"; import { isRequestorBodyType } from "../reducer/request-body"; import { useAiRequestData } from "./generate-request-data"; diff --git a/studio/src/pages/RequestorPage/ai/generate-request-data.ts b/studio/src/pages/RequestorPage/ai/generate-request-data.ts index 19bfeec8a..cda2523ab 100644 --- a/studio/src/pages/RequestorPage/ai/generate-request-data.ts +++ b/studio/src/pages/RequestorPage/ai/generate-request-data.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; -import { ProbedRoute, Requestornator } from "../queries"; -import { RequestBodyType } from "../reducer"; +import type { ProbedRoute, Requestornator } from "../queries"; +import type { RequestBodyType } from "../reducer"; import { simplifyHistoryEntry } from "./utils"; const fetchAiRequestData = ( diff --git a/studio/src/pages/RequestorPage/ai/summarize-traces.ts b/studio/src/pages/RequestorPage/ai/summarize-traces.ts index 4a43167c9..9c9e09398 100644 --- a/studio/src/pages/RequestorPage/ai/summarize-traces.ts +++ b/studio/src/pages/RequestorPage/ai/summarize-traces.ts @@ -12,7 +12,7 @@ import { getStatusCode, getString, } from "@/pages/RequestDetailsPage/v2/otel-helpers"; -import { OtelSpans } from "@/queries"; +import type { OtelSpans } from "@/queries"; import { fetchSourceLocation } from "@/queries"; import { formatHeaders, redactSensitiveHeaders } from "@/utils"; import { useQuery } from "@tanstack/react-query"; diff --git a/studio/src/pages/RequestorPage/ai/utils.ts b/studio/src/pages/RequestorPage/ai/utils.ts index 89184edf8..b9f4836d9 100644 --- a/studio/src/pages/RequestorPage/ai/utils.ts +++ b/studio/src/pages/RequestorPage/ai/utils.ts @@ -1,5 +1,5 @@ import { redactSensitiveHeaders } from "@/utils"; -import { Requestornator } from "../queries"; +import type { Requestornator } from "../queries"; /** * Simplify a history entry into a string that can be used to represent previous requests/responses. @@ -36,7 +36,7 @@ export function appRequestToHttpRequest(entry: Requestornator) { "", `HTTP/1.1 ${entry.app_requests.requestMethod} ${requestUrlWithParams}`, ...Object.entries(requestHeaders).map(([key, value]) => `${key}: ${value}`), - ``, + "", `${requestBody || ""}`, "", ].join("\n"); @@ -54,7 +54,7 @@ export function appResponseToHttpRequest(entry: Requestornator) { ...Object.entries(responseHeaders).map( ([key, value]) => `${key}: ${value}`, ), - ``, + "", `${entry.app_responses?.responseBody || ""}`, "", ].join("\n"); diff --git a/studio/src/pages/RequestorPage/hooks.ts b/studio/src/pages/RequestorPage/hooks.ts index 9cc93e094..8474ae3ef 100644 --- a/studio/src/pages/RequestorPage/hooks.ts +++ b/studio/src/pages/RequestorPage/hooks.ts @@ -1,5 +1,5 @@ -import { SyntheticEvent, useCallback, useMemo, useState } from "react"; -import { ResizeCallbackData } from "react-resizable"; +import { type SyntheticEvent, useCallback, useMemo, useState } from "react"; +import type { ResizeCallbackData } from "react-resizable"; export function useResizableWidth(initialWidth: number, min = 200, max = 600) { const [width, setWidth] = useState(initialWidth); diff --git a/studio/src/pages/RequestorPage/queries.ts b/studio/src/pages/RequestorPage/queries.ts index 22ef583d2..c7a1ca9a1 100644 --- a/studio/src/pages/RequestorPage/queries.ts +++ b/studio/src/pages/RequestorPage/queries.ts @@ -3,7 +3,10 @@ import { validate } from "@scalar/openapi-parser"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { z } from "zod"; import { reduceFormDataParameters } from "./FormDataForm"; -import { KeyValueParameter, reduceKeyValueParameters } from "./KeyValueForm"; +import { + type KeyValueParameter, + reduceKeyValueParameters, +} from "./KeyValueForm"; import type { RequestorActiveResponse, RequestorBody, @@ -216,11 +219,11 @@ export function makeProxiedRequest({ } const queryParamsForUrl = new URLSearchParams(); - queryParams.forEach((param) => { + for (const param of queryParams) { if (param.enabled) { queryParamsForUrl.set(param.key, param.value); } - }); + } // NOTE - we add custom headers to record additional metadata about the request const modHeaders = reduceKeyValueParameters(headers); diff --git a/studio/src/pages/RequestorPage/reducer/persistence.ts b/studio/src/pages/RequestorPage/reducer/persistence.ts index c364b2e1e..02494e17e 100644 --- a/studio/src/pages/RequestorPage/reducer/persistence.ts +++ b/studio/src/pages/RequestorPage/reducer/persistence.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from "react"; import { useBeforeUnload } from "react-router-dom"; import { LOCAL_STORAGE_KEY, - SavedRequestorState, + type SavedRequestorState, SavedRequestorStateSchema, } from "./state"; diff --git a/studio/src/pages/RequestorPage/reducer/reducer.ts b/studio/src/pages/RequestorPage/reducer/reducer.ts index 83bc39a88..3d6999848 100644 --- a/studio/src/pages/RequestorPage/reducer/reducer.ts +++ b/studio/src/pages/RequestorPage/reducer/reducer.ts @@ -1,28 +1,28 @@ import { useCallback, useReducer } from "react"; import { enforceFormDataTerminalDraftParameter } from "../FormDataForm"; -import { KeyValueParameter } from "../KeyValueForm"; +import type { KeyValueParameter } from "../KeyValueForm"; import { enforceTerminalDraftParameter } from "../KeyValueForm/hooks"; -import { ProbedRoute } from "../queries"; +import type { ProbedRoute } from "../queries"; import { findMatchedRoute } from "../routes"; import { - RequestMethod, - RequestMethodInputValue, - RequestType, + type RequestMethod, + type RequestMethodInputValue, + type RequestType, isWsRequest, } from "../types"; import { useSaveUiState } from "./persistence"; import { addContentTypeHeader, setBodyTypeReducer } from "./reducers"; import { - RequestBodyType, - RequestorActiveResponse, - RequestorBody, + type RequestBodyType, + type RequestorActiveResponse, + type RequestorBody, type RequestorState, createInitialState, initialState, } from "./state"; import { - RequestsPanelTab, - ResponsePanelTab, + type RequestsPanelTab, + type ResponsePanelTab, getVisibleRequestPanelTabs, getVisibleResponsePanelTabs, isRequestsPanelTab, @@ -343,9 +343,9 @@ function requestorReducer( const nextPath = action.payload.reduce((accPath, param) => { if (param.enabled) { return accPath.replace(`:${param.key}`, param.value || param.key); - } else { - return accPath; } + + return accPath; }, state.selectedRoute?.path ?? state.path); return { ...state, @@ -762,9 +762,15 @@ function extractPathParams(path: string) { const regex = /\/(:[a-zA-Z0-9_-]+)/g; const result: Array = []; - let match; + // let match = regex.exec(path); let lastIndex = -1; - while ((match = regex.exec(path)) !== null) { + while (true) { + const match = regex.exec(path); + + if (match === null) { + break; + } + // Check if the regex is stuck in an infinite loop if (regex.lastIndex === lastIndex) { break; diff --git a/studio/src/pages/RequestorPage/reducer/reducers/content-type.ts b/studio/src/pages/RequestorPage/reducer/reducers/content-type.ts index 2244c1160..ef691d5e2 100644 --- a/studio/src/pages/RequestorPage/reducer/reducers/content-type.ts +++ b/studio/src/pages/RequestorPage/reducer/reducers/content-type.ts @@ -1,9 +1,9 @@ import { - KeyValueParameter, + type KeyValueParameter, enforceTerminalDraftParameter, } from "../../KeyValueForm"; import { isDraftParameter } from "../../KeyValueForm/data"; -import { RequestorBody, RequestorState } from "../state"; +import type { RequestorBody, RequestorState } from "../state"; /** * This makes sure to synchronize the content type header with the body type. diff --git a/studio/src/pages/RequestorPage/reducer/reducers/set-body-type.ts b/studio/src/pages/RequestorPage/reducer/reducers/set-body-type.ts index 48f7cf170..94e831bfc 100644 --- a/studio/src/pages/RequestorPage/reducer/reducers/set-body-type.ts +++ b/studio/src/pages/RequestorPage/reducer/reducers/set-body-type.ts @@ -1,5 +1,5 @@ import { enforceFormDataTerminalDraftParameter } from "../../FormDataForm"; -import { RequestBodyType, RequestorState } from "../state"; +import type { RequestBodyType, RequestorState } from "../state"; /** * This reducer is responsible for setting the body type of the request. diff --git a/studio/src/pages/RequestorPage/reducer/tabs.ts b/studio/src/pages/RequestorPage/reducer/tabs.ts index 7ff09bd3c..2b264406d 100644 --- a/studio/src/pages/RequestorPage/reducer/tabs.ts +++ b/studio/src/pages/RequestorPage/reducer/tabs.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { RequestMethod, RequestType } from "../types"; +import type { RequestMethod, RequestType } from "../types"; export const RequestsPanelTabSchema = z.enum([ "params", diff --git a/studio/src/pages/RequestorPage/routes/AddRouteButton.tsx b/studio/src/pages/RequestorPage/routes/AddRouteButton.tsx index e0146e6c9..21bb92a20 100644 --- a/studio/src/pages/RequestorPage/routes/AddRouteButton.tsx +++ b/studio/src/pages/RequestorPage/routes/AddRouteButton.tsx @@ -16,13 +16,13 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { PlusIcon } from "@radix-ui/react-icons"; -import { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from "@scalar/openapi-parser"; +import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from "@scalar/openapi-parser"; import { useState } from "react"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { type SubmitHandler, useForm } from "react-hook-form"; import { useHotkeys } from "react-hotkeys-hook"; import { RequestMethodCombobox } from "../RequestMethodCombobox"; -import { Route, useAddRoutes, useOpenApiParse } from "../queries"; -import { RequestMethodInputValue } from "../types"; +import { type Route, useAddRoutes, useOpenApiParse } from "../queries"; +import type { RequestMethodInputValue } from "../types"; export function AddRouteButton() { useHotkeys("c", (e) => { @@ -168,31 +168,35 @@ function OpenApiForm({ setOpen(false); - const submissionRoutes: Route[] = Object.entries(schema.paths!) - .map(([path, pathObj]: [string, PathObject]) => { - // destructure the params so we don't include them - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { parameters, ...pathObjWithoutParams } = pathObj; - return Object.entries(pathObjWithoutParams).map( - ([method, operation]: [string, OperationObject]) => { - const seen = new WeakSet(); - return { - path: path.replace(/{(.*?)}/g, ":$1"), - method: method.toUpperCase(), - handlerType: "route" as const, - routeOrigin: "open_api" as const, - openApiSpec: JSON.stringify(operation, (_key, value) => { - if (typeof value === "object" && value !== null) { - if (seen.has(value)) return "[Circular]"; - seen.add(value); - } - return value; - }), - }; + const submissionRoutes: Route[] = schema.paths + ? Object.entries(schema.paths).flatMap( + ([path, pathObj]: [string, PathObject]) => { + // destructure the params so we don't include them + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { parameters, ...pathObjWithoutParams } = pathObj; + return Object.entries(pathObjWithoutParams).map( + ([method, operation]: [string, OperationObject]) => { + const seen = new WeakSet(); + return { + path: path.replace(/{(.*?)}/g, ":$1"), + method: method.toUpperCase(), + handlerType: "route" as const, + routeOrigin: "open_api" as const, + openApiSpec: JSON.stringify(operation, (_key, value) => { + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return "[Circular]"; + } + seen.add(value); + } + return value; + }), + }; + }, + ); }, - ); - }) - .flat(); + ) + : []; addRoutes(submissionRoutes); } catch (error) { @@ -226,7 +230,7 @@ function OpenApiForm({ placeholder="Paste your OpenAPI spec here" onPaste={onPaste} autoFocus - > + /> {isPending &&

Validating OpenAPI spec...

} {errors.openApiSpec && (

Invalid OpenAPI spec

diff --git a/studio/src/pages/RequestorPage/routes/hooks.ts b/studio/src/pages/RequestorPage/routes/hooks.ts index ba4c4d3e8..946d29383 100644 --- a/studio/src/pages/RequestorPage/routes/hooks.ts +++ b/studio/src/pages/RequestorPage/routes/hooks.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo } from "react"; -import { ProbedRoute, useProbedRoutes } from "../queries"; +import { type ProbedRoute, useProbedRoutes } from "../queries"; import { WEBSOCKETS_ENABLED } from "../webSocketFeatureFlag"; type UseRoutesOptions = { diff --git a/studio/src/pages/RequestorPage/routes/match.test.ts b/studio/src/pages/RequestorPage/routes/match.test.ts index dd5b15929..246380ec7 100644 --- a/studio/src/pages/RequestorPage/routes/match.test.ts +++ b/studio/src/pages/RequestorPage/routes/match.test.ts @@ -1,5 +1,5 @@ -import { ProbedRoute } from "../queries"; -import { RequestMethod, RequestType } from "../types"; +import type { ProbedRoute } from "../queries"; +import type { RequestMethod, RequestType } from "../types"; import { findSmartRouterMatches } from "./match"; const toRoute = ( diff --git a/studio/src/pages/RequestorPage/routes/match.ts b/studio/src/pages/RequestorPage/routes/match.ts index e8d11f131..02f49888b 100644 --- a/studio/src/pages/RequestorPage/routes/match.ts +++ b/studio/src/pages/RequestorPage/routes/match.ts @@ -1,7 +1,7 @@ import { RegExpRouter } from "hono/router/reg-exp-router"; import { SmartRouter } from "hono/router/smart-router"; import { TrieRouter } from "hono/router/trie-router"; -import { ProbedRoute } from "../queries"; +import type { ProbedRoute } from "../queries"; type MatchedRouteResult = { route: ProbedRoute; diff --git a/studio/src/pages/RequestorPage/useMakeWebsocketRequest.ts b/studio/src/pages/RequestorPage/useMakeWebsocketRequest.ts index e16c6b30c..1c08baab2 100644 --- a/studio/src/pages/RequestorPage/useMakeWebsocketRequest.ts +++ b/studio/src/pages/RequestorPage/useMakeWebsocketRequest.ts @@ -1,3 +1,4 @@ +import { useHandler } from "@fiberplane/hooks"; import { useCallback, useReducer, useRef } from "react"; // Define action types @@ -67,44 +68,41 @@ export function useMakeWebsocketRequest() { const [state, dispatch] = useReducer(websocketReducer, initialState); const socket = useRef(null); - const connect = useCallback( - (wsUrl: string) => { - socket.current = new WebSocket(wsUrl); + const connect = useHandler((wsUrl: string) => { + socket.current = new WebSocket(wsUrl); - socket.current.onopen = () => { - console.debug("Connected to websocket server"); - dispatch({ type: WEBSOCKET_CONNECTED }); - }; + socket.current.onopen = () => { + console.debug("Connected to websocket server"); + dispatch({ type: WEBSOCKET_CONNECTED }); + }; - socket.current.onmessage = (ev) => { - console.debug("Received websocket message", ev?.data); - const message = { - data: ev?.data, - timestamp: new Date().toISOString(), - type: "received" as const, - }; - dispatch({ type: WEBSOCKET_MESSAGE_RECEIVED, payload: message }); + socket.current.onmessage = (ev) => { + console.debug("Received websocket message", ev?.data); + const message = { + data: ev?.data, + timestamp: new Date().toISOString(), + type: "received" as const, }; + dispatch({ type: WEBSOCKET_MESSAGE_RECEIVED, payload: message }); + }; - socket.current.onclose = (ev) => { - console.debug("Disconnected from websocket server", ev); - dispatch({ type: WEBSOCKET_DISCONNECTED }); - }; + socket.current.onclose = (ev) => { + console.debug("Disconnected from websocket server", ev); + dispatch({ type: WEBSOCKET_DISCONNECTED }); + }; - socket.current.onerror = (error) => { - console.error("Error with websocket server", error); - dispatch({ type: WEBSOCKET_ERROR }); - }; + socket.current.onerror = (error) => { + console.error("Error with websocket server", error); + dispatch({ type: WEBSOCKET_ERROR }); + }; - dispatch({ type: WEBSOCKET_CONNECTING }); + dispatch({ type: WEBSOCKET_CONNECTING }); - return { - socket, - state, - }; - }, - [state], - ); + return { + socket, + state, + }; + }); // NOTE - Unsure if we should manually dispatch the disconnect event here or if the WebSocket API will handle it // I had a case in the UI where the reducer state wasn't in sync with the actual state of the socket for some reason... @@ -112,26 +110,23 @@ export function useMakeWebsocketRequest() { if (socket.current) { socket.current.close(); } - }, [socket]); - - const sendMessage = useCallback( - (message: string) => { - if (socket.current) { - socket.current.send(message); - const sentMessage = { - data: message, - timestamp: new Date().toISOString(), - type: "sent" as const, - }; - dispatch({ type: WEBSOCKET_MESSAGE_SENT, payload: sentMessage }); - } else { - console.error( - "Tried to send message but WebSocket connection not established", - ); - } - }, - [socket], - ); + }, []); + + const sendMessage = useCallback((message: string) => { + if (socket.current) { + socket.current.send(message); + const sentMessage = { + data: message, + timestamp: new Date().toISOString(), + type: "sent" as const, + }; + dispatch({ type: WEBSOCKET_MESSAGE_SENT, payload: sentMessage }); + } else { + console.error( + "Tried to send message but WebSocket connection not established", + ); + } + }, []); return { connect, diff --git a/studio/src/pages/RequestorPage/useRequestorHistory.ts b/studio/src/pages/RequestorPage/useRequestorHistory.ts index d95caec03..62e5077c9 100644 --- a/studio/src/pages/RequestorPage/useRequestorHistory.ts +++ b/studio/src/pages/RequestorPage/useRequestorHistory.ts @@ -1,14 +1,21 @@ import { removeQueryParams } from "@/utils"; import { useMemo } from "react"; -import { KeyValueParameter, createKeyValueParameters } from "./KeyValueForm"; +import { + type KeyValueParameter, + createKeyValueParameters, +} from "./KeyValueForm"; import { useSessionHistory } from "./RequestorSessionHistoryContext"; import { - ProbedRoute, - Requestornator, + type ProbedRoute, + type Requestornator, useFetchRequestorRequests, } from "./queries"; import { findMatchedRoute } from "./routes"; -import { RequestMethodInputValue, isRequestMethod, isWsRequest } from "./types"; +import { + type RequestMethodInputValue, + isRequestMethod, + isWsRequest, +} from "./types"; import { sortRequestornatorsDescending } from "./utils"; type RequestorHistoryHookArgs = { diff --git a/studio/src/pages/RequestorPage/useRequestorSubmitHandler.ts b/studio/src/pages/RequestorPage/useRequestorSubmitHandler.ts index 380883b23..03429f00b 100644 --- a/studio/src/pages/RequestorPage/useRequestorSubmitHandler.ts +++ b/studio/src/pages/RequestorPage/useRequestorSubmitHandler.ts @@ -1,8 +1,8 @@ import { useToast } from "@/components/ui/use-toast"; import { useCallback } from "react"; -import { KeyValueParameter } from "./KeyValueForm"; -import { MakeProxiedRequestQueryFn, ProbedRoute } from "./queries"; -import { RequestorBody, RequestorState, useRequestor } from "./reducer"; +import type { KeyValueParameter } from "./KeyValueForm"; +import type { MakeProxiedRequestQueryFn, ProbedRoute } from "./queries"; +import type { RequestorBody, RequestorState, useRequestor } from "./reducer"; import { isWsRequest } from "./types"; export function useRequestorSubmitHandler({ diff --git a/studio/src/pages/RequestorPage/utils.ts b/studio/src/pages/RequestorPage/utils.ts index 7ab35fc35..4a476816a 100644 --- a/studio/src/pages/RequestorPage/utils.ts +++ b/studio/src/pages/RequestorPage/utils.ts @@ -1,4 +1,4 @@ -import { Requestornator } from "./queries"; +import type { Requestornator } from "./queries"; export function sortRequestornatorsDescending( a: Requestornator, diff --git a/studio/src/pages/RequestsPage/RequestsPage.tsx b/studio/src/pages/RequestsPage/RequestsPage.tsx index e09946040..5278eb265 100644 --- a/studio/src/pages/RequestsPage/RequestsPage.tsx +++ b/studio/src/pages/RequestsPage/RequestsPage.tsx @@ -5,7 +5,7 @@ import { useToast } from "@/components/ui/use-toast"; import { type OtelTrace, useOtelTraces } from "@/queries"; import { cn } from "@/utils"; import { TrashIcon } from "@radix-ui/react-icons"; -import { Row, getPaginationRowModel } from "@tanstack/react-table"; +import { type Row, getPaginationRowModel } from "@tanstack/react-table"; import { useCallback, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { isFpxTraceError } from "../RequestDetailsPage/v2/otel-helpers"; diff --git a/studio/src/pages/RequestsPage/columns.tsx b/studio/src/pages/RequestsPage/columns.tsx index f6fada435..01b9d326e 100644 --- a/studio/src/pages/RequestsPage/columns.tsx +++ b/studio/src/pages/RequestsPage/columns.tsx @@ -1,5 +1,5 @@ import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; -import { type ColumnDef } from "@tanstack/react-table"; +import type { ColumnDef } from "@tanstack/react-table"; import { Status } from "@/components/ui/status"; import type { OtelTrace } from "@/queries"; diff --git a/studio/src/pages/SettingsPage/AISettingsForm.tsx b/studio/src/pages/SettingsPage/AISettingsForm.tsx index 9bc94ba52..3d21cf7e4 100644 --- a/studio/src/pages/SettingsPage/AISettingsForm.tsx +++ b/studio/src/pages/SettingsPage/AISettingsForm.tsx @@ -26,7 +26,7 @@ import { AnthropicModelOptions, OpenAiModelOptions, ProviderOptions, - Settings, + type Settings, } from "@fiberplane/fpx-types"; import { CaretDownIcon, diff --git a/studio/src/pages/SettingsPage/FpxWorkerProxySettingsForm.tsx b/studio/src/pages/SettingsPage/FpxWorkerProxySettingsForm.tsx index 736d5c996..f21bdd08d 100644 --- a/studio/src/pages/SettingsPage/FpxWorkerProxySettingsForm.tsx +++ b/studio/src/pages/SettingsPage/FpxWorkerProxySettingsForm.tsx @@ -10,7 +10,7 @@ import { import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/utils"; -import { Settings } from "@fiberplane/fpx-types"; +import type { Settings } from "@fiberplane/fpx-types"; import { useSettingsForm } from "./form"; export function FpxWorkerProxySettingsForm({ diff --git a/studio/src/pages/SettingsPage/ProxyRequestsSettingsForm.tsx b/studio/src/pages/SettingsPage/ProxyRequestsSettingsForm.tsx index 18c6fed0d..e74c2d2b1 100644 --- a/studio/src/pages/SettingsPage/ProxyRequestsSettingsForm.tsx +++ b/studio/src/pages/SettingsPage/ProxyRequestsSettingsForm.tsx @@ -10,7 +10,7 @@ import { import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/utils"; -import { Settings } from "@fiberplane/fpx-types"; +import type { Settings } from "@fiberplane/fpx-types"; import { useSettingsForm } from "./form"; // TODO: automatically restart the fpx studio when this is changed diff --git a/studio/src/pages/SettingsPage/SettingsPage.tsx b/studio/src/pages/SettingsPage/SettingsPage.tsx index 50354f735..2489c0b03 100644 --- a/studio/src/pages/SettingsPage/SettingsPage.tsx +++ b/studio/src/pages/SettingsPage/SettingsPage.tsx @@ -10,7 +10,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useFetchSettings } from "@/queries"; import { cn } from "@/utils"; -import { Settings } from "@fiberplane/fpx-types"; +import type { Settings } from "@fiberplane/fpx-types"; import { CaretDownIcon } from "@radix-ui/react-icons"; import { useState } from "react"; import { AISettingsForm } from "./AISettingsForm"; diff --git a/studio/src/pages/SettingsPage/form/form.tsx b/studio/src/pages/SettingsPage/form/form.tsx index 7e06ff1ee..0be1db430 100644 --- a/studio/src/pages/SettingsPage/form/form.tsx +++ b/studio/src/pages/SettingsPage/form/form.tsx @@ -4,7 +4,7 @@ import { errorHasMessage } from "@/utils"; import { CLAUDE_3_5_SONNET, GPT_4o, - Settings, + type Settings, SettingsSchema, } from "@fiberplane/fpx-types"; import { zodResolver } from "@hookform/resolvers/zod"; diff --git a/studio/src/queries/settings.ts b/studio/src/queries/settings.ts index 505576116..4e806bf13 100644 --- a/studio/src/queries/settings.ts +++ b/studio/src/queries/settings.ts @@ -22,7 +22,7 @@ export function useFetchSettings() { export function useSetting(key: T) { const { data } = useFetchSettings(); return useMemo(() => { - if (data && data[key]) { + if (data && key in data && data[key]) { return data[key]; } }, [data, key]); diff --git a/studio/src/queries/traces-otel.ts b/studio/src/queries/traces-otel.ts index d45edf277..39b10e559 100644 --- a/studio/src/queries/traces-otel.ts +++ b/studio/src/queries/traces-otel.ts @@ -1,4 +1,4 @@ -import { QueryFunctionContext, useQuery } from "@tanstack/react-query"; +import { type QueryFunctionContext, useQuery } from "@tanstack/react-query"; import { z } from "zod"; import { MIZU_TRACES_KEY } from "./queries"; diff --git a/studio/src/queries/vscodeLinks.ts b/studio/src/queries/vscodeLinks.ts index 8c1d1956b..5015ff66e 100644 --- a/studio/src/queries/vscodeLinks.ts +++ b/studio/src/queries/vscodeLinks.ts @@ -76,7 +76,7 @@ function parseStackTrace(stack: string) { .filter((l) => !l.startsWith("at neon")); // Attempt to match the regex pattern against the provided stack trace - let match; + let match: RegExpMatchArray | null = null; for (const stack of stackLines) { match = stack.match(regex); if (match) { @@ -95,8 +95,8 @@ function parseStackTrace(stack: string) { line: Number.parseInt(line, 10), column: Number.parseInt(column, 10), }; - } else { - // Return null or throw an error if no match is found - return null; } + + // Return null or throw an error if no match is found + return null; } diff --git a/studio/src/vite-env-override.d.ts b/studio/src/vite-env-override.d.ts index dc8730432..1722cd61d 100644 --- a/studio/src/vite-env-override.d.ts +++ b/studio/src/vite-env-override.d.ts @@ -1,5 +1,5 @@ declare module "*.svg" { - import * as React from "react"; + import type * as React from "react"; const ReactComponent: React.FunctionComponent< React.ComponentProps<"svg"> & { title?: string }