Skip to content

Commit

Permalink
feat: multiple tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
CurryYangxx committed Nov 6, 2024
1 parent c385971 commit ca2d22b
Show file tree
Hide file tree
Showing 13 changed files with 835 additions and 51 deletions.
35 changes: 35 additions & 0 deletions packages/insomnia/src/ui/components/document-tab.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className={`flex w-full h-[40px] items-center ${className} px-1 justify-around`}>
{[
{ id: 'spec', name: 'Spec' },
{ id: 'debug', name: 'Collection' },
{ id: 'test', name: 'Tests' },
].map(item => (
<NavLink
key={item.id}
to={`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${item.id}`}
className={({ isActive, isPending }) =>
`${isActive
? 'text-[--color-font] bg-[--color-surprise]'
: ''
} ${isPending ? 'animate-pulse' : ''} text-center rounded-full px-2`
}
data-testid={`workspace-${item.id}`}
>
{item.name}
</NavLink>
))}
</nav>
);
};
132 changes: 132 additions & 0 deletions packages/insomnia/src/ui/components/tabs/tab.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
'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<string, any> = {
[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 (
<div className={`${WORKSPACE_TAB_UI_MAP[type].bgColor} ${WORKSPACE_TAB_UI_MAP[type].textColor} px-2 flex justify-center items-center h-[20px] w-[20px] rounded-s-sm`}>
<Icon icon={WORKSPACE_TAB_UI_MAP[type].icon} />
</div>
);
}

if (type === TabEnum.Request || type === TabEnum.MockRoute) {
return (
<span className={`w-10 flex-shrink-0 flex text-[0.65rem] rounded-sm border border-solid border-[--hl-sm] items-center justify-center ${REQUEST_TAG_MAP[tab.tag]}`}>{tab.tag}</span>
);
}

if (type === TabEnum.Folder) {
return <Icon icon="folder" />;
}
if (type === TabEnum.Runner) {
return <Icon icon="play" />;
};

if (type === TabEnum.TESTSUITE) {
return <Icon icon="check" />;
}

return null;
};

const handleClose = (id: string) => {
deleteTabById(id);
};

return (
<GridListItem
textValue='tab'
id={tab.id}
className="outline-none"
>
{({ isSelected, isHovered }) => (
<Tooltip message={`${tab.projectName} / ${tab.workspaceName}`} className='h-full'>
<div className={`relative flex items-center h-full px-[20px] flex-nowrap border-solid border-r border-[--hl-sm] hover:text-[--color-font] outline-none max-w-[200px] cursor-pointer ${(!isSelected && !isHovered) && 'opacity-[0.7]'}`}>
{renderTabIcon(tab.type)}
<span className='ml-[8px] text-nowrap overflow-hidden text-ellipsis'>{tab.name}</span>
<div className="bg-[--color-bg] h-[36px] w-[20px] flex justify-center items-center absolute right-0">
{isHovered && (
<Button className='hover:bg-[--hl-md] h-[15px] w-[15px] flex justify-center items-center' onPress={() => handleClose(tab.id)}>
<Icon icon="close" />
</Button>
)}
</div>
{isSelected && <span className='block absolute bottom-[-1px] left-0 right-0 h-[1px] bg-[--color-bg]' />}
</div>
</Tooltip>
)}
</GridListItem>
);
};
68 changes: 68 additions & 0 deletions packages/insomnia/src/ui/components/tabs/tabList.tsx
Original file line number Diff line number Diff line change
@@ -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, string> = {
[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 (
<div className='flex items-center border-b border-solid border-[--hl-sm]'>
<GridList
aria-label="Insomnia Tabs"
onSelectionChange={handleSelectionChange}
selectedKeys={showActiveStatus && activeTabId ? [activeTabId] : []}
disallowEmptySelection
defaultSelectedKeys={['req_737492dce0c3460a8a55762e5d1bbd99']}
selectionMode="single"
selectionBehavior='replace'
className="flex h-[40px]"
items={tabList}
>
{item => <InsomniaTab tab={item} />}
</GridList>
<Icon icon="plus" className='ml-[15px] cursor-pointer' />
</div>
);
};
23 changes: 22 additions & 1 deletion packages/insomnia/src/ui/components/tags/method-tag.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Props> = memo(({ method, override, fullNames }) => {
let methodName = method;
let overrideName = override;
Expand Down
Loading

0 comments on commit ca2d22b

Please sign in to comment.