From ca2d22bab761ae08b742acc6bbc83e5d7c4499bd Mon Sep 17 00:00:00 2001 From: Curry Yang <1019yanglu@gmail.com> Date: Wed, 6 Nov 2024 11:42:19 +0800 Subject: [PATCH] feat: multiple tabs --- .../src/ui/components/document-tab.tsx | 35 +++ .../insomnia/src/ui/components/tabs/tab.tsx | 132 ++++++++ .../src/ui/components/tabs/tabList.tsx | 68 ++++ .../src/ui/components/tags/method-tag.tsx | 23 +- .../ui/context/app/insomnia-tab-context.tsx | 136 ++++++++ packages/insomnia/src/ui/hooks/tab.ts | 292 ++++++++++++++++++ packages/insomnia/src/ui/routes/debug.tsx | 38 ++- packages/insomnia/src/ui/routes/design.tsx | 48 ++- .../insomnia/src/ui/routes/environments.tsx | 19 +- .../insomnia/src/ui/routes/mock-server.tsx | 26 +- .../insomnia/src/ui/routes/organization.tsx | 32 +- packages/insomnia/src/ui/routes/project.tsx | 9 +- packages/insomnia/src/ui/routes/unit-test.tsx | 28 ++ 13 files changed, 835 insertions(+), 51 deletions(-) create mode 100644 packages/insomnia/src/ui/components/document-tab.tsx create mode 100644 packages/insomnia/src/ui/components/tabs/tab.tsx create mode 100644 packages/insomnia/src/ui/components/tabs/tabList.tsx create mode 100644 packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx create mode 100644 packages/insomnia/src/ui/hooks/tab.ts diff --git a/packages/insomnia/src/ui/components/document-tab.tsx b/packages/insomnia/src/ui/components/document-tab.tsx new file mode 100644 index 00000000000..e9e5711e2b7 --- /dev/null +++ b/packages/insomnia/src/ui/components/document-tab.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; + +interface Props { + organizationId: string; + projectId: string; + workspaceId: string; + className?: string; +} + +export const DocumentTab = ({ organizationId, projectId, workspaceId, className }: Props) => { + return ( + + ); +}; diff --git a/packages/insomnia/src/ui/components/tabs/tab.tsx b/packages/insomnia/src/ui/components/tabs/tab.tsx new file mode 100644 index 00000000000..86144e5a64a --- /dev/null +++ b/packages/insomnia/src/ui/components/tabs/tab.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { Button, GridListItem } from 'react-aria-components'; + +import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; +import { Icon } from '../icon'; +import { Tooltip } from '../tooltip'; + +export const enum TabEnum { + Request = 'request', + Folder = 'folder', + Env = 'environment', + Mock = 'mock-server', + MockRoute = 'mock-route', + Document = 'document', + Collection = 'collection', + Runner = 'runner', + TEST = 'test', + TESTSUITE = 'test-suite', +}; + +export interface BaseTab { + type: TabEnum; + name: string; + url: string; + organizationId: string; + projectId: string; + workspaceId: string; + organizationName: string; + projectName: string; + workspaceName: string; + id: string; + [key: string]: string; +}; + +const REQUEST_TAG_MAP: Record = { + 'GET': 'text-[--color-font-surprise] bg-[rgba(var(--color-surprise-rgb),0.5)]', + 'POST': 'text-[--color-font-success] bg-[rgba(var(--color-success-rgb),0.5)]', + 'GQL': 'text-[--color-font-success] bg-[rgba(var(--color-success-rgb),0.5)]', + 'HEAD': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]', + 'OPTIONS': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]', + 'DELETE': 'text-[--color-font-danger] bg-[rgba(var(--color-danger-rgb),0.5)]', + 'PUT': 'text-[--color-font-warning] bg-[rgba(var(--color-warning-rgb),0.5)]', + 'PATCH': 'text-[--color-font-notice] bg-[rgba(var(--color-notice-rgb),0.5)]', + 'WS': 'text-[--color-font-notice] bg-[rgba(var(--color-notice-rgb),0.5)]', + 'gRPC': 'text-[--color-font-info] bg-[rgba(var(--color-info-rgb),0.5)]', +}; + +const WORKSPACE_TAB_UI_MAP: Record = { + [TabEnum.Collection]: { + icon: 'bars', + bgColor: 'bg-[--color-surprise]', + textColor: 'text-[--color-font-surprise]', + }, + [TabEnum.Env]: { + icon: 'code', + bgColor: 'bg-[--color-font]', + textColor: 'text-[--color-bg]', + }, + [TabEnum.Mock]: { + icon: 'server', + bgColor: 'bg-[--color-warning]', + textColor: 'text-[--color-font-warning]', + }, + [TabEnum.Document]: { + icon: 'file', + bgColor: 'bg-[--color-info]', + textColor: 'text-[--color-font-info]', + }, +}; + +export const InsomniaTab = ({ tab }: { tab: BaseTab }) => { + + const { deleteTabById } = useInsomniaTabContext(); + + const renderTabIcon = (type: TabEnum) => { + if (WORKSPACE_TAB_UI_MAP[type]) { + return ( +
+ +
+ ); + } + + if (type === TabEnum.Request || type === TabEnum.MockRoute) { + return ( + {tab.tag} + ); + } + + if (type === TabEnum.Folder) { + return ; + } + if (type === TabEnum.Runner) { + return ; + }; + + if (type === TabEnum.TESTSUITE) { + return ; + } + + return null; + }; + + const handleClose = (id: string) => { + deleteTabById(id); + }; + + return ( + + {({ isSelected, isHovered }) => ( + +
+ {renderTabIcon(tab.type)} + {tab.name} +
+ {isHovered && ( + + )} +
+ {isSelected && } +
+
+ )} +
+ ); +}; diff --git a/packages/insomnia/src/ui/components/tabs/tabList.tsx b/packages/insomnia/src/ui/components/tabs/tabList.tsx new file mode 100644 index 00000000000..29e22f323f9 --- /dev/null +++ b/packages/insomnia/src/ui/components/tabs/tabList.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { GridList, type Key, type Selection } from 'react-aria-components'; +import { useNavigate } from 'react-router-dom'; + +import { useInsomniaTabContext } from '../../context/app/insomnia-tab-context'; +import { Icon } from '../icon'; +import { type BaseTab, InsomniaTab, TabEnum } from './tab'; + +export interface OrganizationTabs { + tabList: BaseTab[]; + activeTabId?: Key | null; +} + +export const TAB_ROUTER_PATH: Record = { + [TabEnum.Collection]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', + [TabEnum.Folder]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/:requestGroupId', + [TabEnum.Request]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId', + [TabEnum.Env]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment', + [TabEnum.Mock]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server', + [TabEnum.Runner]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/runner', + [TabEnum.Document]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/spec', + [TabEnum.MockRoute]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/:mockRouteId', + [TabEnum.TEST]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test', + [TabEnum.TESTSUITE]: '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/*', +}; + +export const OrganizationTabList = ({ showActiveStatus = true }) => { + const { currentOrgTabs } = useInsomniaTabContext(); + const { tabList, activeTabId } = currentOrgTabs; + console.log('activeTabId', activeTabId); + const navigate = useNavigate(); + + const { changeActiveTab } = useInsomniaTabContext(); + + const handleSelectionChange = (keys: Selection) => { + console.log('changeActiveTab'); + if (keys !== 'all') { + console.log('tab change', keys); + const key = [...keys.values()]?.[0] as string; + const tab = tabList.find(tab => tab.id === key); + tab?.url && navigate(tab?.url); + changeActiveTab(key); + } + }; + + if (!tabList.length) { + return null; + }; + + return ( +
+ + {item => } + + +
+ ); +}; diff --git a/packages/insomnia/src/ui/components/tags/method-tag.tsx b/packages/insomnia/src/ui/components/tags/method-tag.tsx index 34e921fa5dd..51983a8f20a 100644 --- a/packages/insomnia/src/ui/components/tags/method-tag.tsx +++ b/packages/insomnia/src/ui/components/tags/method-tag.tsx @@ -1,7 +1,9 @@ import React, { type FC, memo } from 'react'; import { CONTENT_TYPE_GRAPHQL, METHOD_DELETE, METHOD_OPTIONS } from '../../../common/constants'; -import { isEventStreamRequest, type Request } from '../../../models/request'; +import { type GrpcRequest, isGrpcRequest } from '../../../models/grpc-request'; +import { isEventStreamRequest, isRequest, type Request } from '../../../models/request'; +import { isWebSocketRequest, type WebSocketRequest } from '../../../models/websocket-request'; interface Props { method: string; @@ -34,6 +36,25 @@ export function formatMethodName(method: string) { return methodName; } +export const getRequestMethodShortHand = (doc?: Request | WebSocketRequest | GrpcRequest) => { + if (!doc) { + return ''; + } + if (isRequest(doc)) { + return getMethodShortHand(doc); + } + + if (isWebSocketRequest(doc)) { + return 'WS'; + } + + if (isGrpcRequest(doc)) { + return 'gRPC'; + } + + return ''; +}; + export const MethodTag: FC = memo(({ method, override, fullNames }) => { let methodName = method; let overrideName = override; diff --git a/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx new file mode 100644 index 00000000000..7db50910bab --- /dev/null +++ b/packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx @@ -0,0 +1,136 @@ +import React, { createContext, type FC, type PropsWithChildren, useCallback, useContext, useEffect, useRef } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useLocalStorage } from 'react-use'; + +import type { BaseTab } from '../../components/tabs/tab'; +import type { OrganizationTabs } from '../../components/tabs/tabList'; + +interface UpdateInsomniaTabParams { + organizationId: string; + tabList: OrganizationTabs['tabList']; + activeTabId?: string; +} + +interface ContextProps { + currentOrgTabs: OrganizationTabs; + appTabsRef?: React.MutableRefObject; + deleteTabById: (id: string) => void; + addTab: (tab: BaseTab) => void; + changeActiveTab: (id: string) => void; +} + +const InsomniaTabContext = createContext({ + currentOrgTabs: { + tabList: [], + activeTabId: '', + }, + deleteTabById: () => { }, + addTab: () => { }, + changeActiveTab: () => { }, +}); + +interface InsomniaTabs { + [orgId: string]: OrganizationTabs; +}; + +export const InsomniaTabProvider: FC = ({ children }) => { + const { + organizationId, + projectId, + } = useParams() as { + organizationId: string; + projectId: string; + workspaceId: string; + }; + + const [appTabs, setAppTabs] = useLocalStorage('insomnia-tabs', {}); + + // keep a ref of the appTabs to avoid the function recreated, which will cause the useEffect to run again and cannot delete a tab + // file: packages/insomnia/src/ui/hooks/tab.ts + const appTabsRef = useRef(appTabs); + + const navigate = useNavigate(); + + const updateInsomniaTabs = useCallback(({ organizationId, tabList, activeTabId }: UpdateInsomniaTabParams) => { + const newState = { + ...appTabsRef.current, + [organizationId]: { + tabList, + activeTabId, + }, + }; + appTabsRef.current = newState; + setAppTabs(newState); + }, [setAppTabs]); + + const addTab = useCallback((tab: BaseTab) => { + const currentTabs = appTabsRef?.current?.[organizationId] || { tabList: [], activeTabId: '' }; + + updateInsomniaTabs({ + organizationId, + tabList: [...currentTabs.tabList, tab], + activeTabId: tab.id, + }); + }, [organizationId, updateInsomniaTabs]); + + useEffect(() => { + console.log('addTab change'); + }, [addTab]); + + const deleteTabById = useCallback((id: string) => { + const currentTabs = appTabs?.[organizationId]; + if (!currentTabs) { + return; + } + + // If the tab being deleted is the only tab and is active, navigate to the project dashboard + if (currentTabs.activeTabId === id && currentTabs.tabList.length === 1) { + navigate(`/organization/${organizationId}/project/${projectId}`); + updateInsomniaTabs({ + organizationId, + tabList: [], + activeTabId: '', + }); + return; + } + + const index = currentTabs.tabList.findIndex(tab => tab.id === id); + const newTabList = currentTabs.tabList.filter(tab => tab.id !== id); + if (currentTabs.activeTabId === id) { + navigate(newTabList[index - 1 < 0 ? 0 : index - 1]?.url || ''); + } + updateInsomniaTabs({ + organizationId, + tabList: newTabList, + activeTabId: currentTabs.activeTabId === id ? newTabList[index - 1 < 0 ? 0 : index - 1]?.id : currentTabs.activeTabId as string, + }); + }, [appTabs, navigate, organizationId, projectId, updateInsomniaTabs]); + + const changeActiveTab = useCallback((id: string) => { + const currentTabs = appTabsRef?.current?.[organizationId] || { tabList: [], activeTabId: '' }; + if (!currentTabs) { + return; + } + updateInsomniaTabs({ + organizationId, + tabList: currentTabs.tabList, + activeTabId: id, + }); + }, [organizationId, updateInsomniaTabs]); + + return ( + + {children} + + ); +}; + +export const useInsomniaTabContext = () => useContext(InsomniaTabContext); diff --git a/packages/insomnia/src/ui/hooks/tab.ts b/packages/insomnia/src/ui/hooks/tab.ts new file mode 100644 index 00000000000..1874ebbe1c0 --- /dev/null +++ b/packages/insomnia/src/ui/hooks/tab.ts @@ -0,0 +1,292 @@ +import { useCallback, useEffect } from 'react'; +import { matchPath, useLocation } from 'react-router-dom'; + +import type { GrpcRequest } from '../../models/grpc-request'; +import type { MockRoute } from '../../models/mock-route'; +import type { Organization } from '../../models/organization'; +import type { Project } from '../../models/project'; +import type { Request } from '../../models/request'; +import type { RequestGroup } from '../../models/request-group'; +import type { UnitTestSuite } from '../../models/unit-test-suite'; +import type { WebSocketRequest } from '../../models/websocket-request'; +import type { Workspace } from '../../models/workspace'; +import { type BaseTab, TabEnum } from '../components/tabs/tab'; +import { TAB_ROUTER_PATH } from '../components/tabs/tabList'; +import { formatMethodName, getRequestMethodShortHand } from '../components/tags/method-tag'; +import { useInsomniaTabContext } from '../context/app/insomnia-tab-context'; + +interface InsomniaTabProps { + organizationId: string; + projectId: string; + workspaceId: string; + activeProject: Project; + activeWorkspace: Workspace; + activeRequest?: Request | GrpcRequest | WebSocketRequest; + activeRequestGroup?: RequestGroup; + activeOrganization?: Organization; + activeMockRoute?: MockRoute; + unitTestSuite?: UnitTestSuite; +} + +export const useInsomniaTab = ({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeRequest, + activeRequestGroup, + activeOrganization, + activeMockRoute, + unitTestSuite, +}: InsomniaTabProps) => { + + console.log(activeMockRoute, 'activeMockRoute'); + const { appTabsRef, addTab, changeActiveTab } = useInsomniaTabContext(); + + const generateTabUrl = useCallback((type: TabEnum) => { + if (type === TabEnum.Request) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${activeRequest?._id}`; + } + + if (type === TabEnum.Folder) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/${activeRequestGroup?._id}`; + } + + if (type === TabEnum.Collection) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`; + } + + if (type === TabEnum.Env) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment`; + } + + if (type === TabEnum.Runner) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner`; + } + + if (type === TabEnum.Mock) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server`; + } + + if (type === TabEnum.MockRoute) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${activeMockRoute?._id}`; + } + + if (type === TabEnum.Document) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/spec`; + } + + if (type === TabEnum.TEST) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test`; + } + + if (type === TabEnum.TESTSUITE) { + return `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite?._id}`; + } + return ''; + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, organizationId, projectId, unitTestSuite?._id, workspaceId]); + + const location = useLocation(); + + const getTabType = (pathname: string) => { + console.log(pathname); + for (const type in TAB_ROUTER_PATH) { + const ifMatch = matchPath({ + path: TAB_ROUTER_PATH[type as TabEnum], + end: true, + }, pathname); + if (ifMatch) { + return type as TabEnum; + } + } + + return null; + }; + + const getCurrentTab = useCallback((type: TabEnum | null) => { + if (!type) { + return undefined; + } + const currentOrgTabs = appTabsRef?.current?.[organizationId]; + if (type === TabEnum.Request) { + return currentOrgTabs?.tabList.find(tab => tab.id === activeRequest?._id); + } + + if (type === TabEnum.Folder) { + return currentOrgTabs?.tabList.find(tab => tab.id === activeRequestGroup?._id); + } + + if (type === TabEnum.Runner) { + // collection runner tab id is prefixed with 'runner_' + return currentOrgTabs?.tabList.find(tab => tab.id === `runner_${workspaceId}`); + } + + if (type === TabEnum.MockRoute) { + return currentOrgTabs?.tabList.find(tab => tab.id === activeMockRoute?._id); + } + + if (type === TabEnum.TESTSUITE) { + return currentOrgTabs?.tabList.find(tab => tab.id === unitTestSuite?._id); + } + + if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + return currentOrgTabs?.tabList.find(tab => tab.id === workspaceId); + } + return undefined; + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, appTabsRef, organizationId, unitTestSuite?._id, workspaceId]); + + const getTabId = useCallback((type: TabEnum | null): string => { + if (!type) { + return ''; + } + if (type === TabEnum.Request) { + return activeRequest?._id || ''; + } + + if (type === TabEnum.Folder) { + return activeRequestGroup?._id || ''; + } + + if (type === TabEnum.Runner) { + return `runner_${workspaceId}`; + } + + if (type === TabEnum.MockRoute) { + return activeMockRoute?._id || ''; + } + + if (type === TabEnum.TESTSUITE) { + return unitTestSuite?._id || ''; + } + + if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + return workspaceId; + } + + return ''; + }, [activeMockRoute?._id, activeRequest?._id, activeRequestGroup?._id, unitTestSuite?._id, workspaceId]); + + const packTabInfo = useCallback((type: TabEnum): BaseTab | undefined => { + if (!type) { + return undefined; + } + if (type === TabEnum.Request) { + return { + type, + name: activeRequest?.name || 'Untitled request', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + tag: getRequestMethodShortHand(activeRequest), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if (type === TabEnum.Folder) { + return { + type, + name: 'My folder', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + tag: getRequestMethodShortHand(activeRequest), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if ([TabEnum.Collection, TabEnum.Document, TabEnum.Env, TabEnum.Mock, TabEnum.TEST].includes(type)) { + return { + type, + name: activeWorkspace.name, + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if (type === TabEnum.Runner) { + return { + type, + name: 'Runner', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + if (type === TabEnum.MockRoute) { + return { + type, + name: activeMockRoute?.name || 'Untitled mock route', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + tag: formatMethodName(activeMockRoute?.method || ''), + }; + } + + if (type === TabEnum.TESTSUITE) { + return { + type, + name: unitTestSuite?.name || 'Untitled test suite', + url: generateTabUrl(type), + organizationId: organizationId, + projectId: projectId, + workspaceId: workspaceId, + id: getTabId(type), + organizationName: activeOrganization?.name || '', + projectName: activeProject.name, + workspaceName: activeWorkspace.name, + }; + } + + return; + }, [activeMockRoute?.method, activeMockRoute?.name, activeOrganization?.name, activeProject.name, activeRequest, activeWorkspace.name, generateTabUrl, getTabId, organizationId, projectId, unitTestSuite?.name, workspaceId]); + + useEffect(() => { + const type = getTabType(location.pathname); + console.log('tabType:', type); + const currentTab = getCurrentTab(type); + console.log('currentTabExist:', currentTab); + if (!currentTab && type) { + const tabInfo = packTabInfo(type); + if (tabInfo) { + addTab(tabInfo); + return; + } + } + + // keep active tab in sync with the current route + if (currentTab) { + const currentActiveTabId = appTabsRef?.current?.[organizationId]?.activeTabId; + if (currentActiveTabId !== currentTab.id) { + changeActiveTab(currentTab.id); + } + } + }, [addTab, appTabsRef, changeActiveTab, getCurrentTab, location.pathname, organizationId, packTabInfo]); + +}; diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 441fce9c964..867922e99f3 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -68,9 +68,10 @@ import { isWebSocketRequestId, type WebSocketRequest, } from '../../models/websocket-request'; -import { isScratchpad } from '../../models/workspace'; +import { isDesign, isScratchpad } from '../../models/workspace'; import { invariant } from '../../utils/invariant'; import { DropdownHint } from '../components/base/dropdown/dropdown-hint'; +import { DocumentTab } from '../components/document-tab'; import { RequestActionsDropdown } from '../components/dropdowns/request-actions-dropdown'; import { RequestGroupActionsDropdown } from '../components/dropdowns/request-group-actions-dropdown'; import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown'; @@ -96,9 +97,11 @@ import { PlaceholderRequestPane } from '../components/panes/placeholder-request- import { RequestGroupPane } from '../components/panes/request-group-pane'; import { RequestPane } from '../components/panes/request-pane'; import { ResponsePane } from '../components/panes/response-pane'; +import { OrganizationTabList } from '../components/tabs/tabList'; import { getMethodShortHand } from '../components/tags/method-tag'; import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane'; import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane'; +import { useInsomniaTab } from '../hooks/tab'; import { useExecutionState } from '../hooks/use-execution-state'; import { useReadyState } from '../hooks/use-ready-state'; import { @@ -108,11 +111,13 @@ import { useRequestMetaPatcher, useRequestPatcher, } from '../hooks/use-request'; +import { useOrganizationLoaderData } from './organization'; import type { GrpcRequestLoaderData, RequestLoaderData, WebSocketRequestLoaderData, } from './request'; +import type { RequestGroupLoaderData } from './request-group'; import { useRootLoaderData } from './root'; import Runner from './runner'; import type { Child, WorkspaceLoaderData } from './workspace'; @@ -212,6 +217,12 @@ export const Debug: FC = () => { requestId?: string; requestGroupId?: string; }; + + const { activeRequestGroup } = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData || {}; + + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + const [grpcStates, setGrpcStates] = useState( grpcRequests.map(r => ({ requestId: r._id, @@ -739,13 +750,24 @@ export const Debug: FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeRequest, + activeRequestGroup, + activeOrganization, + }); + return (
-
-
- +
+
+ {
+ {isDesign(activeWorkspace) && ( + + )}
{ + { activeCookieJar, caCertificate, clientCertificates, + activeWorkspace, } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const { settings } = useRootLoaderData(); @@ -450,6 +455,18 @@ const Design: FC = () => { } }, [settings.forceVerticalLayout, direction]); + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeOrganization, + }); + return ( @@ -470,29 +487,35 @@ const Design: FC = () => { +
-
+
setEnvironmentModalOpen(true)} />
- - + -
+ +
Spec @@ -983,6 +1006,7 @@ const Design: FC = () => { +
diff --git a/packages/insomnia/src/ui/routes/environments.tsx b/packages/insomnia/src/ui/routes/environments.tsx index f49b3d4989a..8862a043bee 100644 --- a/packages/insomnia/src/ui/routes/environments.tsx +++ b/packages/insomnia/src/ui/routes/environments.tsx @@ -17,11 +17,14 @@ import { handleToggleEnvironmentType } from '../components/editors/environment-u import { Icon } from '../components/icon'; import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; import { showAlert } from '../components/modals'; +import { OrganizationTabList } from '../components/tabs/tabList'; +import { useInsomniaTab } from '../hooks/tab'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; +import { useOrganizationLoaderData } from './organization'; import type { WorkspaceLoaderData } from './workspace'; const Environments = () => { - const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); + const { organizationId = '', projectId = '', workspaceId = '' } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); const routeData = useRouteLoaderData( ':workspaceId' ) as WorkspaceLoaderData; @@ -40,6 +43,7 @@ const Environments = () => { activeEnvironment, subEnvironments, activeWorkspaceMeta, + activeWorkspace, } = routeData; const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(activeEnvironment._id); const isUsingInsomniaCloudSync = Boolean(isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId); @@ -256,6 +260,18 @@ const Environments = () => { sidebar_toggle: toggleSidebar, }); + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeOrganization, + }); + return ( @@ -410,6 +426,7 @@ const Environments = () => { +
diff --git a/packages/insomnia/src/ui/routes/mock-server.tsx b/packages/insomnia/src/ui/routes/mock-server.tsx index 9d80107648d..079b01589b7 100644 --- a/packages/insomnia/src/ui/routes/mock-server.tsx +++ b/packages/insomnia/src/ui/routes/mock-server.tsx @@ -2,7 +2,7 @@ import type { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { Suspense, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { Breadcrumb, Breadcrumbs, Button, GridList, GridListItem, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { type LoaderFunction, NavLink, Route, Routes, useFetcher, useLoaderData, useNavigate, useParams } from 'react-router-dom'; +import { type LoaderFunction, NavLink, Route, Routes, useFetcher, useLoaderData, useNavigate, useParams, useRouteLoaderData } from 'react-router-dom'; import { DEFAULT_SIDEBAR_SIZE } from '../../common/constants'; import * as models from '../../models'; @@ -18,9 +18,13 @@ import { AlertModal } from '../components/modals/alert-modal'; import { AskModal } from '../components/modals/ask-modal'; import { EmptyStatePane } from '../components/panes/empty-state-pane'; import { SvgIcon } from '../components/svg-icon'; +import { OrganizationTabList } from '../components/tabs/tabList'; import { formatMethodName } from '../components/tags/method-tag'; +import { useInsomniaTab } from '../hooks/tab'; import { MockRouteResponse, MockRouteRoute, useMockRoutePatcher } from './mock-route'; +import { useOrganizationLoaderData } from './organization'; import { useRootLoaderData } from './root'; +import type { WorkspaceLoaderData } from './workspace'; export interface MockServerLoaderData { mockServerId: string; mockRoutes: MockRoute[]; @@ -52,6 +56,11 @@ const MockServerRoute = () => { }; const { settings } = useRootLoaderData(); const { mockServerId, mockRoutes } = useLoaderData() as MockServerLoaderData; + + const { activeProject, activeWorkspace } = useRouteLoaderData( + ':workspaceId' + ) as WorkspaceLoaderData; + const fetcher = useFetcher(); const navigate = useNavigate(); const patchMockRoute = useMockRoutePatcher(); @@ -172,6 +181,20 @@ const MockServerRoute = () => { } }, [settings.forceVerticalLayout, direction]); + const { organizations } = useOrganizationLoaderData(); + const activeOrganization = organizations.find(o => o.id === organizationId); + + useInsomniaTab({ + organizationId, + projectId, + workspaceId, + activeProject, + activeWorkspace, + activeOrganization, + mockRouteId, + activeMockRoute: mockRoutes.find(s => s._id === mockRouteId), + }); + return ( @@ -355,6 +378,7 @@ const MockServerRoute = () => { + diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index d7367c60286..3520e7ba60a 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -57,6 +57,7 @@ import { PresentUsers } from '../components/present-users'; import { Toast } from '../components/toast'; import { useAIContext } from '../context/app/ai-context'; import { InsomniaEventStreamProvider } from '../context/app/insomnia-event-stream-context'; +import { InsomniaTabProvider } from '../context/app/insomnia-tab-context'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; import { syncProjects } from './project'; import { useRootLoaderData } from './root'; @@ -613,41 +614,17 @@ const OrganizationRoute = () => { return ( +
-
+
{!user ? : null} -
-
- {workspaceData && isDesign(workspaceData?.activeWorkspace) && ( - - )} -
+
{user ? ( @@ -1095,6 +1072,7 @@ const OrganizationRoute = () => {
+
); }; diff --git a/packages/insomnia/src/ui/routes/project.tsx b/packages/insomnia/src/ui/routes/project.tsx index cbb254086e5..30bcfe27499 100644 --- a/packages/insomnia/src/ui/routes/project.tsx +++ b/packages/insomnia/src/ui/routes/project.tsx @@ -57,7 +57,7 @@ import { LandingPage, SentryMetrics } from '../../common/sentry'; import { descendingNumberSort, sortMethodMap } from '../../common/sorting'; import * as models from '../../models'; import { userSession } from '../../models'; -import type { ApiSpec } from '../../models/api-spec'; +import { type ApiSpec } from '../../models/api-spec'; import { sortProjects } from '../../models/helpers/project'; import type { MockServer } from '../../models/mock-server'; import type { Organization } from '../../models/organization'; @@ -86,6 +86,7 @@ import { GitRepositoryCloneModal } from '../components/modals/git-repository-set import { ImportModal } from '../components/modals/import-modal'; import { MockServerSettingsModal } from '../components/modals/mock-server-settings-modal'; import { EmptyStatePane } from '../components/panes/project-empty-state-pane'; +import { OrganizationTabList } from '../components/tabs/tabList'; import { TimeFromNow } from '../components/time-from-now'; import { useInsomniaEventStreamContext } from '../context/app/insomnia-event-stream-context'; import { useLoaderDeferData } from '../hooks/use-loader-defer-data'; @@ -990,7 +991,7 @@ const ProjectRoute: FC = () => {
-
+