Skip to content

Commit

Permalink
runner improvements (multiple tabs)-[INS-4779] (#8244)
Browse files Browse the repository at this point in the history
* runner improvements

* runner improvement

* maintain runner state in context

* fix lint

* save file object to context

* add request list to runner context to keep order

* fix ts error

* add eventbus & clear runner context state

* also delete folder runner tab when delete a folder

* clean

* change runner context data structure

* clean up
  • Loading branch information
CurryYangxx committed Jan 6, 2025
1 parent 57edf80 commit cbbc8c2
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 161 deletions.
2 changes: 1 addition & 1 deletion packages/insomnia-inso/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"sourceMap": true,
/* Runs in the DOM NOTE: this is inconsistent with reality */
"lib": [
"ES2020",
"ES2023",
"DOM",
"DOM.Iterable"
],
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia-sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"jsx": "react",
/* If your code runs in the DOM: */
"lib": [
"es2022",
"es2023",
"dom",
"dom.iterable"
],
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/ui/components/document-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const DocumentTab = ({ organizationId, projectId, workspaceId, className
key={item.id}
to={`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${item.id}`}
className={({ isActive, isPending }) => classnames('text-center rounded-full px-2', {
'text-[--color-font] bg-[--color-surprise]': isActive,
'text-[--color-font-surprise] bg-[--color-surprise]': isActive,
'animate-pulse': isPending,
})}
data-testid={`workspace-${item.id}`}
Expand Down
6 changes: 5 additions & 1 deletion packages/insomnia/src/ui/components/tabs/tabList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = ''
closeTabById,
closeAllTabsUnderWorkspace,
closeAllTabsUnderProject,
batchCloseTabs,
updateTabById,
updateProjectName,
updateWorkspaceName,
Expand Down Expand Up @@ -98,11 +99,14 @@ export const OrganizationTabList = ({ showActiveStatus = true, currentPage = ''
if (docType === models.workspace.type) {
// delete all tabs of this workspace
closeAllTabsUnderWorkspace?.(docId);
} else if (docType === models.requestGroup.type) {
// when delete a folder, we need also delete the corresponding folder runner tab(if exists)
batchCloseTabs?.([docId, `runner_${docId}`]);
} else {
// delete tab by id
closeTabById(docId);
}
}, [closeAllTabsUnderProject, closeAllTabsUnderWorkspace, closeTabById]);
}, [batchCloseTabs, closeAllTabsUnderProject, closeAllTabsUnderWorkspace, closeTabById]);

const handleUpdate = useCallback(async (doc: models.BaseModel, patches: Partial<models.BaseModel>[] = []) => {
const patchObj: Record<string, any> = {};
Expand Down
44 changes: 44 additions & 0 deletions packages/insomnia/src/ui/context/app/insomnia-tab-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLocalStorage } from 'react-use';

import type { BaseTab } from '../../components/tabs/tab';
import type { OrganizationTabs } from '../../components/tabs/tabList';
import uiEventBus, { UIEventType } from '../../eventBus';

interface UpdateInsomniaTabParams {
organizationId: string;
Expand All @@ -19,6 +20,7 @@ interface ContextProps {
changeActiveTab: (id: string) => void;
closeAllTabsUnderWorkspace?: (workspaceId: string) => void;
closeAllTabsUnderProject?: (projectId: string) => void;
batchCloseTabs?: (ids: string[]) => void;
updateProjectName?: (projectId: string, name: string) => void;
updateWorkspaceName?: (projectId: string, name: string) => void;
updateTabById?: (tabId: string, patches: Partial<BaseTab>) => void;
Expand Down Expand Up @@ -97,6 +99,7 @@ export const InsomniaTabProvider: FC<PropsWithChildren> = ({ children }) => {
tabList: [],
activeTabId: '',
});
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, [id]);
return;
}

Expand All @@ -114,34 +117,71 @@ export const InsomniaTabProvider: FC<PropsWithChildren> = ({ children }) => {
tabList: newTabList,
activeTabId: currentTabs.activeTabId === id ? newTabList[Math.max(index - 1, 0)]?.id : currentTabs.activeTabId as string,
});
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, [id]);
}, [navigate, organizationId, projectId, updateInsomniaTabs]);

const batchCloseTabs = useCallback((deleteIds: string[]) => {
const currentTabs = appTabsRef?.current?.[organizationId];
if (!currentTabs) {
return;
}

if (currentTabs.tabList.every(tab => deleteIds.includes(tab.id))) {
navigate(`/organization/${organizationId}/project/${projectId}`);
updateInsomniaTabs({
organizationId,
tabList: [],
activeTabId: '',
});
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, 'all');
return;
}

const index = currentTabs.tabList.findIndex(tab => deleteIds.includes(tab.id));
const newTabList = currentTabs.tabList.filter(tab => !deleteIds.includes(tab.id));
if (deleteIds.includes(currentTabs.activeTabId || '')) {
const url = newTabList[Math.max(index - 1, 0)]?.url;
navigate(url);
}

updateInsomniaTabs({
organizationId,
tabList: newTabList,
activeTabId: deleteIds.includes(currentTabs.activeTabId || '') ? newTabList[Math.max(index - 1, 0)]?.id : currentTabs.activeTabId as string,
});
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, deleteIds);
}, [navigate, organizationId, projectId, updateInsomniaTabs]);

