diff --git a/.changeset/strong-hornets-jog.md b/.changeset/strong-hornets-jog.md new file mode 100644 index 00000000000..fc5489e114b --- /dev/null +++ b/.changeset/strong-hornets-jog.md @@ -0,0 +1,5 @@ +--- +'@graphql-hive/laboratory': major +--- + +First release diff --git a/package.json b/package.json index e524a037a03..317163fbbee 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "ip": "npm:neoip@2.1.0", "miniflare@3>undici": "^7.18.2", "tailwindcss": "3.4.17", + "@graphql-hive/laboratory>tailwindcss": "4.1.18", "@hive/app>tailwindcss": "4.1.18", "@tailwindcss/node>tailwindcss": "4.1.18", "@tailwindcss/vite>tailwindcss": "4.1.18", @@ -153,7 +154,8 @@ "seroval@<1.4.1": "^1.4.1", "fast-xml-parser@<5.3.6": "^5.3.6", "minimatch@10.x.x": "^10.2.2", - "qs@<6.14.2": "^6.14.2" + "qs@<6.14.2": "^6.14.2", + "amqplib": "^0.8.0" }, "patchedDependencies": { "mjml-core@4.14.0": "patches/mjml-core@4.14.0.patch", diff --git a/packages/libraries/laboratory/components.json b/packages/libraries/laboratory/components.json new file mode 100644 index 00000000000..b266af2d175 --- /dev/null +++ b/packages/libraries/laboratory/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/laboratory/components", + "utils": "@/laboratory/lib/utils", + "ui": "@/laboratory/components/ui", + "lib": "@/laboratory/lib", + "hooks": "@/laboratory/hooks" + }, + "registries": {} +} diff --git a/packages/libraries/laboratory/index.html b/packages/libraries/laboratory/index.html new file mode 100644 index 00000000000..4ae65e503e2 --- /dev/null +++ b/packages/libraries/laboratory/index.html @@ -0,0 +1,16 @@ + + + + + + + Hive Laboratory + + + + + +
+ + + diff --git a/packages/libraries/laboratory/package.json b/packages/libraries/laboratory/package.json new file mode 100644 index 00000000000..09e8b09654a --- /dev/null +++ b/packages/libraries/laboratory/package.json @@ -0,0 +1,145 @@ +{ + "name": "@graphql-hive/laboratory", + "version": "0.0.1", + "type": "module", + "main": "./dist/hive-laboratory.cjs.js", + "module": "./dist/hive-laboratory.es.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/hive-laboratory.es.js", + "require": "./dist/hive-laboratory.cjs.js" + }, + "./umd": "./dist/hive-laboratory.umd.js" + }, + "jsdelivr": "./dist/hive-laboratory.umd.js", + "unpkg": "./dist/hive-laboratory.umd.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "vite build --config vite.lib.config.ts && vite build --config vite.umd.config.ts", + "dev": "vite", + "dev:electron": "VITE_TARGET=electron concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"", + "lint": "eslint .", + "preview": "vite preview" + }, + "peerDependencies": { + "@tanstack/react-form": "^1.23.8", + "date-fns": "^4.1.0", + "graphql-ws": "^6.0.6", + "lucide-react": "^0.548.0", + "lz-string": "^1.5.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "tslib": "^2.8.1", + "zod": "^4.1.12" + }, + "devDependencies": { + "@dagrejs/dagre": "^1.1.8", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@eslint/js": "^9.39.2", + "@mlc-ai/web-llm": "^0.2.80", + "@monaco-editor/react": "4.8.0-rc.2", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.8", + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@tailwindcss/cli": "^4.1.18", + "@tailwindcss/postcss": "^4.1.18", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-form": "^1.27.7", + "@tanstack/react-router": "^1.154.13", + "@tanstack/react-router-devtools": "^1.154.13", + "@tanstack/router-plugin": "^1.154.13", + "@types/crypto-js": "^4.2.2", + "@types/lodash": "^4.17.23", + "@types/node": "^24.10.9", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "@xyflow/react": "^12.10.0", + "autoprefixer": "^10.4.23", + "babel-plugin-react-compiler": "19.1.0-rc.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "color": "^5.0.3", + "concurrently": "^9.2.1", + "crypto-js": "^4.2.0", + "date-fns": "^4.1.0", + "esbuild": "^0.25.12", + "eslint": "^9.39.2", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^16.5.0", + "graphql": "^16.12.0", + "graphql-ws": "^6.0.6", + "lodash": "^4.17.23", + "lucide-react": "^0.548.0", + "lz-string": "^1.5.0", + "monaco-editor": "^0.52.2", + "monaco-graphql": "^1.7.3", + "monacopilot": "^1.2.12", + "next-themes": "^0.4.6", + "postcss": "^8.5.6", + "postcss-prefixwrap": "^1.57.2", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-resizable-panels": "^3.0.6", + "react-shadow": "^20.6.0", + "rollup-plugin-typescript2": "^0.36.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "tailwindcss-scoped-preflight": "^3.5.7", + "tslib": "^2.8.1", + "tsup": "^8.5.1", + "tw-animate-css": "^1.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.53.1", + "unplugin-dts": "1.0.0-beta.6", + "vite": "npm:rolldown-vite@7.1.14", + "vite-plugin-commonjs": "^0.10.4", + "vite-plugin-dts": "^4.5.4", + "vite-plugin-monaco-editor": "^1.1.0", + "wait-on": "^9.0.3", + "zod": "^4.3.6" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + }, + "sideEffects": [ + "**/*.css" + ], + "build": { + "appId": "com.guild.hive.laboratory", + "productName": "Hive Laboratory", + "artifactName": "HiveLab-${version}-Do_Not_Open_(Seriously_It's_Pre-Release)-${os}-${arch}.${ext}", + "files": [ + "dist/**/*", + "electron/**/*" + ], + "directories": { + "buildResources": "assets" + } + } +} diff --git a/packages/web/app/src/laboratory/components/graphql-type.tsx b/packages/libraries/laboratory/src/components/graphql-type.tsx similarity index 81% rename from packages/web/app/src/laboratory/components/graphql-type.tsx rename to packages/libraries/laboratory/src/components/graphql-type.tsx index e05b5eb7832..cfb04958d5d 100644 --- a/packages/web/app/src/laboratory/components/graphql-type.tsx +++ b/packages/libraries/laboratory/src/components/graphql-type.tsx @@ -15,7 +15,7 @@ export const GraphQLType = (props: { return ( - ! + ! ); } @@ -23,9 +23,9 @@ export const GraphQLType = (props: { if (props.type instanceof GraphQLList) { return ( - [ + [ - ] + ] ); } diff --git a/packages/web/app/src/laboratory/components/icons.tsx b/packages/libraries/laboratory/src/components/icons.tsx similarity index 100% rename from packages/web/app/src/laboratory/components/icons.tsx rename to packages/libraries/laboratory/src/components/icons.tsx diff --git a/packages/web/app/src/laboratory/components/laboratory/builder.tsx b/packages/libraries/laboratory/src/components/laboratory/builder.tsx similarity index 66% rename from packages/web/app/src/laboratory/components/laboratory/builder.tsx rename to packages/libraries/laboratory/src/components/laboratory/builder.tsx index fccd0b7c621..dbb99cbdb32 100644 --- a/packages/web/app/src/laboratory/components/laboratory/builder.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/builder.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from "react"; import { GraphQLEnumType, GraphQLObjectType, @@ -6,8 +6,8 @@ import { GraphQLUnionType, type GraphQLArgument, type GraphQLField, -} from 'graphql'; -import { throttle } from 'lodash'; +} from "graphql"; +import { throttle } from "lodash"; import { BoxIcon, ChevronDownIcon, @@ -15,36 +15,44 @@ import { CuboidIcon, FolderIcon, RotateCcwIcon, -} from 'lucide-react'; -import { GraphQLType } from '@/laboratory/components/graphql-type'; -import { GraphQLIcon } from '@/laboratory/components/icons'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; -import { Button } from '@/laboratory/components/ui/button'; -import { Checkbox } from '@/laboratory/components/ui/checkbox'; +} from "lucide-react"; +import { GraphQLType } from "@/components/graphql-type"; +import { GraphQLIcon } from "@/components/icons"; +import { useLaboratory } from "@/components/laboratory/context"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, -} from '@/laboratory/components/ui/collapsible'; +} from "@/components/ui/collapsible"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, -} from '@/laboratory/components/ui/empty'; +} from "@/components/ui/empty"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, -} from '@/laboratory/components/ui/input-group'; -import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/laboratory/components/ui/tabs'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip'; -import type { LaboratoryOperation } from '@/laboratory/lib/operations'; -import { getOpenPaths, isArgInQuery, isPathInQuery } from '@/laboratory/lib/operations.utils'; -import { cn } from '@/laboratory/lib/utils'; +} from "@/components/ui/input-group"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { LaboratoryOperation } from "@/lib/operations"; +import { + getOpenPaths, + isArgInQuery, + isPathInQuery, +} from "@/lib/operations.utils"; +import { cn } from "@/lib/utils"; export const BuilderArgument = (props: { field: GraphQLArgument; @@ -65,36 +73,43 @@ export const BuilderArgument = (props: { }, [props.operation, activeOperation]); const path = useMemo(() => { - return props.path.join('.'); + return props.path.join("."); }, [props.path]); const isInQuery = useMemo(() => { - return isArgInQuery(operation?.query ?? '', path, props.field.name); + return isArgInQuery(operation?.query ?? "", path, props.field.name); }, [operation?.query, path, props.field.name]); return ( - + {isOpen && (
{args.length > 0 && ( @@ -200,10 +225,10 @@ export const BuilderScalarField = (props: { - - {args.map(arg => ( + + {args.map((arg) => (
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} checked={isInQuery} - disabled={activeTab?.type !== 'operation'} - onCheckedChange={checked => { + disabled={activeTab?.type !== "operation"} + onCheckedChange={(checked) => { if (checked) { - addPathToActiveOperation(props.path.join('.')); + addPathToActiveOperation(props.path.join(".")); } else { - deletePathFromActiveOperation(props.path.join('.')); + deletePathFromActiveOperation(props.path.join(".")); } }} /> @@ -289,28 +321,30 @@ export const BuilderObjectField = (props: { }, [props.operation, activeOperation]); const isOpen = useMemo(() => { - return props.openPaths.includes(props.path.join('.')); + return props.openPaths.includes(props.path.join(".")); }, [props.openPaths, props.path]); const setIsOpen = useCallback( (isOpen: boolean) => { props.setOpenPaths( isOpen - ? [...props.openPaths, props.path.join('.')] - : props.openPaths.filter(path => path !== props.path.join('.')), + ? [...props.openPaths, props.path.join(".")] + : props.openPaths.filter((path) => path !== props.path.join(".")) ); }, - [props], + [props] ); const fields = useMemo( () => Object.values( ( - schema?.getType(props.field.type.toString().replace(/\[|\]|!/g, '')) as GraphQLObjectType - )?.getFields?.() ?? {}, + schema?.getType( + props.field.type.toString().replace(/\[|\]|!/g, "") + ) as GraphQLObjectType + )?.getFields?.() ?? {} ), - [schema, props.field.type], + [schema, props.field.type] ); const args = useMemo(() => { @@ -318,15 +352,17 @@ export const BuilderObjectField = (props: { }, [props.field]); const hasArgs = useMemo(() => { - return args.some(arg => isArgInQuery(operation?.query ?? '', props.path.join('.'), arg.name)); + return args.some((arg) => + isArgInQuery(operation?.query ?? "", props.path.join("."), arg.name) + ); }, [operation?.query, args, props.path]); const path = useMemo(() => { - return props.path.join('.'); + return props.path.join("."); }, [props.path]); const isInQuery = useMemo(() => { - return isPathInQuery(operation?.query ?? '', path); + return isPathInQuery(operation?.query ?? "", path); }, [operation?.query, path]); return ( @@ -335,28 +371,28 @@ export const BuilderObjectField = (props: { - + {isOpen && (
{args.length > 0 && ( @@ -378,10 +414,10 @@ export const BuilderObjectField = (props: { - - {args.map(arg => ( + + {args.map((arg) => ( )} - {fields?.map(child => ( + {fields?.map((child) => ( { const { schema } = useLaboratory(); - const type = schema?.getType(props.field.type.toString().replace(/\[|\]|!/g, '')); + const type = schema?.getType( + props.field.type.toString().replace(/\[|\]|!/g, "") + ); if ( !type || @@ -474,9 +519,10 @@ export const Builder = (props: { operation?: LaboratoryOperation | null; isReadOnly?: boolean; }) => { - const { schema, activeOperation, endpoint, setEndpoint, defaultEndpoint } = useLaboratory(); + const { schema, activeOperation, endpoint, setEndpoint, defaultEndpoint } = + useLaboratory(); - const [endpointValue, setEndpointValue] = useState(endpoint ?? ''); + const [endpointValue, setEndpointValue] = useState(endpoint ?? ""); const [openPaths, setOpenPaths] = useState([]); const operation = useMemo(() => { @@ -485,7 +531,7 @@ export const Builder = (props: { useEffect(() => { if (schema) { - const newOpenPaths = getOpenPaths(operation?.query ?? ''); + const newOpenPaths = getOpenPaths(operation?.query ?? ""); if (newOpenPaths.length > 0) { setOpenPaths(newOpenPaths); @@ -496,27 +542,27 @@ export const Builder = (props: { const queryFields = useMemo( () => Object.values(schema?.getQueryType()?.getFields?.() ?? {}), - [schema], + [schema] ); const mutationFields = useMemo( () => Object.values(schema?.getMutationType()?.getFields?.() ?? {}), - [schema], + [schema] ); const subscriptionFields = useMemo( () => Object.values(schema?.getSubscriptionType()?.getFields?.() ?? {}), - [schema], + [schema] ); - const [tabValue, setTabValue] = useState('query'); + const [tabValue, setTabValue] = useState("query"); const throttleSetEndpoint = useMemo( () => throttle((endpoint: string) => { setEndpoint(endpoint); }, 1000), - [setEndpoint], + [setEndpoint] ); useEffect(() => { @@ -524,12 +570,12 @@ export const Builder = (props: { }, [endpointValue, throttleSetEndpoint]); const restoreEndpoint = useCallback(() => { - setEndpointValue(endpoint ?? ''); - setEndpoint(defaultEndpoint ?? ''); + setEndpointValue(endpoint ?? ""); + setEndpoint(defaultEndpoint ?? ""); }, [defaultEndpoint, setEndpointValue]); return ( -
+
Builder
@@ -542,7 +588,7 @@ export const Builder = (props: { className="p-1! size-6 rounded-sm" disabled={openPaths.length === 0} > - + Collapse all @@ -554,14 +600,18 @@ export const Builder = (props: { setEndpointValue(e.currentTarget.value)} + onChange={(e) => setEndpointValue(e.currentTarget.value)} /> {defaultEndpoint && ( - + @@ -581,9 +631,13 @@ export const Builder = (props: { onValueChange={setTabValue} className="flex size-full flex-col gap-0" > -
+
- + Query
- {queryFields?.map(field => ( + {queryFields?.map((field) => ( - {mutationFields?.map(field => ( + {mutationFields?.map((field) => ( - {subscriptionFields?.map(field => ( + {subscriptionFields?.map((field) => ( - + - No endpoint selected + + No endpoint selected + - You haven't selected any endpoint yet. Get started by selecting an endpoint. + You haven't selected any endpoint yet. Get started by selecting + an endpoint. diff --git a/packages/web/app/src/laboratory/components/laboratory/collections.tsx b/packages/libraries/laboratory/src/components/laboratory/collections.tsx similarity index 68% rename from packages/web/app/src/laboratory/components/laboratory/collections.tsx rename to packages/libraries/laboratory/src/components/laboratory/collections.tsx index ba66c0f97b9..e54c3e527c0 100644 --- a/packages/web/app/src/laboratory/components/laboratory/collections.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/collections.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useState } from "react"; import { FolderIcon, FolderOpenIcon, @@ -6,9 +6,9 @@ import { SearchIcon, TrashIcon, XIcon, -} from 'lucide-react'; -import { GraphQLIcon } from '@/laboratory/components/icons'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; +} from "lucide-react"; +import { GraphQLIcon } from "@/components/icons"; +import { useLaboratory } from "@/components/laboratory/context"; import { AlertDialog, AlertDialogAction, @@ -19,13 +19,13 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, -} from '@/laboratory/components/ui/alert-dialog'; -import { Button } from '@/laboratory/components/ui/button'; +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, -} from '@/laboratory/components/ui/collapsible'; +} from "@/components/ui/collapsible"; import { Empty, EmptyContent, @@ -33,16 +33,16 @@ import { EmptyHeader, EmptyMedia, EmptyTitle, -} from '@/laboratory/components/ui/empty'; -import { Input } from '@/laboratory/components/ui/input'; -import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area'; -import { Tooltip, TooltipContent } from '@/laboratory/components/ui/tooltip'; +} from "@/components/ui/empty"; +import { Input } from "@/components/ui/input"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; import type { LaboratoryCollection, LaboratoryCollectionOperation, -} from '@/laboratory/lib/collections'; -import { cn } from '@/laboratory/lib/utils'; -import { TooltipTrigger } from '@radix-ui/react-tooltip'; +} from "@/lib/collections"; +import { cn } from "@/lib/utils"; +import { TooltipTrigger } from "@radix-ui/react-tooltip"; export const CollectionItem = (props: { collection: LaboratoryCollection }) => { const { @@ -64,24 +64,24 @@ export const CollectionItem = (props: { collection: LaboratoryCollection }) => { - + {isOpen && - props.collection.operations.map(operation => { + props.collection.operations.map((operation) => { const isActive = activeOperation?.id === operation.id; return ( ); @@ -248,21 +264,22 @@ export const CollectionsSearchResult = (props: { items: CollectionsSearchResultI }; export const Collections = () => { - const [search, setSearch] = useState(''); - const { collections, openAddCollectionDialog, checkPermissions } = useLaboratory(); + const [search, setSearch] = useState(""); + const { collections, openAddCollectionDialog, checkPermissions } = + useLaboratory(); const searchResults = useMemo(() => { return collections .reduce((acc, collection) => { return [ ...acc, - ...collection.operations.map(operation => ({ + ...collection.operations.map((operation) => ({ ...operation, parent: collection, })), ]; }, [] as CollectionsSearchResultItem[]) - .filter(item => { + .filter((item) => { return item.name.toLowerCase().includes(search.toLowerCase()); }); }, [collections, search]); @@ -273,7 +290,7 @@ export const Collections = () => {
Collections
- {checkPermissions?.('collections:create') && ( + {checkPermissions?.("collections:create") && ( Add collection @@ -290,23 +307,23 @@ export const Collections = () => { )}
-
- +
+ setSearch(e.target.value)} + onChange={(e) => setSearch(e.target.value)} /> {search.length > 0 && ( )}
@@ -321,9 +338,11 @@ export const Collections = () => { - + - No results found + + No results found + No collections found matching your search. @@ -331,21 +350,29 @@ export const Collections = () => { ) ) : collections.length > 0 ? ( - collections.map(item => ) + collections.map((item) => ( + + )) ) : ( - + - No collections yet + + No collections yet + - You haven't created any collections yet. Get started by adding your first - collection. + You haven't created any collections yet. Get started by + adding your first collection. - diff --git a/packages/web/app/src/laboratory/components/laboratory/command.tsx b/packages/libraries/laboratory/src/components/laboratory/command.tsx similarity index 79% rename from packages/web/app/src/laboratory/components/laboratory/command.tsx rename to packages/libraries/laboratory/src/components/laboratory/command.tsx index a0103901053..e1f7b2537ca 100644 --- a/packages/web/app/src/laboratory/components/laboratory/command.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/command.tsx @@ -1,6 +1,12 @@ -import { Fragment, useEffect, useState } from 'react'; -import { FilePlus2Icon, FolderPlusIcon, PlayIcon, RefreshCcwIcon, ServerIcon } from 'lucide-react'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; +import { Fragment, useEffect, useState } from "react"; +import { + FilePlus2Icon, + FolderPlusIcon, + PlayIcon, + RefreshCcwIcon, + ServerIcon, +} from "lucide-react"; +import { useLaboratory } from "@/components/laboratory/context"; import { CommandDialog, CommandEmpty, @@ -10,9 +16,12 @@ import { CommandList, CommandSeparator, CommandShortcut, -} from '@/laboratory/components/ui/command'; +} from "@/components/ui/command"; -export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) => void }) { +export function Command(props: { + open?: boolean; + onOpenChange?: (open: boolean) => void; +}) { const { endpoint, openAddCollectionDialog, @@ -35,7 +44,7 @@ export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) useEffect(() => { const down = (e: KeyboardEvent) => { - if (e.key === 'j' && (e.metaKey || e.ctrlKey)) { + if (e.key === "j" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); const newOpen = !open; setOpen(newOpen); @@ -43,15 +52,15 @@ export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) } }; - document.addEventListener('keydown', down); - return () => document.removeEventListener('keydown', down); + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); }, [open, props]); return ( <> { + onOpenChange={(newOpen) => { setOpen(newOpen); props.onOpenChange?.(newOpen); }} @@ -74,14 +83,14 @@ export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) { const newOperation = addOperation({ - name: '', - query: '', - variables: '', - headers: '', - extensions: '', + name: "", + query: "", + variables: "", + headers: "", + extensions: "", }); const tab = addTab({ - type: 'operation', + type: "operation", data: newOperation, }); @@ -131,9 +140,9 @@ export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) { const tab = - tabs.find(t => t.type === 'env') ?? + tabs.find((t) => t.type === "env") ?? addTab({ - type: 'env', + type: "env", data: env ?? { variables: {} }, }); @@ -147,10 +156,10 @@ export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) { const tab = - tabs.find(t => t.type === 'preflight') ?? + tabs.find((t) => t.type === "preflight") ?? addTab({ - type: 'preflight', - data: preflight ?? { script: '' }, + type: "preflight", + data: preflight ?? { script: "" }, }); setActiveTab(tab); @@ -162,8 +171,8 @@ export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) {plugins - .filter(plugin => !!plugin.commands?.length) - .map(plugin => ( + .filter((plugin) => !!plugin.commands?.length) + .map((plugin) => ( @@ -175,11 +184,11 @@ export function Command(props: { open?: boolean; onOpenChange?: (open: boolean) setOpen(false); }} > - {typeof command.icon === 'function' + {typeof command.icon === "function" ? command.icon(laboratory, {}) : command.icon} - {typeof command.name === 'function' + {typeof command.name === "function" ? command.name(laboratory, {}) : command.name} diff --git a/packages/web/app/src/laboratory/components/laboratory/context.tsx b/packages/libraries/laboratory/src/components/laboratory/context.tsx similarity index 87% rename from packages/web/app/src/laboratory/components/laboratory/context.tsx rename to packages/libraries/laboratory/src/components/laboratory/context.tsx index e6caf7a7d42..47aa791662e 100644 --- a/packages/web/app/src/laboratory/components/laboratory/context.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/context.tsx @@ -5,47 +5,32 @@ import { type LaboratoryCollectionOperation, type LaboratoryCollectionsActions, type LaboratoryCollectionsState, -} from '@/laboratory/lib/collections'; -import { - type LaboratoryEndpointActions, - type LaboratoryEndpointState, -} from '@/laboratory/lib/endpoint'; -import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/laboratory/lib/env'; +} from '@/lib/collections'; +import { type LaboratoryEndpointActions, type LaboratoryEndpointState } from '@/lib/endpoint'; +import type { LaboratoryEnv, LaboratoryEnvActions, LaboratoryEnvState } from '@/lib/env'; import type { LaboratoryHistory, LaboratoryHistoryActions, LaboratoryHistoryState, -} from '@/laboratory/lib/history'; +} from '@/lib/history'; import { type LaboratoryOperation, type LaboratoryOperationsActions, type LaboratoryOperationsState, -} from '@/laboratory/lib/operations'; -import { - LaboratoryPlugin, - LaboratoryPluginsActions, - LaboratoryPluginsState, -} from '@/laboratory/lib/plugins'; +} from '@/lib/operations'; +import { LaboratoryPlugin, LaboratoryPluginsActions, LaboratoryPluginsState } from '@/lib/plugins'; import type { LaboratoryPreflight, LaboratoryPreflightActions, LaboratoryPreflightState, -} from '@/laboratory/lib/preflight'; +} from '@/lib/preflight'; import type { LaboratorySettings, LaboratorySettingsActions, LaboratorySettingsState, -} from '@/laboratory/lib/settings'; -import type { - LaboratoryTab, - LaboratoryTabsActions, - LaboratoryTabsState, -} from '@/laboratory/lib/tabs'; -import type { - LaboratoryTest, - LaboratoryTestActions, - LaboratoryTestState, -} from '@/laboratory/lib/tests'; +} from '@/lib/settings'; +import type { LaboratoryTab, LaboratoryTabsActions, LaboratoryTabsState } from '@/lib/tabs'; +import type { LaboratoryTest, LaboratoryTestActions, LaboratoryTestState } from '@/lib/tests'; type LaboratoryContextState = LaboratoryCollectionsState & LaboratoryEndpointState & @@ -58,6 +43,7 @@ type LaboratoryContextState = LaboratoryCollectionsState & LaboratoryPluginsState & LaboratoryTestState & { isFullScreen?: boolean; + theme?: 'light' | 'dark'; }; type LaboratoryContextActions = LaboratoryCollectionsActions & LaboratoryEndpointActions & @@ -73,9 +59,7 @@ type LaboratoryContextActions = LaboratoryCollectionsActions & openUpdateEndpointDialog?: () => void; openAddTestDialog?: () => void; openPreflightPromptModal?: (props: { - title?: string; - description?: string; - placeholder?: string; + placeholder: string; defaultValue?: string; onSubmit?: (value: string | null) => void; }) => void; @@ -108,6 +92,7 @@ export interface LaboratoryPermissions { } export interface LaboratoryApi { + theme?: 'light' | 'dark'; defaultEndpoint?: string | null; onEndpointChange?: (endpoint: string | null) => void; defaultSchemaIntrospection?: IntrospectionQuery | null; @@ -144,9 +129,7 @@ export interface LaboratoryApi { openUpdateEndpointDialog?: () => void; openAddTestDialog?: () => void; openPreflightPromptModal?: (props: { - title?: string; - description?: string; - placeholder?: string; + placeholder: string; defaultValue?: string; onSubmit?: (value: string | null) => void; }) => void; diff --git a/packages/web/app/src/laboratory/components/laboratory/editor.tsx b/packages/libraries/laboratory/src/components/laboratory/editor.tsx similarity index 91% rename from packages/web/app/src/laboratory/components/laboratory/editor.tsx rename to packages/libraries/laboratory/src/components/laboratory/editor.tsx index 2096378670f..3975f446d03 100644 --- a/packages/web/app/src/laboratory/components/laboratory/editor.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/editor.tsx @@ -1,11 +1,12 @@ import { forwardRef, useEffect, useId, useImperativeHandle, useRef } from 'react'; +import color from 'color'; import * as monaco from 'monaco-editor'; import { initializeMode } from 'monaco-graphql/initializeMode'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; +import { useLaboratory } from '@/components/laboratory/context'; import MonacoEditor, { loader } from '@monaco-editor/react'; if (typeof window !== 'undefined') { - (window as any).monaco = monaco; + (window as Window & typeof globalThis & { monaco: typeof monaco }).monaco = monaco; } loader.config({ monaco }); @@ -73,7 +74,7 @@ const darkTheme: monaco.editor.IStandaloneThemeData = { ], colors: { 'editor.foreground': '#f6f8fa', - 'editor.background': '#18181b', + 'editor.background': '#0f1214', 'editor.selectionBackground': '#2A2F34', 'editor.inactiveSelectionBackground': '#2A2F34', 'editor.lineHighlightBackground': '#2A2F34', @@ -87,6 +88,15 @@ const darkTheme: monaco.editor.IStandaloneThemeData = { monaco.editor.defineTheme('hive-laboratory-dark', darkTheme); +const lightTheme: monaco.editor.IStandaloneThemeData = { + base: 'vs', + inherit: true, + rules: [], + colors: {}, +}; + +monaco.editor.defineTheme('hive-laboratory-light', lightTheme); + monaco.languages.setMonarchTokensProvider('dotenv', { tokenizer: { root: [ @@ -116,7 +126,7 @@ export const Editor = forwardRef< >((props, ref) => { const id = useId(); const editorRef = useRef(null); - const { introspection, endpoint } = useLaboratory(); + const { introspection, endpoint, theme } = useLaboratory(); useEffect(() => { if (introspection) { @@ -182,13 +192,16 @@ export const Editor = forwardRef< { editorRef.current = editor; }} loading={null} options={{ ...props.options, + lineNumbers: 'on', + cursorStyle: 'line', + cursorBlinking: 'smooth', padding: { top: 16, }, diff --git a/packages/web/app/src/laboratory/components/laboratory/env.tsx b/packages/libraries/laboratory/src/components/laboratory/env.tsx similarity index 50% rename from packages/web/app/src/laboratory/components/laboratory/env.tsx rename to packages/libraries/laboratory/src/components/laboratory/env.tsx index c00c2f726d9..e14ae70e2ff 100644 --- a/packages/web/app/src/laboratory/components/laboratory/env.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/env.tsx @@ -1,33 +1,33 @@ -import { useLaboratory } from '@/laboratory/components/laboratory/context'; -import { Editor } from '@/laboratory/components/laboratory/editor'; +import { useLaboratory } from "@/components/laboratory/context"; +import { Editor } from "@/components/laboratory/editor"; export const Env = () => { const { env, setEnv } = useLaboratory(); return ( -
+
`${key}=${value}`) - .join('\n')} - onChange={value => { + .join("\n")} + onChange={(value) => { setEnv({ variables: Object.fromEntries( value - ?.split('\n') - .filter(line => line.trim() && !line.trim().startsWith('#')) - .map(line => { + ?.split("\n") + .filter((line) => line.trim() && !line.trim().startsWith("#")) + .map((line) => { const parts = line.split(/=(.*)/s); - return [parts[0].trim(), (parts[1] ?? '').trim()]; - }) ?? [], + return [parts[0].trim(), (parts[1] ?? "").trim()]; + }) ?? [] ), }); }} language="dotenv" options={{ scrollbar: { - horizontal: 'hidden', + horizontal: "hidden", }, }} /> diff --git a/packages/libraries/laboratory/src/components/laboratory/history-item.tsx b/packages/libraries/laboratory/src/components/laboratory/history-item.tsx new file mode 100644 index 00000000000..d0139b561cb --- /dev/null +++ b/packages/libraries/laboratory/src/components/laboratory/history-item.tsx @@ -0,0 +1,28 @@ +import { useMemo } from "react"; +import { useLaboratory } from "@/components/laboratory/context"; +import { Operation } from "@/components/laboratory/operation"; +import { LaboratoryHistoryRequest } from "@/lib/history"; + +export const HistoryItem = () => { + const { activeTab, history } = useLaboratory(); + + const historyItem = useMemo(() => { + if (activeTab?.type !== "history") { + return null; + } + + return ( + history.find( + (h) => h.id === (activeTab.data as LaboratoryHistoryRequest).id + ) ?? null + ); + }, [history, activeTab]); + + if (!historyItem) { + return null; + } + + return ( + + ); +}; diff --git a/packages/web/app/src/laboratory/components/laboratory/history.tsx b/packages/libraries/laboratory/src/components/laboratory/history.tsx similarity index 67% rename from packages/web/app/src/laboratory/components/laboratory/history.tsx rename to packages/libraries/laboratory/src/components/laboratory/history.tsx index 0c4b8868154..e58e613193d 100644 --- a/packages/web/app/src/laboratory/components/laboratory/history.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/history.tsx @@ -1,7 +1,13 @@ -import { useCallback, useMemo, useState } from 'react'; -import { format } from 'date-fns'; -import { ClockIcon, FolderClockIcon, FolderOpenIcon, HistoryIcon, TrashIcon } from 'lucide-react'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; +import { useCallback, useMemo, useState } from "react"; +import { format } from "date-fns"; +import { + ClockIcon, + FolderClockIcon, + FolderOpenIcon, + HistoryIcon, + TrashIcon, +} from "lucide-react"; +import { useLaboratory } from "@/components/laboratory/context"; import { AlertDialog, AlertDialogAction, @@ -12,31 +18,40 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, -} from '@/laboratory/components/ui/alert-dialog'; -import { Button } from '@/laboratory/components/ui/button'; +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, -} from '@/laboratory/components/ui/collapsible'; +} from "@/components/ui/collapsible"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, -} from '@/laboratory/components/ui/empty'; -import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip'; -import type { LaboratoryHistory, LaboratoryHistoryRequest } from '@/laboratory/lib/history'; -import { cn } from '@/laboratory/lib/utils'; +} from "@/components/ui/empty"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { + LaboratoryHistory, + LaboratoryHistoryRequest, +} from "@/lib/history"; +import { cn } from "@/lib/utils"; -export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequest }) => { +export const HistoryOperationItem = (props: { + historyItem: LaboratoryHistoryRequest; +}) => { const { activeTab, addTab, setActiveTab, deleteHistory } = useLaboratory(); const isActive = useMemo(() => { return ( - activeTab?.type === 'history' && + activeTab?.type === "history" && (activeTab.data as LaboratoryHistoryRequest).id === props.historyItem.id ); }, [activeTab, props.historyItem]); @@ -49,7 +64,8 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ return ( props.historyItem.status < 200 || props.historyItem.status >= 300 || - ('response' in props.historyItem && JSON.parse(props.historyItem.response).errors) + ("response" in props.historyItem && + JSON.parse(props.historyItem.response).errors) ); }, [props.historyItem]); @@ -57,31 +73,36 @@ export const HistoryOperationItem = (props: { historyItem: LaboratoryHistoryRequ - - {props.group.items.map(h => { - return ; + + {props.group.items.map((h) => { + return ( + + ); })} @@ -198,19 +233,21 @@ export const HistoryGroup = (props: { group: { date: string; items: LaboratoryHi }; export const History = () => { - const { history, deleteAllHistory, tabs, setTabs, setActiveTab } = useLaboratory(); + const { history, deleteAllHistory, tabs, setTabs, setActiveTab } = + useLaboratory(); const historyItems = useMemo(() => { return history.sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); }, [history]); const goupedByDate = useMemo(() => { return historyItems.reduce( (acc, h) => { - const date = format(new Date(h.createdAt), 'dd MMM yyyy'); - let item = acc.find(i => i.date === date); + const date = format(new Date(h.createdAt), "dd MMM yyyy"); + let item = acc.find((i) => i.date === date); if (!item) { item = { date, items: [] }; @@ -222,15 +259,15 @@ export const History = () => { return acc; }, - [] as { date: string; items: LaboratoryHistory[] }[], + [] as { date: string; items: LaboratoryHistory[] }[] ); }, [historyItems]); const handleDeleteAllHistory = useCallback(() => { deleteAllHistory(); - setTabs(tabs.filter(t => t.type !== 'history')); + setTabs(tabs.filter((t) => t.type !== "history")); - const newTab = tabs.find(t => t.type !== 'history'); + const newTab = tabs.find((t) => t.type !== "history"); if (newTab) { setActiveTab(newTab); @@ -239,7 +276,7 @@ export const History = () => { return (
-
+
History
@@ -249,7 +286,7 @@ export const History = () => { - - - - - - - - - - Add collection - - Add a new collection of operations to your laboratory. - - -
-
{ - e.preventDefault(); - void addCollectionForm.handleSubmit(); - }} - > - - - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; - return ( - - Name - field.handleChange(e.target.value)} - aria-invalid={isInvalid} - placeholder="Enter name of the collection" - autoComplete="off" - /> - {isInvalid && } - - ); - }} - - -
-
- - - - - - -
-
- - - - Add test - Add a new test to your laboratory. - -
-
{ - e.preventDefault(); - void addTestForm.handleSubmit(); - }} - > - - - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; - return ( - - Name + + +
+ + + + + Update endpoint + Update the endpoint of your laboratory. + +
+ { + e.preventDefault(); + void updateEndpointForm.handleSubmit(); + }} + > + + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + return ( field.handleChange(e.target.value)} aria-invalid={isInvalid} - placeholder="Enter name of the test" + placeholder="Enter endpoint" autoComplete="off" /> - {isInvalid && } - - ); - }} - - - -
- - - - - - -
-
- - - - -
+ ); + }} + +
+ +
+ + + + + + +
+
+ + + + + Add collection + + Add a new collection of operations to your laboratory. + + +
+
{ + e.preventDefault(); + void addCollectionForm.handleSubmit(); + }} + > + + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + Name + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Enter name of the collection" + autoComplete="off" + /> + {isInvalid && } + + ); + }} + + +
+
+ + + + + + +
+
+ + + + Add test + Add a new test to your laboratory. + +
+
{ + e.preventDefault(); + void addTestForm.handleSubmit(); + }} + > + + + {field => { + const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + return ( + + Name + field.handleChange(e.target.value)} + aria-invalid={isInvalid} + placeholder="Enter name of the test" + autoComplete="off" + /> + {isInvalid && } + + ); + }} + + +
+
+ + + + + + +
+
+ + + + +
+ ); }; diff --git a/packages/web/app/src/laboratory/components/laboratory/operation.tsx b/packages/libraries/laboratory/src/components/laboratory/operation.tsx similarity index 70% rename from packages/web/app/src/laboratory/components/laboratory/operation.tsx rename to packages/libraries/laboratory/src/components/laboratory/operation.tsx index b0132857f6c..e4a70f30086 100644 --- a/packages/web/app/src/laboratory/components/laboratory/operation.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/operation.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from "react"; import { BookmarkIcon, CircleCheckIcon, @@ -11,16 +11,16 @@ import { PowerIcon, PowerOffIcon, SquarePenIcon, -} from 'lucide-react'; -import { compressToEncodedURIComponent } from 'lz-string'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import { toast } from 'sonner'; -import { z } from 'zod'; -import { Builder } from '@/laboratory/components/laboratory/builder'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; -import { Editor } from '@/laboratory/components/laboratory/editor'; -import { Badge } from '@/laboratory/components/ui/badge'; -import { Button } from '@/laboratory/components/ui/button'; +} from "lucide-react"; +import { compressToEncodedURIComponent } from "lz-string"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Builder } from "@/components/laboratory/builder"; +import { useLaboratory } from "@/components/laboratory/context"; +import { Editor } from "@/components/laboratory/editor"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, @@ -29,49 +29,52 @@ import { DialogFooter, DialogHeader, DialogTitle, -} from '@/laboratory/components/ui/dialog'; +} from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, -} from '@/laboratory/components/ui/dropdown-menu'; +} from "@/components/ui/dropdown-menu"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, -} from '@/laboratory/components/ui/empty'; -import { Field, FieldGroup, FieldLabel } from '@/laboratory/components/ui/field'; +} from "@/components/ui/empty"; +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, -} from '@/laboratory/components/ui/resizable'; -import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area'; +} from "@/components/ui/resizable"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/laboratory/components/ui/select'; -import { Spinner } from '@/laboratory/components/ui/spinner'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/laboratory/components/ui/tabs'; -import { Toggle } from '@/laboratory/components/ui/toggle'; +} from "@/components/ui/select"; +import { Spinner } from "@/components/ui/spinner"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Toggle } from "@/components/ui/toggle"; import type { LaboratoryHistory, LaboratoryHistoryRequest, LaboratoryHistorySubscription, -} from '@/laboratory/lib/history'; -import type { LaboratoryOperation } from '@/laboratory/lib/operations'; -import { cn } from '@/laboratory/lib/utils'; -import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'; -import { useForm } from '@tanstack/react-form'; +} from "@/lib/history"; +import type { LaboratoryOperation } from "@/lib/operations"; +import { cn } from "@/lib/utils"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { useForm } from "@tanstack/react-form"; -const variablesUri = monaco.Uri.file('variables.json'); +const variablesUri = monaco.Uri.file("variables.json"); -const Variables = (props: { operation?: LaboratoryOperation | null; isReadOnly?: boolean }) => { +const Variables = (props: { + operation?: LaboratoryOperation | null; + isReadOnly?: boolean; +}) => { const { activeOperation, updateActiveOperation } = useLaboratory(); const operation = useMemo(() => { @@ -81,11 +84,11 @@ const Variables = (props: { operation?: LaboratoryOperation | null; isReadOnly?: return ( { + onChange={(value) => { updateActiveOperation({ - variables: value ?? '', + variables: value ?? "", }); }} options={{ @@ -95,7 +98,10 @@ const Variables = (props: { operation?: LaboratoryOperation | null; isReadOnly?: ); }; -const Headers = (props: { operation?: LaboratoryOperation | null; isReadOnly?: boolean }) => { +const Headers = (props: { + operation?: LaboratoryOperation | null; + isReadOnly?: boolean; +}) => { const { activeOperation, updateActiveOperation } = useLaboratory(); const operation = useMemo(() => { @@ -104,11 +110,11 @@ const Headers = (props: { operation?: LaboratoryOperation | null; isReadOnly?: b return ( { + uri={monaco.Uri.file("headers.json")} + value={operation?.headers ?? ""} + onChange={(value) => { updateActiveOperation({ - headers: value ?? '', + headers: value ?? "", }); }} options={{ @@ -118,7 +124,10 @@ const Headers = (props: { operation?: LaboratoryOperation | null; isReadOnly?: b ); }; -const Extensions = (props: { operation?: LaboratoryOperation | null; isReadOnly?: boolean }) => { +const Extensions = (props: { + operation?: LaboratoryOperation | null; + isReadOnly?: boolean; +}) => { const { activeOperation, updateActiveOperation } = useLaboratory(); const operation = useMemo(() => { @@ -127,11 +136,11 @@ const Extensions = (props: { operation?: LaboratoryOperation | null; isReadOnly? return ( { + uri={monaco.Uri.file("extensions.json")} + value={operation?.extensions ?? ""} + onChange={(value) => { updateActiveOperation({ - extensions: value ?? '', + extensions: value ?? "", }); }} options={{ @@ -141,13 +150,17 @@ const Extensions = (props: { operation?: LaboratoryOperation | null; isReadOnly? ); }; -export const ResponseBody = ({ historyItem }: { historyItem?: LaboratoryHistory | null }) => { +export const ResponseBody = ({ + historyItem, +}: { + historyItem?: LaboratoryHistory | null; +}) => { return ( { +export const ResponseHeaders = ({ + historyItem, +}: { + historyItem?: LaboratoryHistory | null; +}) => { return ( { +export const ResponsePreflight = ({ + historyItem, +}: { + historyItem?: LaboratoryHistory | null; +}) => { return (
{historyItem?.preflightLogs?.map((log, i) => (
- {log.createdAt}{' '} + + {log.createdAt} + {" "} {log.level.toUpperCase()} - {' '} - {log.message.join(' ')} + {" "} + {log.message.join(" ")}
))}
@@ -208,7 +231,7 @@ export const ResponseSubscription = ({ return (
-
+
Subscription
{isActiveOperationLoading ? ( @@ -226,19 +249,23 @@ export const ResponseSubscription = ({
{historyItem?.responses - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .sort( + (a, b) => + new Date(b.createdAt).getTime() - + new Date(a.createdAt).getTime() + ) .map((response, i) => { const value = [ `// ${response.createdAt}`, - '', + "", JSON.stringify(JSON.parse(response.data), null, 2), - ].join('\n'); + ].join("\n"); - const height = 20.5 * value.split('\n').length; + const height = 20.5 * value.split("\n").length; return (
@@ -251,7 +278,7 @@ export const ResponseSubscription = ({ readOnly: true, scrollBeyondLastLine: false, scrollbar: { - vertical: 'hidden', + vertical: "hidden", handleMouseWheel: false, alwaysConsumeMouseWheel: false, }, @@ -268,7 +295,11 @@ export const ResponseSubscription = ({ ); }; -export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryRequest | null }) => { +export const Response = ({ + historyItem, +}: { + historyItem?: LaboratoryHistoryRequest | null; +}) => { const isError = useMemo(() => { if (!historyItem) { return false; @@ -281,12 +312,15 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque return ( historyItem.status < 200 || historyItem.status >= 300 || - ('response' in historyItem && JSON.parse(historyItem.response).errors) + ("response" in historyItem && JSON.parse(historyItem.response).errors) ); }, [historyItem]); return ( - + Response @@ -294,17 +328,18 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque Headers - {historyItem?.preflightLogs && historyItem?.preflightLogs.length > 0 && ( - - Preflight - - )} + {historyItem?.preflightLogs && + historyItem?.preflightLogs.length > 0 && ( + + Preflight + + )} {historyItem ? (
{historyItem?.status && ( {!isError ? ( @@ -316,19 +351,23 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque )} {historyItem?.duration && ( - + - {Math.round((historyItem as LaboratoryHistoryRequest).duration!)} + {Math.round( + (historyItem as LaboratoryHistoryRequest).duration! + )} ms )} {historyItem?.size && ( - + - {Math.round((historyItem as LaboratoryHistoryRequest).size! / 1024)} + {Math.round( + (historyItem as LaboratoryHistoryRequest).size! / 1024 + )} KB @@ -350,7 +389,7 @@ export const Response = ({ historyItem }: { historyItem?: LaboratoryHistoryReque }; const saveToCollectionFormSchema = z.object({ - collectionId: z.string().min(1, 'Collection is required'), + collectionId: z.string().min(1, "Collection is required"), }); export const Query = (props: { @@ -395,9 +434,9 @@ export const Query = (props: { setPluginsState(result?.pluginsState ?? {}); - if (result?.status === 'error') { + if (result?.status === "error") { const newItemHistory = addHistory({ - headers: '{}', + headers: "{}", operation, preflightLogs: result?.logs ?? [], response: `{ @@ -408,7 +447,7 @@ export const Query = (props: { ] }`, createdAt: new Date().toISOString(), - } as Omit); + } as Omit); props.onAfterOperationRun?.(newItemHistory); return; @@ -420,12 +459,12 @@ export const Query = (props: { operation, preflightLogs: result?.logs ?? [], createdAt: new Date().toISOString(), - } as Omit); + } as Omit); void runActiveOperation(endpoint, { env: result?.env, headers: result?.headers, - onResponse: data => { + onResponse: (data) => { addResponseToHistory(newItemHistory.id, data); }, }); @@ -452,12 +491,16 @@ export const Query = (props: { status, duration, size, - headers: JSON.stringify(Object.fromEntries(response.headers.entries()), null, 2), + headers: JSON.stringify( + Object.fromEntries(response.headers.entries()), + null, + 2 + ), operation, preflightLogs: result?.logs ?? [], response: responseText, createdAt: new Date().toISOString(), - } as Omit); + } as Omit); props.onAfterOperationRun?.(newItemHistory); } @@ -475,7 +518,7 @@ export const Query = (props: { useEffect(() => { const down = (e: KeyboardEvent) => { - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); e.stopPropagation(); @@ -483,15 +526,17 @@ export const Query = (props: { } }; - document.addEventListener('keydown', down, { capture: true }); - return () => document.removeEventListener('keydown', down, { capture: true }); + document.addEventListener("keydown", down, { capture: true }); + return () => + document.removeEventListener("keydown", down, { capture: true }); }, [handleRunOperation]); - const [isSaveToCollectionDialogOpen, setIsSaveToCollectionDialogOpen] = useState(false); + const [isSaveToCollectionDialogOpen, setIsSaveToCollectionDialogOpen] = + useState(false); const saveToCollectionForm = useForm({ defaultValues: { - collectionId: '', + collectionId: "", }, validators: { onSubmit: saveToCollectionFormSchema, @@ -502,13 +547,13 @@ export const Query = (props: { } addOperationToCollection(value.collectionId, { - id: operation.id ?? '', - name: operation.name ?? '', - query: operation.query ?? '', - variables: operation.variables ?? '', - headers: operation.headers ?? '', - extensions: operation.extensions ?? '', - description: '', + id: operation.id ?? "", + name: operation.name ?? "", + query: operation.query ?? "", + variables: operation.variables ?? "", + headers: operation.headers ?? "", + extensions: operation.extensions ?? "", + description: "", }); setIsSaveToCollectionDialogOpen(false); @@ -517,18 +562,24 @@ export const Query = (props: { const openSaveToCollectionDialog = useCallback(() => { saveToCollectionForm.reset({ - collectionId: collections[0]?.id ?? '', + collectionId: collections[0]?.id ?? "", }); setIsSaveToCollectionDialogOpen(true); }, [saveToCollectionForm, collections]); const isActiveOperationSavedToCollection = useMemo(() => { - return collections.some(c => c.operations.some(o => o.id === operation?.id)); + return collections.some((c) => + c.operations.some((o) => o.id === operation?.id) + ); }, [operation?.id, collections]); const share = useCallback( - (options: { variables?: boolean; headers?: boolean; extensions?: boolean }) => { + (options: { + variables?: boolean; + headers?: boolean; + extensions?: boolean; + }) => { const value = compressToEncodedURIComponent( JSON.stringify({ n: operation?.name, @@ -536,21 +587,24 @@ export const Query = (props: { v: options.variables ? operation?.variables : undefined, h: options.headers ? operation?.headers : undefined, e: options.extensions ? operation?.extensions : undefined, - }), + }) ); void navigator.clipboard.writeText( - `${window.location.origin}${window.location.pathname}?share=${value}`, + `${window.location.origin}${window.location.pathname}?share=${value}` ); - toast.success('Operation copied to clipboard'); + toast.success("Operation copied to clipboard"); }, - [operation], + [operation] ); return (
- + Add collection @@ -561,15 +615,16 @@ export const Query = (props: {
{ + onSubmit={(e) => { e.preventDefault(); void saveToCollectionForm.handleSubmit(); }} > - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + {(field) => { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; return ( @@ -579,11 +634,14 @@ export const Query = (props: { value={field.state.value} onValueChange={field.handleChange} > - + - {collections.map(c => ( + {collections.map((c) => ( {c.name} @@ -607,9 +665,9 @@ export const Query = (props: {
-
+
Operation - {checkPermissions?.('collectionsOperations:create') && ( + {checkPermissions?.("collectionsOperations:create") && ( - {isActiveOperationSavedToCollection ? 'Saved' : 'Save'} + {isActiveOperationSavedToCollection ? "Saved" : "Save"} )}
@@ -635,25 +693,31 @@ export const Query = (props: { share({ variables: true })}> Share with variables - share({ variables: true, extensions: true })}> + share({ variables: true, extensions: true })} + > Share with variables and extensions share({ variables: true, headers: true, extensions: true })} + onClick={() => + share({ variables: true, headers: true, extensions: true }) + } > Share with variables, extensions, headers { setPreflight({ - ...(preflight ?? { script: '', enabled: true }), + ...(preflight ?? { script: "", enabled: true }), enabled: !preflight?.enabled, }); }} @@ -703,9 +767,9 @@ export const Query = (props: { setActiveTab( addTab({ - type: 'operation', + type: "operation", data: addOperation(operation), - }), + }) ); }} > @@ -719,10 +783,10 @@ export const Query = (props: { { + value={operation?.query ?? ""} + onChange={(value) => { updateActiveOperation({ - query: value ?? '', + query: value ?? "", }); }} language="graphql" @@ -750,8 +814,11 @@ export const Operation = (props: { return ( props.historyItem ?? history - .filter(h => h.operation.id === operation?.id) - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0] ?? + .filter((h) => h.operation.id === operation?.id) + .sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + )[0] ?? null ); }, [history, props.historyItem, operation?.id]); @@ -761,7 +828,7 @@ export const Operation = (props: { }, [props.historyItem]); return ( -
+
@@ -773,8 +840,15 @@ export const Operation = (props: { - - + + Variables @@ -803,7 +877,7 @@ export const Operation = (props: { {historyItem ? ( <> - {'responses' in historyItem ? ( + {"responses" in historyItem ? ( ) : ( @@ -813,11 +887,12 @@ export const Operation = (props: { - + No history yet - No response available yet. Run your operation to see the response here. + No response available yet. Run your operation to see the + response here. diff --git a/packages/web/app/src/laboratory/components/laboratory/preflight.tsx b/packages/libraries/laboratory/src/components/laboratory/preflight.tsx similarity index 81% rename from packages/web/app/src/laboratory/components/laboratory/preflight.tsx rename to packages/libraries/laboratory/src/components/laboratory/preflight.tsx index 128d35b8b12..120a55ea220 100644 --- a/packages/web/app/src/laboratory/components/laboratory/preflight.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/preflight.tsx @@ -1,23 +1,23 @@ -import { useCallback } from 'react'; -import { HistoryIcon, PlayIcon } from 'lucide-react'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; -import { Editor } from '@/laboratory/components/laboratory/editor'; -import { Button } from '@/laboratory/components/ui/button'; +import { useCallback } from "react"; +import { HistoryIcon, PlayIcon } from "lucide-react"; +import { useLaboratory } from "@/components/laboratory/context"; +import { Editor } from "@/components/laboratory/editor"; +import { Button } from "@/components/ui/button"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, -} from '@/laboratory/components/ui/empty'; +} from "@/components/ui/empty"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, -} from '@/laboratory/components/ui/resizable'; -import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area'; -import { runIsolatedLabScript } from '@/laboratory/lib/preflight'; -import { cn } from '@/laboratory/lib/utils'; +} from "@/components/ui/resizable"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { runIsolatedLabScript } from "@/lib/preflight"; +import { cn } from "@/lib/utils"; export const Preflight = () => { const { @@ -39,23 +39,21 @@ export const Preflight = () => { } const result = await runIsolatedLabScript( - preflight?.script ?? '', + preflight?.script ?? "", env ?? { variables: {} }, - (title, defaultValue, options) => { - return new Promise(resolve => { + (placeholder, defaultValue) => { + return new Promise((resolve) => { openPreflightPromptModal?.({ - title, - description: options?.description, - placeholder: options?.placeholder, + placeholder, defaultValue, - onSubmit: value => { + onSubmit: (value) => { resolve(value); }, }); }); }, plugins, - pluginsState, + pluginsState ); setEnv(result?.env ?? { variables: {} }); @@ -65,12 +63,17 @@ export const Preflight = () => { return ( - +
-
+
Preflight
- @@ -78,11 +81,11 @@ export const Preflight = () => {
{ + value={preflight?.script ?? ""} + onChange={(value) => { setPreflight({ - ...(preflight ?? { script: '', enabled: true }), - script: value ?? '', + ...(preflight ?? { script: "", enabled: true }), + script: value ?? "", }); }} language="typescript" @@ -97,13 +100,13 @@ export const Preflight = () => { request: { headers: Headers; }; - prompt: (title: string, defaultValue: string, options?: { placeholder?: string; description?: string }) => Promise; + prompt: (placeholder: string, defaultValue: string) => Promise; CryptoJS: typeof CryptoJS; plugins: { ${plugins - .filter(plugin => plugin.preflight?.lab?.definition) - .map(plugin => plugin.preflight?.lab?.definition) - .join('\n')} + .filter((plugin) => plugin.preflight?.lab?.definition) + .map((plugin) => plugin.preflight?.lab?.definition) + .join("\n")} } } @@ -246,17 +249,18 @@ export const Preflight = () => { `, ]} options={{ - readOnly: !checkPermissions?.('preflight:update'), + readOnly: !checkPermissions?.("preflight:update"), }} />
- - {preflight?.lastTestResult?.logs && preflight?.lastTestResult?.logs.length > 0 ? ( + + {preflight?.lastTestResult?.logs && + preflight?.lastTestResult?.logs.length > 0 ? (
-
+
Logs
@@ -264,19 +268,21 @@ export const Preflight = () => {
{preflight?.lastTestResult?.logs.map((log, i) => (
- {log.createdAt}{' '} + + {log.createdAt} + {" "} {log.level.toUpperCase()} - {' '} - {log.message.join(' ')} + {" "} + {log.message.join(" ")}
))}
@@ -287,7 +293,7 @@ export const Preflight = () => { - + No logs yet diff --git a/packages/web/app/src/laboratory/components/laboratory/settings.tsx b/packages/libraries/laboratory/src/components/laboratory/settings.tsx similarity index 65% rename from packages/web/app/src/laboratory/components/laboratory/settings.tsx rename to packages/libraries/laboratory/src/components/laboratory/settings.tsx index 86adaf2aeaa..5ab685fe8c2 100644 --- a/packages/web/app/src/laboratory/components/laboratory/settings.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/settings.tsx @@ -1,25 +1,25 @@ -import { z } from 'zod'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; +import { z } from "zod"; +import { useLaboratory } from "@/components/laboratory/context"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from '@/laboratory/components/ui/card'; -import { Field, FieldGroup, FieldLabel } from '@/laboratory/components/ui/field'; +} from "@/components/ui/card"; +import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '@/laboratory/components/ui/select'; -import { useForm } from '@tanstack/react-form'; +} from "@/components/ui/select"; +import { useForm } from "@tanstack/react-form"; const settingsFormSchema = z.object({ fetch: z.object({ - credentials: z.enum(['include', 'omit', 'same-origin']), + credentials: z.enum(["include", "omit", "same-origin"]), }), }); @@ -37,7 +37,7 @@ export const Settings = () => { }); return ( -
+
{ Fetch - Configure the fetch options for the laboratory. + + Configure the fetch options for the laboratory. + - {field => { - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + {(field) => { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid; return ( @@ -61,8 +64,10 @@ export const Settings = () => { diff --git a/packages/web/app/src/laboratory/components/laboratory/tabs.tsx b/packages/libraries/laboratory/src/components/laboratory/tabs.tsx similarity index 66% rename from packages/web/app/src/laboratory/components/laboratory/tabs.tsx rename to packages/libraries/laboratory/src/components/laboratory/tabs.tsx index ac2619d597e..11ad3abd182 100644 --- a/packages/web/app/src/laboratory/components/laboratory/tabs.tsx +++ b/packages/libraries/laboratory/src/components/laboratory/tabs.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { capitalize } from 'lodash'; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { capitalize } from "lodash"; import { CirclePlus, FileIcon, @@ -12,29 +12,33 @@ import { ScrollTextIcon, SettingsIcon, XIcon, -} from 'lucide-react'; -import { GraphQLIcon } from '@/laboratory/components/icons'; -import { useLaboratory } from '@/laboratory/components/laboratory/context'; -import { Button } from '@/laboratory/components/ui/button'; +} from "lucide-react"; +import { GraphQLIcon } from "@/components/icons"; +import { useLaboratory } from "@/components/laboratory/context"; +import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, -} from '@/laboratory/components/ui/context-menu'; -import { ScrollArea, ScrollBar } from '@/laboratory/components/ui/scroll-area'; -import * as Sortable from '@/laboratory/components/ui/sortable'; -import { Spinner } from '@/laboratory/components/ui/spinner'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/laboratory/components/ui/tooltip'; -import { getOperationName, getOperationType } from '@/laboratory/lib/operations.utils'; -import { LaboratoryPluginTab } from '@/laboratory/lib/plugins'; +} from "@/components/ui/context-menu"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import * as Sortable from "@/components/ui/sortable"; +import { Spinner } from "@/components/ui/spinner"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { getOperationName, getOperationType } from "@/lib/operations.utils"; +import { LaboratoryPluginTab } from "@/lib/plugins"; import type { LaboratoryTab, LaboratoryTabHistory, LaboratoryTabOperation, LaboratoryTabTest, -} from '@/laboratory/lib/tabs'; -import { cn } from '@/laboratory/lib/utils'; +} from "@/lib/tabs"; +import { cn } from "@/lib/utils"; export const Tab = (props: { item: LaboratoryTab; @@ -61,10 +65,10 @@ export const Tab = (props: { bypassMouseDownRef.current = false; } - document.addEventListener('mouseup', handleMouseUp); + document.addEventListener("mouseup", handleMouseUp); return () => { - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener("mouseup", handleMouseUp); }; }, []); @@ -73,29 +77,35 @@ export const Tab = (props: { }, [props.activeTab, props.item]); const historyItem = useMemo(() => { - if (props.item.type !== 'history') { + if (props.item.type !== "history") { return null; } return history.find( - h => props.item.type === 'history' && h.id === (props.item.data as LaboratoryTabHistory).id, + (h) => + props.item.type === "history" && + h.id === (props.item.data as LaboratoryTabHistory).id ); }, [history, props.item]); const operation = useMemo(() => { - if (props.item.type !== 'operation') { + if (props.item.type !== "operation") { return null; } - return operations.find(o => o.id === (props.item.data as LaboratoryTabOperation).id); + return operations.find( + (o) => o.id === (props.item.data as LaboratoryTabOperation).id + ); }, [props.item, operations]); const test = useMemo(() => { - if (props.item.type !== 'test') { + if (props.item.type !== "test") { return null; } - return tests.find(t => t.id === (props.item.data as LaboratoryTabTest).id); + return tests.find( + (t) => t.id === (props.item.data as LaboratoryTabTest).id + ); }, [props.item, tests]); const isError = useMemo(() => { @@ -104,9 +114,9 @@ export const Tab = (props: { } return ( - ('status' in historyItem && historyItem.status! < 200) || - ('status' in historyItem && historyItem.status! >= 300) || - ('response' in historyItem && JSON.parse(historyItem.response).errors) + ("status" in historyItem && historyItem.status! < 200) || + ("status" in historyItem && historyItem.status! >= 300) || + ("response" in historyItem && JSON.parse(historyItem.response).errors) ); }, [historyItem]); @@ -123,11 +133,16 @@ export const Tab = (props: { }, [props]); const tabName = useMemo(() => { - if (props.item.type === 'operation') { - const name = operation?.name || getOperationName(operation?.query || '') || 'Untitled'; + if (props.item.type === "operation") { + const name = + operation?.name || + getOperationName(operation?.query || "") || + "Untitled"; - if (name === 'Untitled') { - const type = capitalize(getOperationType(operation?.query || '') || 'query'); + if (name === "Untitled") { + const type = capitalize( + getOperationType(operation?.query || "") || "query" + ); return name + type; } @@ -135,14 +150,16 @@ export const Tab = (props: { return name; } - if (props.item.type === 'history') { + if (props.item.type === "history") { const name = historyItem?.operation.name || - getOperationName(historyItem?.operation.query || '') || - 'Untitled'; + getOperationName(historyItem?.operation.query || "") || + "Untitled"; - if (name === 'Untitled') { - const type = capitalize(getOperationType(historyItem?.operation.query || '') || 'query'); + if (name === "Untitled") { + const type = capitalize( + getOperationType(historyItem?.operation.query || "") || "query" + ); return name + type; } @@ -150,20 +167,20 @@ export const Tab = (props: { return name; } - if (props.item.type === 'preflight') { - return 'Preflight'; + if (props.item.type === "preflight") { + return "Preflight"; } - if (props.item.type === 'env') { - return 'Environment Variables'; + if (props.item.type === "env") { + return "Environment Variables"; } - if (props.item.type === 'settings') { - return 'Settings'; + if (props.item.type === "settings") { + return "Settings"; } - if (props.item.type === 'test') { - return test?.name || 'Untitled'; + if (props.item.type === "test") { + return test?.name || "Untitled"; } let customTab: LaboratoryPluginTab> | null = null; @@ -178,45 +195,45 @@ export const Tab = (props: { } if (customTab) { - if (typeof customTab.name === 'function') { + if (typeof customTab.name === "function") { return customTab.name(laboratory, {}); } return customTab.name; } - return 'Untitled'; + return "Untitled"; }, [props.item, historyItem, operation, test]); const tabIcon = useMemo(() => { - if (props.item.type === 'operation') { + if (props.item.type === "operation") { return ; } - if (props.item.type === 'preflight') { + if (props.item.type === "preflight") { return ; } - if (props.item.type === 'env') { + if (props.item.type === "env") { return ; } - if (props.item.type === 'history') { + if (props.item.type === "history") { return ( ); } - if (props.item.type === 'settings') { - return ; + if (props.item.type === "settings") { + return ; } - if (props.item.type === 'test') { + if (props.item.type === "test") { return ; } @@ -232,14 +249,14 @@ export const Tab = (props: { } if (customTab) { - if (typeof customTab.icon === 'function') { + if (typeof customTab.icon === "function") { return customTab.icon(laboratory, {}); } return customTab.icon; } - return ; + return ; }, [props.item, isError]); return ( @@ -249,13 +266,13 @@ export const Tab = (props: { value={props.item.id} asHandle className={cn( - 'data-dragging:opacity-0 flex h-12 w-max items-stretch', - props.isOverlay && 'bg-neutral-3', - props.isOverlay && !isActive && 'h-12', + "data-dragging:opacity-0 flex h-12 w-max items-stretch", + props.isOverlay && "bg-background", + props.isOverlay && !isActive && "h-12" )} >
{ + onMouseDown={(e) => { if (bypassMouseDownRef.current) { return; } @@ -269,18 +286,18 @@ export const Tab = (props: { bypassMouseDownRef.current = true; event.currentTarget.dispatchEvent( - new MouseEvent('mousedown', { + new MouseEvent("mousedown", { ...(event as unknown as MouseEventInit), - }), + }) ); }, 200); }} >
{ props.setActiveTab(props.item); @@ -296,21 +313,25 @@ export const Tab = (props: { > {tabIcon} {tabName} - {props.isOperationLoading(props.item.id) && } - {props.item.readOnly && } + {props.isOperationLoading(props.item.id) && ( + + )} + {props.item.readOnly && ( + + )} { + className="text-muted-foreground size-3" + onMouseDown={(e) => { e.stopPropagation(); }} - onClick={e => { + onClick={(e) => { e.stopPropagation(); props.handleDeleteTab(props.item.id); }} />
-
+
@@ -342,15 +363,15 @@ export const Tabs = ({ className }: { className?: string }) => { const handleAddOperation = useCallback(() => { const newOperation = addOperation({ - name: '', - query: '', - variables: '', - headers: '', - extensions: '', + name: "", + query: "", + variables: "", + headers: "", + extensions: "", }); const tab = addTab({ - type: 'operation', + type: "operation", data: newOperation, }); @@ -359,7 +380,7 @@ export const Tabs = ({ className }: { className?: string }) => { const handleDeleteTab = useCallback( (tabId: string) => { - const tabIndex = tabs.findIndex(t => t.id === tabId); + const tabIndex = tabs.findIndex((t) => t.id === tabId); if (tabIndex === -1) { return; @@ -367,7 +388,7 @@ export const Tabs = ({ className }: { className?: string }) => { const tab = tabs[tabIndex]; - if (tab.type === 'operation') { + if (tab.type === "operation") { deleteOperation((tab.data as LaboratoryTabOperation).id); } @@ -381,7 +402,7 @@ export const Tabs = ({ className }: { className?: string }) => { setActiveTab(tabs[0] ?? null); } }, - [tabs, deleteTab, deleteOperation, setActiveTab], + [tabs, deleteTab, deleteOperation, setActiveTab] ); const handleDeleteAllTabs = useCallback(() => { @@ -391,28 +412,33 @@ export const Tabs = ({ className }: { className?: string }) => { const handleDeleteOtherTabs = useCallback( (excludeTabId: string) => { - const newActiveTab = tabs.find(t => t.id === excludeTabId); + const newActiveTab = tabs.find((t) => t.id === excludeTabId); if (newActiveTab) { - const tabsToDelete = tabs.filter(t => t.id !== excludeTabId); + const tabsToDelete = tabs.filter((t) => t.id !== excludeTabId); const operationsToDelete = tabsToDelete - .filter(t => t.type === 'operation') - .map(t => (t.data as LaboratoryTabOperation).id); + .filter((t) => t.type === "operation") + .map((t) => (t.data as LaboratoryTabOperation).id); - setOperations(operations.filter(o => !operationsToDelete.includes(o.id))); + setOperations( + operations.filter((o) => !operationsToDelete.includes(o.id)) + ); setTabs([newActiveTab]); setActiveTab(newActiveTab); } }, - [tabs, setOperations, operations, setTabs, setActiveTab], + [tabs, setOperations, operations, setTabs, setActiveTab] ); return (
-
+
@@ -423,7 +449,7 @@ export const Tabs = ({ className }: { className?: string }) => { orientation="horizontal" > - {tabs.map(item => { + {tabs.map((item) => { return ( <> { })} - {activeItem => { - const tab = tabs.find(t => t.id === activeItem.value); + {(activeItem) => { + const tab = tabs.find((t) => t.id === activeItem.value); if (!tab) { return null; @@ -467,7 +493,7 @@ export const Tabs = ({ className }: { className?: string }) => {