mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Improve UI for design view (#6476)
* design ui * preview toggle * handle spec errors and fix test * aria labels * disabled items * fix lint test
This commit is contained in:
parent
7867080f26
commit
ea77b6d663
@ -8,7 +8,7 @@ test('Clone from github', async ({ page }) => {
|
||||
await page.getByPlaceholder('MyUser').fill('J');
|
||||
await page.getByPlaceholder('88e7ee63b254e4b0bf047559eafe86ba9dd49507').fill('J');
|
||||
await page.getByTestId('git-repository-settings-modal__sync-btn').click();
|
||||
await page.getByRole('button', { name: 'Toggle Preview' }).click();
|
||||
await page.getByLabel('Toggle preview').click();
|
||||
});
|
||||
test('Sign in with GitHub', async ({ app, page }) => {
|
||||
await page.getByRole('button', { name: 'New Document' }).click();
|
||||
|
@ -16,5 +16,6 @@ test('can render Spectral OpenAPI lint errors', async ({ page }) => {
|
||||
await page.locator('textarea').nth(1).press('Tab');
|
||||
// TODO - fix the locator so we don't rely on `.nth(1)` https://linear.app/insomnia/issue/INS-2255/revisit-codemirror-playwright-selectorfill
|
||||
|
||||
await page.getByLabel('Toggle lint panel').click();
|
||||
await expect(codeEditor).toContainText('oas3-schema Object must have required property "info"');
|
||||
});
|
||||
|
@ -1,94 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { FC, Fragment, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as session from '../../account/session';
|
||||
import { GitHubStarsButton } from './github-stars-button';
|
||||
import { InsomniaAILogo } from './insomnia-icon';
|
||||
const LogoWrapper = styled.div({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
export interface AppHeaderProps {
|
||||
gridCenter?: ReactNode;
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
className?: string;
|
||||
gridLeft?: ReactNode;
|
||||
gridCenter?: ReactNode;
|
||||
gridRight?: ReactNode;
|
||||
}
|
||||
|
||||
const StyledHeader = styled.div({
|
||||
gridArea: 'Header',
|
||||
borderBottom: '1px solid var(--hl-md)',
|
||||
display: 'grid',
|
||||
padding: 'var(--padding-xs) 0',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gridTemplateRows: '1fr',
|
||||
gridTemplateAreas: "'header_left header_center header_right'",
|
||||
'.header_left': {
|
||||
gridArea: 'header_left',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-sm)',
|
||||
},
|
||||
'.header_center': {
|
||||
gridArea: 'header_center',
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
'.header_right': {
|
||||
gridArea: 'header_right',
|
||||
textAlign: 'right',
|
||||
display: 'flex',
|
||||
gap: 'var(--padding-xs)',
|
||||
padding: 'var(--padding-xs)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
|
||||
'&&': {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
});
|
||||
|
||||
const Header: FC<HeaderProps> = ({ className, gridLeft, gridCenter, gridRight }) => (
|
||||
<StyledHeader className={classNames('app-header theme--app-header', className)}>
|
||||
<div className="header_left">{gridLeft}</div>
|
||||
<div className="header_center">{gridCenter}</div>
|
||||
<div className="header_right">{gridRight}</div>
|
||||
</StyledHeader>
|
||||
);
|
||||
|
||||
Header.displayName = 'Header';
|
||||
|
||||
export const AppHeader: FC<AppHeaderProps> = ({
|
||||
gridCenter,
|
||||
gridRight,
|
||||
}) => {
|
||||
return (
|
||||
<Header
|
||||
gridLeft={(
|
||||
<Fragment>
|
||||
<LogoWrapper>
|
||||
<InsomniaAILogo />
|
||||
</LogoWrapper>
|
||||
{!session.isLoggedIn() ? <GitHubStarsButton /> : null}
|
||||
</Fragment>
|
||||
)}
|
||||
gridCenter={gridCenter}
|
||||
gridRight={
|
||||
<Fragment>
|
||||
{gridRight}
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,297 +0,0 @@
|
||||
import React, { PropsWithChildren, ReactNode, useCallback, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { IconEnum, SvgIcon } from './svg-icon';
|
||||
import { Button } from './themed-button';
|
||||
|
||||
export interface Notice {
|
||||
type: 'warning' | 'error' | 'info';
|
||||
line: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TableProps {
|
||||
children: ReactNode;
|
||||
striped?: boolean;
|
||||
outlined?: boolean;
|
||||
compact?: boolean;
|
||||
headings?: ReactNode[];
|
||||
}
|
||||
|
||||
export const Table = styled.table<TableProps>`
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: ${({ compact }) => (compact ? 'var(--padding-xs)' : 'var(--padding-sm)')}
|
||||
${({ compact }) => (compact ? 'var(--padding-sm)' : 'var(--padding-md)')};
|
||||
}
|
||||
|
||||
${({ striped }) =>
|
||||
striped &&
|
||||
`
|
||||
tbody tr:nth-child(odd) {
|
||||
background: var(--hl-xs);
|
||||
}`}
|
||||
|
||||
${({ outlined }) =>
|
||||
outlined &&
|
||||
`
|
||||
& {
|
||||
th {
|
||||
background: var(--hl-xxs);
|
||||
}
|
||||
|
||||
&,
|
||||
td {
|
||||
border: 1px solid var(--hl-sm);
|
||||
}
|
||||
|
||||
tr.table--no-outline-row td {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
& {
|
||||
border-radius: 3px;
|
||||
border-collapse: unset;
|
||||
}
|
||||
|
||||
td {
|
||||
border-left: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}`}
|
||||
`;
|
||||
|
||||
/********/
|
||||
/* <tr> */
|
||||
/********/
|
||||
export const TableRow = styled.tr``;
|
||||
|
||||
/********/
|
||||
/* <td> */
|
||||
/********/
|
||||
export interface TableDataProps {
|
||||
compact?: boolean;
|
||||
align?: 'center' | 'left';
|
||||
}
|
||||
|
||||
export const TableData = styled.td<TableDataProps>`
|
||||
vertical-align: top;
|
||||
padding: 0 var(--padding-md);
|
||||
text-align: ${({ align }) => align || 'left'};
|
||||
`;
|
||||
|
||||
export interface TableHeaderProps {
|
||||
compact?: boolean;
|
||||
align?: 'center' | 'left';
|
||||
}
|
||||
|
||||
export const TableHeader = styled.th<TableHeaderProps>`
|
||||
vertical-align: top;
|
||||
padding: 0 var(--padding-md);
|
||||
text-align: ${({ align }) => align || 'left'};
|
||||
`;
|
||||
|
||||
export const TableHead = styled.thead``;
|
||||
|
||||
export const TableBody = styled.tbody``;
|
||||
|
||||
export interface NoticeTableProps<T extends Notice> {
|
||||
notices: T[];
|
||||
onClick: (notice: T) => void;
|
||||
onVisibilityToggle?: (expanded: boolean) => any;
|
||||
toggleRightPane: () => void;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
|
||||
td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: var(--hl-sm) !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const ScrollWrapperStyled = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-height: 13rem;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const ErrorCount = styled.div`
|
||||
margin-right: var(--padding-md);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const JumpButton = styled.button`
|
||||
outline: 0;
|
||||
border: 0;
|
||||
ebackground: transparent;
|
||||
font-size: var(--font-size-md);
|
||||
margin: 0;
|
||||
display: none;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: -0.5em;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
z-index: 1;
|
||||
|
||||
& svg {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active svg {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
tr:hover & {
|
||||
// To keep icon centered vertically
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = styled.header`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 0;
|
||||
justify-content: space-between;
|
||||
|
||||
border: 1px solid var(--hl-sm);
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
padding-left: var(--padding-md);
|
||||
`;
|
||||
|
||||
const NoticeRow = <T extends Notice>({
|
||||
notice,
|
||||
onClick: propsOnClick,
|
||||
}: PropsWithChildren<{
|
||||
notice: T;
|
||||
onClick: (notice: T) => void;
|
||||
}>) => {
|
||||
const onClick = useCallback(() => {
|
||||
propsOnClick?.(notice);
|
||||
}, [notice, propsOnClick]);
|
||||
|
||||
return (
|
||||
<TableRow key={`${notice.line}:${notice.type}:${notice.message}`}>
|
||||
<TableData align="center">
|
||||
<SvgIcon icon={notice.type} />
|
||||
</TableData>
|
||||
<TableData align="center">
|
||||
{notice.line}
|
||||
<JumpButton onClick={onClick}>
|
||||
<SvgIcon icon={IconEnum.arrowRight} />
|
||||
</JumpButton>
|
||||
</TableData>
|
||||
<TableData align="left">{notice.message}</TableData>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoticeTable = <T extends Notice>({
|
||||
notices,
|
||||
compact,
|
||||
onClick,
|
||||
onVisibilityToggle,
|
||||
toggleRightPane,
|
||||
}: PropsWithChildren<NoticeTableProps<T>>) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const onCollapse = useCallback(() => {
|
||||
setCollapsed(!collapsed);
|
||||
if (onVisibilityToggle) {
|
||||
onVisibilityToggle(!collapsed);
|
||||
}
|
||||
}, [onVisibilityToggle, collapsed]);
|
||||
|
||||
const errors = notices.filter(notice => notice.type === 'error');
|
||||
const warnings = notices.filter(notice => notice.type === 'warning');
|
||||
return (
|
||||
<Wrapper>
|
||||
<Header>
|
||||
<div>
|
||||
{errors.length > 0 && (
|
||||
<ErrorCount>
|
||||
<SvgIcon icon={IconEnum.error} label={errors.length} />
|
||||
</ErrorCount>
|
||||
)}
|
||||
{warnings.length > 0 && (
|
||||
<ErrorCount>
|
||||
<SvgIcon icon={IconEnum.warning} label={warnings.length} />
|
||||
</ErrorCount>
|
||||
)}
|
||||
</div>
|
||||
<Button className='btn btn--compact' onClick={onCollapse}>
|
||||
Toggle Warnings
|
||||
<SvgIcon
|
||||
style={{ marginLeft: 'var(--padding-xs)' }}
|
||||
icon={collapsed ? IconEnum.chevronUp : IconEnum.chevronDown}
|
||||
/>
|
||||
</Button>
|
||||
<Button className='btn btn--compact' onClick={toggleRightPane}>
|
||||
Toggle Preview
|
||||
</Button>
|
||||
</Header>
|
||||
{!collapsed && (
|
||||
<ScrollWrapperStyled>
|
||||
<Table striped compact={compact}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeader align="center">Type</TableHeader>
|
||||
<TableHeader
|
||||
style={{
|
||||
minWidth: '3em',
|
||||
}}
|
||||
align="center"
|
||||
>
|
||||
Line
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
align="left"
|
||||
>
|
||||
Message
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{notices.map(notice => (
|
||||
<NoticeRow
|
||||
key={`${notice.line}${notice.message}`}
|
||||
notice={notice}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollWrapperStyled>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
export { Sidebar, type SidebarProps } from './sidebar';
|
||||
export { SidebarBadge, type SidebarBadgeProps } from './sidebar-badge';
|
||||
export { SidebarFilter, type SidebarFilterProps } from './sidebar-filter';
|
||||
export { SidebarHeader, type SidebarHeaderProps } from './sidebar-header';
|
||||
export { SidebarHeaders, type SidebarHeadersProps } from './sidebar-headers';
|
||||
export { SidebarInfo, type SidebarInfoProps, type SidebarInfoType } from './sidebar-info';
|
||||
export { SidebarInvalidSection, type SidebarInvalidSectionProps } from './sidebar-invalid-section';
|
||||
export { SidebarItem, type SidebarItemProps } from './sidebar-item';
|
||||
export { SidebarParameters, type SidebarParametersProps } from './sidebar-parameters';
|
||||
export { SidebarPaths, type SidebarPathsProps, type SidebarPathsType } from './sidebar-paths';
|
||||
export { SidebarRequests, type SidebarRequestsProps } from './sidebar-requests';
|
||||
export { SidebarResponses, type SidebarResponsesProps } from './sidebar-responses';
|
||||
export { SidebarSchemas, type SidebarSchemasProps } from './sidebar-schemas';
|
||||
export { SidebarSection, type SidebarSectionProps } from './sidebar-section';
|
||||
export { SidebarSecurity, type SidebarSecurityProps } from './sidebar-security';
|
||||
export { SidebarServers, type SidebarServersProps, type SidebarServer } from './sidebar-servers';
|
||||
export { SidebarTextItem, type SidebarTextItemProps } from './sidebar-text-item';
|
@ -1,86 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SidebarBadgeProps {
|
||||
label?: string;
|
||||
onClick?: () => void;
|
||||
method?: 'get' | 'post' | 'delete' | 'parameters' | 'patch' | 'put' | 'options-head' | string;
|
||||
}
|
||||
|
||||
const StyledBadge = styled.span`
|
||||
display: table;
|
||||
border-spacing: var(--padding-xxs) 0;
|
||||
&:first-of-type {
|
||||
padding-left: var(--padding-lg);
|
||||
}
|
||||
margin: 0px !important;
|
||||
span {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
padding: var(--padding-xxs) var(--padding-xs);
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-sm);
|
||||
text-shadow: 1px 1px 0px var(--hl-sm);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&.post {
|
||||
background-color: rgba(var(--color-success-rgb), 0.8);
|
||||
&:hover {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
}
|
||||
&.get {
|
||||
background-color: rgba(var(--color-surprise-rgb), 0.8);
|
||||
&:hover {
|
||||
background-color: var(--color-surprise);
|
||||
}
|
||||
}
|
||||
&.delete {
|
||||
background-color: rgba(var(--color-danger-rgb), 0.8);
|
||||
&:hover {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
&.parameters {
|
||||
display: none;
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
&.options-head,
|
||||
&.custom {
|
||||
background-color: rgba(var(--color-info-rgb), 0.8);
|
||||
&:hover {
|
||||
background-color: var(--color-info);
|
||||
}
|
||||
}
|
||||
&.patch {
|
||||
background-color: rgba(var(--color-notice-rgb), 0.8);
|
||||
&:hover {
|
||||
background-color: var(--color-notice);
|
||||
}
|
||||
}
|
||||
&.put {
|
||||
background-color: rgba(var(--color-warning-rgb), 0.8);
|
||||
&:hover {
|
||||
background-color: var(--color-warning);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SidebarBadge: FunctionComponent<SidebarBadgeProps> = ({
|
||||
onClick = () => {},
|
||||
method = 'post',
|
||||
label = method,
|
||||
}) => {
|
||||
return (
|
||||
<StyledBadge onClick={onClick}>
|
||||
<span className={method}>{label}</span>
|
||||
</StyledBadge>
|
||||
);
|
||||
};
|
@ -1,56 +0,0 @@
|
||||
import React, { createRef, FunctionComponent, useLayoutEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SidebarFilterProps {
|
||||
filter: boolean;
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const StyledFilter = styled.div`
|
||||
padding-left: var(--padding-md);
|
||||
padding-right: var(--padding-md);
|
||||
overflow: hidden;
|
||||
input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--padding-xs);
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--padding-sm);
|
||||
outline-style: none;
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--hl-md);
|
||||
color: var(--color-font);
|
||||
background: transparent;
|
||||
|
||||
:focus::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: var(--color-font);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SidebarFilter: FunctionComponent<SidebarFilterProps> = ({ filter, onChange }) => {
|
||||
const filterField = createRef<HTMLInputElement>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (filterField.current && !filter) {
|
||||
filterField.current.value = '';
|
||||
} else if (filterField.current) {
|
||||
filterField.current.focus();
|
||||
}
|
||||
}, [filter, filterField]);
|
||||
|
||||
return (
|
||||
<StyledFilter
|
||||
style={{
|
||||
height: filter ? '100%' : '0px',
|
||||
}}
|
||||
>
|
||||
<input type="text" placeholder="Filter..." onChange={onChange} ref={filterField} />
|
||||
</StyledFilter>
|
||||
);
|
||||
};
|
@ -1,88 +0,0 @@
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
|
||||
export interface SidebarHeaderProps {
|
||||
headerTitle: string;
|
||||
toggleSection: React.MouseEventHandler<HTMLLIElement>;
|
||||
toggleFilter?: () => void;
|
||||
sectionVisible: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const StyledHeader = styled.li`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--hl-xs);
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: var(--font-size-sm);
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
&:hover {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
h6:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
label {
|
||||
color: red !important;
|
||||
position: absolute;
|
||||
padding-top: var(--padding-xs);
|
||||
}
|
||||
|
||||
& > * {
|
||||
padding: var(--padding-md) var(--padding-md) var(--padding-md) var(--padding-md);
|
||||
font-size: var(--font-size-md);
|
||||
|
||||
svg {
|
||||
margin-left: var(--padding-sm);
|
||||
font-size: var(--font-size-xl);
|
||||
|
||||
&:hover {
|
||||
fill: var(--color-font);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SidebarHeader: FunctionComponent<SidebarHeaderProps> = ({
|
||||
headerTitle,
|
||||
toggleSection,
|
||||
toggleFilter,
|
||||
sectionVisible,
|
||||
children,
|
||||
}) => {
|
||||
const handleFilterClick: React.MouseEventHandler<HTMLSpanElement> | undefined =
|
||||
sectionVisible && toggleFilter // only handle a click if the section is open
|
||||
? event => {
|
||||
event.stopPropagation(); // Prevent a parent from also handling the click
|
||||
|
||||
toggleFilter();
|
||||
}
|
||||
: undefined;
|
||||
return (
|
||||
<StyledHeader onClick={toggleSection}>
|
||||
<h6>{headerTitle}</h6>
|
||||
<div>
|
||||
{children || (
|
||||
<span
|
||||
onClick={handleFilterClick}
|
||||
style={{ opacity: sectionVisible ? 0.6 : 0 }}
|
||||
>
|
||||
<SvgIcon icon={IconEnum.search} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</StyledHeader>
|
||||
);
|
||||
};
|
@ -1,55 +0,0 @@
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export interface SidebarHeadersProps {
|
||||
headers: Record<string, any>;
|
||||
onClick: (section: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarHeaders extends Component<SidebarHeadersProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { headers, onClick } = this.props;
|
||||
|
||||
if (Object.prototype.toString.call(headers) !== '[object Object]') {
|
||||
return <SidebarInvalidSection name={'header'} />;
|
||||
}
|
||||
|
||||
const filteredValues = Object.keys(headers).filter(header =>
|
||||
header.toLowerCase().includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredValues.map(header => (
|
||||
<Fragment key={header}>
|
||||
<SidebarItem onClick={() => onClick('components', 'headers', header)}>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.indentation} />
|
||||
</div>
|
||||
<span>
|
||||
<Tooltip message={headers[header].description} position="right">
|
||||
{header}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</SidebarItem>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="HEADERS" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarTextItem } from './sidebar-text-item';
|
||||
|
||||
export interface SidebarInfoType {
|
||||
title: string;
|
||||
description: string;
|
||||
version: string;
|
||||
license: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SidebarInfoProps {
|
||||
info: SidebarInfoType;
|
||||
childrenVisible: boolean;
|
||||
onClick: (section: string, ...args: string[]) => void;
|
||||
}
|
||||
|
||||
export const SidebarInfo: FunctionComponent<SidebarInfoProps> = ({
|
||||
info: {
|
||||
title,
|
||||
description,
|
||||
version,
|
||||
license,
|
||||
},
|
||||
childrenVisible,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ height: childrenVisible ? '100%' : 0 }}>
|
||||
{title && (
|
||||
<SidebarItem onClick={() => onClick('info', 'title')}>
|
||||
<SidebarTextItem label={'Title:'} headline={title} />
|
||||
</SidebarItem>
|
||||
)}
|
||||
{description && (
|
||||
<SidebarItem onClick={() => onClick('info', 'description')}>
|
||||
<SidebarTextItem label={'Description:'} headline={description} />
|
||||
</SidebarItem>
|
||||
)}
|
||||
{version && (
|
||||
<SidebarItem onClick={() => onClick('info', 'version')}>
|
||||
<SidebarTextItem label={'Version:'} headline={version} />
|
||||
</SidebarItem>
|
||||
)}
|
||||
{license && license.name && (
|
||||
<SidebarItem onClick={() => onClick('info', 'license')}>
|
||||
<SidebarTextItem label={'License:'} headline={license.name} />
|
||||
</SidebarItem>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SidebarInvalidSectionProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const StyledInvalidSection = styled.div`
|
||||
padding: var(--padding-xs) var(--padding-xs) var(--padding-md) var(--padding-md);
|
||||
color: var(--color-warning);
|
||||
`;
|
||||
|
||||
export const SidebarInvalidSection: FunctionComponent<SidebarInvalidSectionProps> = ({ name }) => (
|
||||
<StyledInvalidSection>Error: Invalid {name} specification.</StyledInvalidSection>
|
||||
);
|
@ -1,84 +0,0 @@
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SidebarItemProps {
|
||||
children: ReactNode;
|
||||
gridLayout?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const StyledBlockItem = styled.div`
|
||||
padding: var(--padding-xs) var(--padding-md) var(--padding-xs) 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: var(--font-size-md);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
background-color: var(--hl-xxs);
|
||||
cursor: default;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: var(--padding-md);
|
||||
}
|
||||
span {
|
||||
margin: 0 0 0 var(--padding-xs);
|
||||
}
|
||||
div {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
div:nth-child(1) {
|
||||
padding-left: var(--padding-xs);
|
||||
}
|
||||
div.tooltip {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledGridItem = styled.li`
|
||||
padding: 0 0 0 var(--padding-sm);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
font-size: var(--font-size-md);
|
||||
line-height: var(--font-size-sm);
|
||||
&:first-child {
|
||||
margin-top: var(--padding-xxs);
|
||||
}
|
||||
span {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 4px var(--padding-sm) var(--padding-xs) 0px;
|
||||
margin-left: var(--padding-xs);
|
||||
}
|
||||
a {
|
||||
color: var(--hl-xl);
|
||||
}
|
||||
div:nth-child(1) {
|
||||
text-align: right;
|
||||
svg {
|
||||
padding-left: var(--padding-sm);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--hl-xxs);
|
||||
cursor: default;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: var(--padding-md);
|
||||
}
|
||||
`;
|
||||
|
||||
export const SidebarItem: FunctionComponent<SidebarItemProps> = ({ children, gridLayout, onClick }) => {
|
||||
if (gridLayout) {
|
||||
return <StyledGridItem onClick={onClick}>{children}</StyledGridItem>;
|
||||
} else {
|
||||
return <StyledBlockItem onClick={onClick}>{children}</StyledBlockItem>;
|
||||
}
|
||||
};
|
@ -1,55 +0,0 @@
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export interface SidebarParametersProps {
|
||||
parameters: Record<string, any>;
|
||||
onClick: (section: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarParameters extends Component<SidebarParametersProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { parameters, onClick } = this.props;
|
||||
|
||||
if (Object.prototype.toString.call(parameters) !== '[object Object]') {
|
||||
return <SidebarInvalidSection name={'parameter'} />;
|
||||
}
|
||||
|
||||
const filteredValues = Object.keys(parameters).filter(parameter =>
|
||||
parameter.toLowerCase().includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredValues.map(parameter => (
|
||||
<Fragment key={parameter}>
|
||||
<SidebarItem onClick={() => onClick('components', 'parameters', parameter)}>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.indentation} />
|
||||
</div>
|
||||
<span>
|
||||
<Tooltip message={parameters[parameter].description} position="right">
|
||||
{parameter}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</SidebarItem>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="PARAMETERS" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { SidebarBadge } from './sidebar-badge';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export type SidebarPathsType = Record<string, any> | string;
|
||||
|
||||
export interface SidebarPathsProps {
|
||||
paths: SidebarPathsType;
|
||||
onClick: (section: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
const isNotXDashKey = (key: string) => key.indexOf('x-') !== 0;
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarPaths extends Component<SidebarPathsProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { paths, onClick } = this.props;
|
||||
let pathItems = {};
|
||||
|
||||
if (typeof paths !== 'string') {
|
||||
pathItems = Object.entries(paths || {});
|
||||
}
|
||||
|
||||
if (Object.prototype.toString.call(pathItems) !== '[object Array]') {
|
||||
return <SidebarInvalidSection name={'path'} />;
|
||||
}
|
||||
|
||||
// @ts-expect-error TSCONVERSION
|
||||
const filteredValues = pathItems.filter(pathDetail =>
|
||||
pathDetail[0].toLowerCase().includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{// @ts-expect-error TSCONVERSION
|
||||
filteredValues.map(([route, routeBody]) => (
|
||||
<Fragment key={route}>
|
||||
<SidebarItem gridLayout onClick={() => onClick('paths', route)}>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.indentation} />
|
||||
</div>
|
||||
<span>{route}</span>
|
||||
</SidebarItem>
|
||||
<SidebarItem>
|
||||
{Object.keys(routeBody)
|
||||
.filter(isNotXDashKey)
|
||||
.map(method => (
|
||||
<SidebarBadge
|
||||
key={method}
|
||||
method={method}
|
||||
onClick={() => onClick('paths', route, method)}
|
||||
/>
|
||||
))}
|
||||
</SidebarItem>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="PATHS" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
import { SidebarBadge } from './sidebar-badge';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export interface SidebarRequestsProps {
|
||||
requests: Record<string, any>;
|
||||
onClick: (section: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
const StyledRequestFormat = styled.span`
|
||||
padding-left: var(--padding-sm);
|
||||
`;
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarRequests extends Component<SidebarRequestsProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { requests, onClick } = this.props;
|
||||
|
||||
if (Object.prototype.toString.call(requests) !== '[object Object]') {
|
||||
return <SidebarInvalidSection name={'request'} />;
|
||||
}
|
||||
|
||||
const filteredValues = Object.keys(requests).filter(requestName =>
|
||||
requestName.toLowerCase().includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredValues.map(requestName => {
|
||||
const { description, content } = requests[requestName];
|
||||
return (
|
||||
<Fragment key={requestName}>
|
||||
<SidebarItem
|
||||
gridLayout
|
||||
onClick={() => onClick('components', 'requestBodies', requestName)}
|
||||
>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.folderOpen} />
|
||||
</div>
|
||||
<span>
|
||||
<Tooltip message={description} position="right">
|
||||
{requestName}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</SidebarItem>
|
||||
{Object.keys(content).map(requestFormat => (
|
||||
<Fragment key={requestFormat}>
|
||||
<SidebarItem>
|
||||
<StyledRequestFormat>
|
||||
<SvgIcon icon={IconEnum.indentation} />
|
||||
<span
|
||||
onClick={() =>
|
||||
onClick(
|
||||
'components',
|
||||
'requestBodies',
|
||||
requestName,
|
||||
'content',
|
||||
requestFormat,
|
||||
)
|
||||
}
|
||||
>
|
||||
{requestFormat}
|
||||
</span>
|
||||
</StyledRequestFormat>
|
||||
</SidebarItem>
|
||||
{content[requestFormat].examples && (
|
||||
<SidebarItem>
|
||||
{Object.keys(content[requestFormat].examples).map(requestExample => (
|
||||
<SidebarBadge
|
||||
key={requestExample}
|
||||
label={requestExample}
|
||||
onClick={() =>
|
||||
onClick(
|
||||
'components',
|
||||
'requestBodies',
|
||||
requestName,
|
||||
'content',
|
||||
requestFormat,
|
||||
'examples',
|
||||
requestExample,
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SidebarItem>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="REQUESTS" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { Tooltip } from '../../tooltip';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export interface SidebarResponsesProps {
|
||||
responses: Record<string, any>;
|
||||
onClick: (section: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarResponses extends Component<SidebarResponsesProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { responses, onClick } = this.props;
|
||||
|
||||
if (Object.prototype.toString.call(responses) !== '[object Object]') {
|
||||
return <SidebarInvalidSection name={'response'} />;
|
||||
}
|
||||
|
||||
const filteredValues = Object.keys(responses).filter(response =>
|
||||
response.toLowerCase().includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredValues.map(response => (
|
||||
<SidebarItem key={response} onClick={() => onClick('components', 'responses', response)}>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.indentation} />
|
||||
</div>
|
||||
<span>
|
||||
<Tooltip message={responses[response].description} position="right">
|
||||
{response}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</SidebarItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="RESPONSES" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export interface SidebarSchemasProps {
|
||||
schemas: Record<string, any>;
|
||||
onClick: (section: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarSchemas extends Component<SidebarSchemasProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { schemas, onClick } = this.props;
|
||||
|
||||
if (Object.prototype.toString.call(schemas) !== '[object Object]') {
|
||||
return <SidebarInvalidSection name={'schema'} />;
|
||||
}
|
||||
|
||||
const filteredValues = Object.keys(schemas).filter(schema =>
|
||||
schema.toLowerCase().includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredValues.map(schema => (
|
||||
<SidebarItem key={schema} onClick={() => onClick('components', 'schemas', schema)}>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.brackets} />
|
||||
</div>
|
||||
<span>{schema}</span>
|
||||
</SidebarItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="SCHEMAS" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import React, { ChangeEvent, FunctionComponent, ReactNode, useCallback, useLayoutEffect, useState } from 'react';
|
||||
import useToggle from 'react-use/lib/useToggle';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { SidebarFilter } from './sidebar-filter';
|
||||
import { SidebarHeader } from './sidebar-header';
|
||||
|
||||
export interface SidebarSectionProps {
|
||||
title: string;
|
||||
renderBody: (filterValue: string) => ReactNode;
|
||||
}
|
||||
|
||||
const StyledSection = styled.ul`
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--hl-md);
|
||||
`;
|
||||
|
||||
const StyledNoResults = styled.div`
|
||||
padding: var(--padding-xs) var(--padding-xs) var(--padding-md) var(--padding-md);
|
||||
color: var(--color-warning);
|
||||
`;
|
||||
|
||||
export const SidebarSection: FunctionComponent<SidebarSectionProps> = ({ title, renderBody }) => {
|
||||
const [bodyVisible, toggleBodyVisible] = useToggle(false);
|
||||
const [filterVisible, toggleFilterVisible] = useToggle(false);
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
|
||||
const handleFilterChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterValue(event.target.value);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
toggleFilterVisible(false);
|
||||
setFilterValue('');
|
||||
}, [bodyVisible, toggleFilterVisible]);
|
||||
|
||||
return (
|
||||
<StyledSection>
|
||||
<SidebarHeader
|
||||
headerTitle={title}
|
||||
sectionVisible={bodyVisible}
|
||||
toggleSection={toggleBodyVisible}
|
||||
toggleFilter={toggleFilterVisible}
|
||||
/>
|
||||
<div style={{ height: bodyVisible ? '100%' : 0 }}>
|
||||
<SidebarFilter filter={filterVisible} onChange={handleFilterChange} />
|
||||
{renderBody(filterValue) || (
|
||||
<StyledNoResults>No results found for "{filterValue}"...</StyledNoResults>
|
||||
)}
|
||||
</div>
|
||||
</StyledSection>
|
||||
);
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export interface SidebarSecurityProps {
|
||||
security: Record<string, any>;
|
||||
onClick: (section: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarSecurity extends Component<SidebarSecurityProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { security, onClick } = this.props;
|
||||
|
||||
if (Object.prototype.toString.call(security) !== '[object Object]') {
|
||||
return <SidebarInvalidSection name={'security'} />;
|
||||
}
|
||||
|
||||
const filteredValues = Object.keys(security).filter(scheme =>
|
||||
scheme.toLowerCase().includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredValues.map(scheme => (
|
||||
<Fragment key={scheme}>
|
||||
<SidebarItem onClick={() => onClick('components', 'securitySchemes', scheme)}>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.key} />
|
||||
</div>
|
||||
<span>{scheme}</span>
|
||||
</SidebarItem>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="SECURITY" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import React, { Component, Fragment, ReactNode } from 'react';
|
||||
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { SidebarInvalidSection } from './sidebar-invalid-section';
|
||||
import { SidebarItem } from './sidebar-item';
|
||||
import { SidebarSection } from './sidebar-section';
|
||||
|
||||
export interface SidebarServer {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface SidebarServersProps {
|
||||
servers: SidebarServer[];
|
||||
onClick: (section: string, path: string | number) => void;
|
||||
}
|
||||
|
||||
// Implemented as a class component because of a caveat with render props
|
||||
// https://reactjs.org/docs/render-props.html#be-careful-when-using-render-props-with-reactpurecomponent
|
||||
export class SidebarServers extends Component<SidebarServersProps> {
|
||||
renderBody = (filter: string): null | ReactNode => {
|
||||
const { servers, onClick } = this.props;
|
||||
|
||||
if (!Array.isArray(servers)) {
|
||||
return <SidebarInvalidSection name={'server'} />;
|
||||
}
|
||||
|
||||
const filteredValues = servers.filter(server =>
|
||||
server.url.includes(filter.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
if (!filteredValues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{filteredValues.map((server, index) => (
|
||||
<Fragment key={server.url}>
|
||||
<SidebarItem onClick={() => onClick('servers', index)}>
|
||||
<div>
|
||||
<SvgIcon icon={IconEnum.indentation} />
|
||||
</div>
|
||||
<span>{server.url}</span>
|
||||
</SidebarItem>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <SidebarSection title="SERVERS" renderBody={this.renderBody} />;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface SidebarTextItemProps {
|
||||
label: string;
|
||||
headline: string;
|
||||
}
|
||||
|
||||
const StyledTextItem = styled.span`
|
||||
display: block;
|
||||
padding-left: var(--padding-sm);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const SidebarTextItem: FunctionComponent<SidebarTextItemProps> = ({ label, headline }) => (
|
||||
<StyledTextItem>
|
||||
<strong>{label}</strong>
|
||||
<span>{headline}</span>
|
||||
</StyledTextItem>
|
||||
);
|
@ -1,258 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import useToggle from 'react-use/lib/useToggle';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../../base/dropdown';
|
||||
import { IconEnum, SvgIcon } from '../../svg-icon';
|
||||
import { SidebarHeader } from './sidebar-header';
|
||||
import { SidebarHeaders } from './sidebar-headers';
|
||||
import { SidebarInfo, SidebarInfoType } from './sidebar-info';
|
||||
import { SidebarParameters } from './sidebar-parameters';
|
||||
import { SidebarPaths, SidebarPathsType } from './sidebar-paths';
|
||||
import { SidebarRequests } from './sidebar-requests';
|
||||
import { SidebarResponses } from './sidebar-responses';
|
||||
import { SidebarSchemas } from './sidebar-schemas';
|
||||
import { SidebarSecurity } from './sidebar-security';
|
||||
import { SidebarServer, SidebarServers } from './sidebar-servers';
|
||||
|
||||
export interface SidebarProps {
|
||||
className?: string;
|
||||
onClick: (section: string, path: any) => void;
|
||||
jsonData?: {
|
||||
servers?: SidebarServer[];
|
||||
info?: SidebarInfoType;
|
||||
paths?: SidebarPathsType;
|
||||
components?: {
|
||||
requestBodies?: Record<string, any>;
|
||||
responses?: Record<string, any>;
|
||||
parameters?: Record<string, any>;
|
||||
headers?: Record<string, any>;
|
||||
schemas?: Record<string, any>;
|
||||
securitySchemes?: Record<string, any>;
|
||||
};
|
||||
};
|
||||
pathItems?: Record<string, any>[];
|
||||
}
|
||||
|
||||
const StyledSidebar = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg);
|
||||
border: none;
|
||||
color: var(--color-font);
|
||||
position: relative;
|
||||
svg {
|
||||
fill: var(--hl-lg);
|
||||
}
|
||||
ul:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
ul:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSection = styled.ul`
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--hl-md);
|
||||
`;
|
||||
|
||||
const StyledItem = styled.div({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--padding-xs)',
|
||||
|
||||
label: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
});
|
||||
|
||||
interface ItemWrapperProps {
|
||||
checked: boolean;
|
||||
htmlFor: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const ItemWrapper = ({ checked, htmlFor, label }: ItemWrapperProps) => {
|
||||
return (
|
||||
<StyledItem>
|
||||
<input type="checkbox" checked={checked} readOnly />
|
||||
<label htmlFor={htmlFor}>{label}</label>
|
||||
</StyledItem>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownEllipsis = () => <SvgIcon icon={IconEnum.ellipsesCircle} />;
|
||||
|
||||
// Section Expansion & Filtering
|
||||
export const Sidebar: FunctionComponent<SidebarProps> = ({ jsonData, onClick }) => {
|
||||
// Section Visibility
|
||||
const [infoSec, setInfoSec] = useToggle(false);
|
||||
const [pathsVisible, setPathsVisible] = useToggle(true);
|
||||
const [serversVisible, setServersVisible] = useToggle(true);
|
||||
const [requestsVisible, setRequestsVisible] = useToggle(true);
|
||||
const [responsesVisible, setResponsesVisible] = useToggle(true);
|
||||
const [parametersVisible, setParametersVisible] = useToggle(true);
|
||||
const [headersVisible, setHeadersVisible] = useToggle(true);
|
||||
const [schemasVisible, setSchemasVisible] = useToggle(true);
|
||||
const [securityVisible, setSecurityVisible] = useToggle(true);
|
||||
|
||||
// Sections
|
||||
if (jsonData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
servers,
|
||||
info,
|
||||
paths,
|
||||
} = jsonData || {};
|
||||
|
||||
const {
|
||||
requestBodies,
|
||||
responses,
|
||||
parameters,
|
||||
headers,
|
||||
schemas,
|
||||
securitySchemes,
|
||||
} = jsonData?.components || {};
|
||||
|
||||
return (
|
||||
<StyledSidebar className="theme--sidebar">
|
||||
{info && (
|
||||
<StyledSection>
|
||||
<SidebarHeader headerTitle="INFO" sectionVisible={infoSec} toggleSection={setInfoSec}>
|
||||
<Dropdown
|
||||
aria-label='Info Dropdown'
|
||||
closeOnSelect={false}
|
||||
triggerButton={
|
||||
<DropdownButton>
|
||||
<DropdownEllipsis />
|
||||
</DropdownButton>
|
||||
}
|
||||
>
|
||||
<DropdownSection
|
||||
aria-label='Visibility section'
|
||||
title="VISIBILITY"
|
||||
>
|
||||
<DropdownItem aria-label='Servers'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setServersVisible}
|
||||
>
|
||||
<ItemWrapper
|
||||
checked={serversVisible}
|
||||
htmlFor="servers"
|
||||
label="Servers"
|
||||
/>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Paths'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setPathsVisible}
|
||||
>
|
||||
<ItemWrapper
|
||||
checked={pathsVisible}
|
||||
htmlFor="paths"
|
||||
label="Paths"
|
||||
/>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Requests'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setRequestsVisible}
|
||||
>
|
||||
<ItemWrapper
|
||||
checked={requestsVisible}
|
||||
htmlFor="requests"
|
||||
label="Requests"
|
||||
/>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Responses'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setResponsesVisible}
|
||||
>
|
||||
<ItemWrapper
|
||||
checked={responsesVisible}
|
||||
htmlFor="responses"
|
||||
label="Responses"
|
||||
/>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Parameters'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setParametersVisible}
|
||||
>
|
||||
<StyledItem>
|
||||
<input type="checkbox" checked={parametersVisible} readOnly />
|
||||
<label htmlFor="parameters">Parameters</label>
|
||||
</StyledItem>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Headers'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setHeadersVisible}
|
||||
>
|
||||
<ItemWrapper
|
||||
checked={headersVisible}
|
||||
htmlFor="headers"
|
||||
label="Headers"
|
||||
/>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Schemas'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setSchemasVisible}
|
||||
>
|
||||
<ItemWrapper
|
||||
checked={schemasVisible}
|
||||
htmlFor="schemas"
|
||||
label="Schemas"
|
||||
/>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
<DropdownItem aria-label='Security'>
|
||||
<ItemContent
|
||||
stayOpenAfterClick
|
||||
onClick={setSecurityVisible}
|
||||
>
|
||||
<ItemWrapper
|
||||
checked={securityVisible}
|
||||
htmlFor="security"
|
||||
label="Security"
|
||||
/>
|
||||
</ItemContent>
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</Dropdown>
|
||||
</SidebarHeader>
|
||||
<SidebarInfo childrenVisible={infoSec} info={info} onClick={onClick} />
|
||||
</StyledSection>
|
||||
)}
|
||||
{serversVisible && servers && <SidebarServers servers={servers} onClick={onClick} />}
|
||||
{pathsVisible && paths && <SidebarPaths paths={paths} onClick={onClick} />}
|
||||
{requestsVisible && requestBodies && (
|
||||
<SidebarRequests requests={requestBodies} onClick={onClick} />
|
||||
)}
|
||||
{responsesVisible && responses && (
|
||||
<SidebarResponses responses={responses} onClick={onClick} />
|
||||
)}
|
||||
{parametersVisible && parameters && (
|
||||
<SidebarParameters parameters={parameters} onClick={onClick} />
|
||||
)}
|
||||
{headersVisible && headers && <SidebarHeaders headers={headers} onClick={onClick} />}
|
||||
{schemasVisible && schemas && <SidebarSchemas schemas={schemas} onClick={onClick} />}
|
||||
{securityVisible && securitySchemes && (
|
||||
<SidebarSecurity security={securitySchemes} onClick={onClick} />
|
||||
)}
|
||||
</StyledSidebar>
|
||||
);
|
||||
};
|
@ -1,88 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import YAML from 'yaml';
|
||||
import YAMLSourceMap from 'yaml-source-map';
|
||||
|
||||
import type { ApiSpec } from '../../../models/api-spec';
|
||||
import { useAIContext } from '../../context/app/ai-context';
|
||||
import { InsomniaAI } from '../insomnia-ai-icon';
|
||||
import { Button } from '../themed-button';
|
||||
import { Sidebar } from './sidebar';
|
||||
|
||||
interface Props {
|
||||
apiSpec: ApiSpec;
|
||||
handleSetSelection: (chStart: number, chEnd: number, lineStart: number, lineEnd: number) => void;
|
||||
}
|
||||
|
||||
const StyledSpecEditorSidebar = styled.div`
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
export const SpecEditorSidebar: FC<Props> = ({ apiSpec, handleSetSelection }) => {
|
||||
const {
|
||||
generating: loading,
|
||||
generateTestsFromSpec,
|
||||
access,
|
||||
} = useAIContext();
|
||||
const onClick = (...itemPath: any[]): void => {
|
||||
const scrollPosition = { start: { line: 0, col: 0 }, end: { line: 0, col: 200 } };
|
||||
|
||||
try {
|
||||
JSON.parse(apiSpec.contents);
|
||||
// Account for JSON (as string) line number shift
|
||||
scrollPosition.start.line = 1;
|
||||
} catch { }
|
||||
|
||||
const sourceMap = new YAMLSourceMap();
|
||||
const specMap = sourceMap.index(
|
||||
YAML.parseDocument(apiSpec.contents, {
|
||||
keepCstNodes: true,
|
||||
}),
|
||||
);
|
||||
const itemMappedPosition = sourceMap.lookup(itemPath, specMap);
|
||||
if (itemMappedPosition) {
|
||||
scrollPosition.start.line += itemMappedPosition.start.line;
|
||||
}
|
||||
const isServersSection = itemPath[0] === 'servers';
|
||||
if (!isServersSection) {
|
||||
scrollPosition.start.line -= 1;
|
||||
}
|
||||
|
||||
scrollPosition.end.line = scrollPosition.start.line;
|
||||
// NOTE: We're subtracting 1 from everything because YAML CST uses
|
||||
// 1-based indexing and we use 0-based.
|
||||
handleSetSelection(scrollPosition.start.col - 1, scrollPosition.end.col - 1, scrollPosition.start.line - 1, scrollPosition.end.line - 1);
|
||||
};
|
||||
|
||||
const specJSON = YAML.parse(apiSpec.contents);
|
||||
|
||||
return (
|
||||
<StyledSpecEditorSidebar>
|
||||
<div>
|
||||
{access.enabled && (
|
||||
<Button
|
||||
variant="text"
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start!important',
|
||||
gap: 'var(--padding-xs)',
|
||||
}}
|
||||
onClick={generateTestsFromSpec}
|
||||
>
|
||||
<InsomniaAI
|
||||
style={{
|
||||
flex: '0 0 20px',
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
Auto-generate Tests For Collection
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Sidebar jsonData={specJSON} onClick={onClick} />
|
||||
</StyledSpecEditorSidebar>
|
||||
);
|
||||
};
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user