const closeAllTabsUnderWorkspace = useCallback((workspaceId: string) => {
const currentTabs = appTabsRef?.current?.[organizationId];
if (!currentTabs) {
return;
}
const closeIds = currentTabs.tabList.filter(tab => tab.workspaceId === workspaceId).map(tab => tab.id);
const newTabList = currentTabs.tabList.filter(tab => tab.workspaceId !== workspaceId);

updateInsomniaTabs({
organizationId,
tabList: newTabList,
activeTabId: '',
});
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds);
}, [organizationId, updateInsomniaTabs]);

const closeAllTabsUnderProject = useCallback((projectId: string) => {
const currentTabs = appTabsRef?.current?.[organizationId];
if (!currentTabs) {
return;
}
const closeIds = currentTabs.tabList.filter(tab => tab.projectId === projectId).map(tab => tab.id);
const newTabList = currentTabs.tabList.filter(tab => tab.projectId !== projectId);

updateInsomniaTabs({
organizationId,
tabList: newTabList,
activeTabId: '',
});
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds);
}, [organizationId, updateInsomniaTabs]);

const closeAllTabs = useCallback(() => {
Expand All @@ -151,6 +191,7 @@ export const InsomniaTabProvider: FC<PropsWithChildren> = ({ children }) => {
tabList: [],
activeTabId: '',
});
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, 'all');
}, [navigate, organizationId, projectId, updateInsomniaTabs]);

const closeOtherTabs = useCallback((id: string) => {
Expand All @@ -171,6 +212,8 @@ export const InsomniaTabProvider: FC<PropsWithChildren> = ({ children }) => {
tabList: [reservedTab],
activeTabId: id,
});
const closeIds = currentTabs.tabList.filter(tab => tab.id !== id).map(tab => tab.id);
uiEventBus.emit(UIEventType.CLOSE_TAB, organizationId, closeIds);
}, [navigate, organizationId, updateInsomniaTabs]);

const updateTabById = useCallback((tabId: string, patches: Partial<BaseTab>) => {
Expand Down Expand Up @@ -324,6 +367,7 @@ export const InsomniaTabProvider: FC<PropsWithChildren> = ({ children }) => {
closeAllTabsUnderProject,
closeAllTabs,
closeOtherTabs,
batchCloseTabs,
addTab,
updateTabById,
changeActiveTab,
Expand Down
99 changes: 99 additions & 0 deletions packages/insomnia/src/ui/context/app/runner-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { createContext, type FC, type PropsWithChildren, useCallback, useContext, useEffect } from 'react';
import type { Selection } from 'react-aria-components';

import type { UploadDataType } from '../../components/modals/upload-runner-data-modal';
import uiEventBus, { UIEventType } from '../../eventBus';
import useStateRef from '../../hooks/use-state-ref';
import type { RequestRow } from '../../routes/runner';

interface RunnerState {
selectedKeys: Selection;
iterationCount: number;
delay: number;
uploadData: UploadDataType[];
advancedConfig: Record<string, boolean>;
file: File | null;
reqList: RequestRow[];
}

interface OrgRunnerStateMap {
[runnerId: string]: Partial<RunnerState>;
}

interface RunnerStateMap {
[orgId: string]: OrgRunnerStateMap;
};
interface ContextProps {
runnerStateMap: RunnerStateMap;
runnerStateRef?: React.MutableRefObject<RunnerStateMap>;
updateRunnerState: (organizationId: string, runnerId: string, patch: Partial<RunnerState>) => void;
}
const RunnerContext = createContext<ContextProps>({
runnerStateMap: {},
updateRunnerState: () => { },
});

export const RunnerProvider: FC<PropsWithChildren> = ({ children }) => {

const [runnerState, setRunnerState, runnerStateRef] = useStateRef<RunnerStateMap>({});

const updateRunnerState = useCallback((organizationId: string, runnerId: string, patch: Partial<RunnerState>) => {
setRunnerState(prevState => {
const newState = {
...prevState,
[organizationId]: {
...prevState[organizationId],
[runnerId]: { ...prevState[organizationId]?.[runnerId], ...patch },
},
};
return newState;
});
}, [setRunnerState]);

const handleTabClose = useCallback((organizationId: string, ids: 'all' | string[]) => {
if (ids === 'all') {
setRunnerState(prevState => {
const newState = { ...prevState };
delete newState[organizationId];
return newState;
});
return;
}

setRunnerState(prevState => {
const newOrgState = { ...prevState?.[organizationId] };
ids.forEach(id => {
// runner tab id starts with 'runner' prefix, but the runnerId in this context doesn't have the prefix, so we need to remove it
if (id.startsWith('runner')) {
const runnerId = id.replace('runner_', '');
delete newOrgState[runnerId];
}
});
return {
...prevState,
[organizationId]: newOrgState,
};
});
}, [setRunnerState]);

useEffect(() => {
uiEventBus.on(UIEventType.CLOSE_TAB, handleTabClose);
return () => {
uiEventBus.off(UIEventType.CLOSE_TAB, handleTabClose);
};
}, [handleTabClose]);

return (
<RunnerContext.Provider
value={{
runnerStateMap: runnerState,
runnerStateRef,
updateRunnerState,
}}
>
{children}
</RunnerContext.Provider>
);
};

export const useRunnerContext = () => useContext(RunnerContext);
37 changes: 37 additions & 0 deletions packages/insomnia/src/ui/eventBus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
type EventHandler = (...args: any[]) => void;

export enum UIEventType {
CLOSE_TAB = 'closeTab',
}
class EventBus {
private events: Record<UIEventType, EventHandler[]> = {
[UIEventType.CLOSE_TAB]: [],
};

// Subscribe to event
on(event: UIEventType, handler: EventHandler): void {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}

// Unsubscribe from event
off(event: UIEventType, handler: EventHandler): void {
if (!this.events[event]) {
return;
}
this.events[event] = this.events[event].filter(h => h !== handler);
}

// emit event
emit(event: UIEventType, ...args: any[]): void {
if (!this.events[event]) {
return;
}
this.events[event].forEach(handler => handler(...args));
}
}

const uiEventBus = new EventBus();
export default uiEventBus;
25 changes: 9 additions & 16 deletions packages/insomnia/src/ui/hooks/use-runner-request-list.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useEffect, useMemo, useRef } from 'react';
import { useRouteLoaderData } from 'react-router-dom';
import { useListData } from 'react-stately';
import { usePrevious } from 'react-use';

import { isRequest, type Request } from '../../models/request';
import { isRequestGroup } from '../../models/request-group';
import { invariant } from '../../utils/invariant';
import { useRunnerContext } from '../context/app/runner-context';
import type { RequestRow } from '../routes/runner';
import type { Child, WorkspaceLoaderData } from '../routes/workspace';

export const useRunnerRequestList = (workspaceId: string, targetFolderId: string) => {
export const useRunnerRequestList = (organizationId: string, targetFolderId: string, runnerId: string) => {
const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const entityMapRef = useRef(new Map<string, Child>());

Expand Down Expand Up @@ -52,24 +51,18 @@ export const useRunnerRequestList = (workspaceId: string, targetFolderId: string
});
}, [collection, targetFolderId]);

const reqList = useListData({
initialItems: requestRows,
});

const previousWorkspaceId = usePrevious(workspaceId);
const previousTargetFolderId = usePrevious(targetFolderId);
const { runnerStateMap, runnerStateRef, updateRunnerState } = useRunnerContext();

useEffect(() => {
if ((previousWorkspaceId && previousWorkspaceId !== workspaceId) || (previousTargetFolderId !== undefined && previousTargetFolderId !== targetFolderId)) {
// reset the list when workspace changes
const keys = reqList.items.map(item => item.id);
reqList.remove(...keys);
reqList.append(...requestRows);
if (!runnerStateRef?.current?.[organizationId]?.[runnerId]) {
updateRunnerState(organizationId, runnerId, {
reqList: requestRows,
});
}
}, [reqList, requestRows, workspaceId, targetFolderId, previousWorkspaceId, previousTargetFolderId]);
}, [organizationId, requestRows, runnerId, runnerStateRef, updateRunnerState]);

return {
reqList,
reqList: runnerStateMap[organizationId]?.[runnerId]?.reqList || [],
requestRows,
entityMap: entityMapRef.current,
};
Expand Down
5 changes: 4 additions & 1 deletion packages/insomnia/src/ui/routes/organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ 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 { RunnerProvider } from '../context/app/runner-context';
import { useOrganizationPermissions } from '../hooks/use-organization-features';
import { syncProjects } from './project';
import { useRootLoaderData } from './root';
Expand Down Expand Up @@ -879,7 +880,9 @@ const OrganizationRoute = () => {
</nav>
</div>}
<div className='[grid-area:Content] overflow-hidden border-b border-[--hl-md]'>
<Outlet />
<RunnerProvider>
<Outlet />
</RunnerProvider>
</div>
<div className="relative [grid-area:Statusbar] flex items-center overflow-hidden">
<TooltipTrigger>
Expand Down
Loading

0 comments on commit cbbc8c2

Please sign in to comment.