2022-01-27 12:48:39 +00:00
|
|
|
import { IRuleResult } from '@stoplight/spectral';
|
|
|
|
import { Button, Notice, NoticeTable } from 'insomnia-components';
|
|
|
|
import React, { createRef, FC, Fragment, ReactNode, RefObject, useCallback, useState } from 'react';
|
|
|
|
import { useAsync, useDebounce } from 'react-use';
|
2022-01-13 14:03:18 +00:00
|
|
|
import styled from 'styled-components';
|
2020-04-26 20:33:39 +00:00
|
|
|
import SwaggerUI from 'swagger-ui-react';
|
2021-07-22 23:04:56 +00:00
|
|
|
|
fixes 'previewHidden' of undefined error (#3409)
* readability improvements and reduced indirection
* adds type for handleShowModifyCookieModal
* correctly types wrapperProps property, thereby fixing bug.
if you add `console.log({ previewHidden, propsOne: this.props.wrapperProps.activeWorkspaceMeta });` to the third line of `_renderPreview()` you'll see that `activeWorkspaceMeta` is indeed, sometimes, `undefined.
Also, there's no reason to use `await` on `this.setState`. I didn't find any more of these in the codebase, I just found this one.
* adds type for swaggerUiSpec
* undoes lifting props to state
almost always, this is done for performance reasons, but I removed it the app is working pretty quick-and-snappy for me without needing to introduced duplicated application state and keep track of it.
I went ahead and measured it before and after this commit (using performance.now):
before = [
1.93500000750646,
1.149999996414408,
0.9499999869149178,
0.9950000094249845,
0.8650000090710819,
1.560000004246831,
1.5699999930802733,
0.8450000023003668,
1.4550000196322799,
1.3299999991431832,
1.3050000125076622,
1.4099999971222132,
1.3099999923724681,
1.3100000214762986,
1.1999999987892807,
1.0099999781232327,
0.830000004498288,
1.2449999921955168,
1.2500000011641532,
1.4349999837577343,
]
after = [
2.9400000057648867,
2.449999999953434,
2.33499999740161,
2.2849999950267375,
1.7700000025797635,
1.8149999959859997,
2.1249999990686774,
1.9150000007357448,
2.074999996693805,
1.9899999897461385,
2.0200000144541264,
2.869999996619299,
2.1450000058393925,
2.33499999740161,
2.130000008037314,
2.119999990100041,
2.144999976735562,
2.130000008037314,
2.380000009201467,
2.8999999922234565,
]
> R.mean(before)
> 1.2480000004870817
> R.mean(after)
> 2.243749999080319
> R.median(before)
> 1.2775000068359077
> R.median(after)
> 2.137499992386438
So basically, considering a 16ms render rate (i.e. 60hz), 1ms saved by lifting props to state makes no difference in application performance.
This is committed separately so that if there's any reason we want to keep the prior implementation, we can just still do so.
2021-05-24 14:14:00 +00:00
|
|
|
import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs';
|
2020-06-30 23:54:56 +00:00
|
|
|
import type { GlobalActivity } from '../../common/constants';
|
2021-06-30 15:11:20 +00:00
|
|
|
import { initializeSpectral, isLintError } from '../../common/spectral';
|
2021-07-22 23:04:56 +00:00
|
|
|
import * as models from '../../models/index';
|
2022-01-13 14:03:18 +00:00
|
|
|
import { superFaint } from '../css/css-in-js';
|
2021-07-22 23:04:56 +00:00
|
|
|
import previewIcon from '../images/icn-eye.svg';
|
2021-09-27 13:47:22 +00:00
|
|
|
import { CodeEditor, UnconnectedCodeEditor } from './codemirror/code-editor';
|
2022-01-13 14:03:18 +00:00
|
|
|
import { DesignEmptyState } from './design-empty-state';
|
2021-09-27 13:47:22 +00:00
|
|
|
import { ErrorBoundary } from './error-boundary';
|
|
|
|
import { PageLayout } from './page-layout';
|
|
|
|
import { SpecEditorSidebar } from './spec-editor/spec-editor-sidebar';
|
|
|
|
import { WorkspacePageHeader } from './workspace-page-header';
|
2021-07-22 23:04:56 +00:00
|
|
|
import type { WrapperProps } from './wrapper';
|
2020-04-26 20:33:39 +00:00
|
|
|
|
2022-01-13 14:03:18 +00:00
|
|
|
const EmptySpaceHelper = styled.div({
|
|
|
|
...superFaint,
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
justifyContent: 'center',
|
|
|
|
padding: '2em',
|
|
|
|
textAlign: 'center',
|
|
|
|
});
|
|
|
|
|
2021-06-30 15:11:20 +00:00
|
|
|
const spectral = initializeSpectral();
|
2020-04-26 20:33:39 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
const RenderPageHeader: FC<Pick<Props,
|
|
|
|
| 'gitSyncDropdown'
|
|
|
|
| 'handleActivityChange'
|
|
|
|
| 'wrapperProps'
|
|
|
|
>> = ({
|
|
|
|
gitSyncDropdown,
|
|
|
|
handleActivityChange,
|
|
|
|
wrapperProps,
|
|
|
|
}) => {
|
|
|
|
const { activeWorkspace, activeWorkspaceMeta } = wrapperProps;
|
|
|
|
const previewHidden = Boolean(activeWorkspaceMeta?.previewHidden);
|
|
|
|
|
|
|
|
const handleTogglePreview = useCallback(async () => {
|
2021-06-30 07:47:17 +00:00
|
|
|
if (!activeWorkspace) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const workspaceId = activeWorkspace._id;
|
fixes 'previewHidden' of undefined error (#3409)
* readability improvements and reduced indirection
* adds type for handleShowModifyCookieModal
* correctly types wrapperProps property, thereby fixing bug.
if you add `console.log({ previewHidden, propsOne: this.props.wrapperProps.activeWorkspaceMeta });` to the third line of `_renderPreview()` you'll see that `activeWorkspaceMeta` is indeed, sometimes, `undefined.
Also, there's no reason to use `await` on `this.setState`. I didn't find any more of these in the codebase, I just found this one.
* adds type for swaggerUiSpec
* undoes lifting props to state
almost always, this is done for performance reasons, but I removed it the app is working pretty quick-and-snappy for me without needing to introduced duplicated application state and keep track of it.
I went ahead and measured it before and after this commit (using performance.now):
before = [
1.93500000750646,
1.149999996414408,
0.9499999869149178,
0.9950000094249845,
0.8650000090710819,
1.560000004246831,
1.5699999930802733,
0.8450000023003668,
1.4550000196322799,
1.3299999991431832,
1.3050000125076622,
1.4099999971222132,
1.3099999923724681,
1.3100000214762986,
1.1999999987892807,
1.0099999781232327,
0.830000004498288,
1.2449999921955168,
1.2500000011641532,
1.4349999837577343,
]
after = [
2.9400000057648867,
2.449999999953434,
2.33499999740161,
2.2849999950267375,
1.7700000025797635,
1.8149999959859997,
2.1249999990686774,
1.9150000007357448,
2.074999996693805,
1.9899999897461385,
2.0200000144541264,
2.869999996619299,
2.1450000058393925,
2.33499999740161,
2.130000008037314,
2.119999990100041,
2.144999976735562,
2.130000008037314,
2.380000009201467,
2.8999999922234565,
]
> R.mean(before)
> 1.2480000004870817
> R.mean(after)
> 2.243749999080319
> R.median(before)
> 1.2775000068359077
> R.median(after)
> 2.137499992386438
So basically, considering a 16ms render rate (i.e. 60hz), 1ms saved by lifting props to state makes no difference in application performance.
This is committed separately so that if there's any reason we want to keep the prior implementation, we can just still do so.
2021-05-24 14:14:00 +00:00
|
|
|
await models.workspaceMeta.updateByParentId(workspaceId, { previewHidden: !previewHidden });
|
2022-01-27 12:48:39 +00:00
|
|
|
}, [activeWorkspace, previewHidden]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<WorkspacePageHeader
|
|
|
|
wrapperProps={wrapperProps}
|
|
|
|
handleActivityChange={handleActivityChange}
|
|
|
|
gridRight={
|
|
|
|
<Fragment>
|
|
|
|
<Button variant="contained" onClick={handleTogglePreview}>
|
|
|
|
<img src={previewIcon} alt="Preview" width="15" />
|
|
|
|
{previewHidden ? 'Preview: Off' : 'Preview: On'}
|
|
|
|
</Button>
|
|
|
|
{gitSyncDropdown}
|
|
|
|
</Fragment>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
interface LintMessage {
|
|
|
|
message: string;
|
|
|
|
line: number;
|
|
|
|
type: 'error' | 'warning';
|
|
|
|
range: IRuleResult['range'];
|
|
|
|
}
|
2021-06-30 07:47:17 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
const RenderEditor: FC<Pick<Props, 'wrapperProps'> & {
|
|
|
|
editor: RefObject<UnconnectedCodeEditor>;
|
|
|
|
}> = ({
|
|
|
|
editor,
|
|
|
|
wrapperProps,
|
|
|
|
}) => {
|
|
|
|
const { activeApiSpec } = wrapperProps;
|
|
|
|
const [forceRefreshCounter, setForceRefreshCounter] = useState(0);
|
|
|
|
const [lintMessages, setLintMessages] = useState<LintMessage[]>([]);
|
|
|
|
const contents = activeApiSpec?.contents ?? '';
|
|
|
|
const [contentsState, setContentsState] = useState(contents);
|
|
|
|
|
|
|
|
const uniquenessKey = `${forceRefreshCounter}::${activeApiSpec?._id}`;
|
|
|
|
|
|
|
|
useDebounce(async () => {
|
2021-06-30 07:47:17 +00:00
|
|
|
if (!activeApiSpec) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
await models.apiSpec.update({ ...activeApiSpec, contents: contentsState });
|
|
|
|
setForceRefreshCounter(forceRefreshCounter => forceRefreshCounter + 1);
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- this is a problem with react-use
|
|
|
|
}, 500, [contentsState]);
|
2020-04-26 20:33:39 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
useAsync(async () => {
|
2020-04-26 20:33:39 +00:00
|
|
|
// Lint only if spec has content
|
2022-01-27 12:48:39 +00:00
|
|
|
if (contents && contents.length !== 0) {
|
|
|
|
const results: LintMessage[] = (await spectral.run(contents))
|
|
|
|
.filter(isLintError)
|
|
|
|
.map(({ severity, code, message, range }) => ({
|
|
|
|
type: severity === 0 ? 'error' : 'warning',
|
|
|
|
message: `${code} ${message}`,
|
|
|
|
line: range.start.line,
|
2020-04-26 20:33:39 +00:00
|
|
|
// Attach range that will be returned to our click handler
|
2022-01-27 12:48:39 +00:00
|
|
|
range,
|
|
|
|
}));
|
|
|
|
setLintMessages(results);
|
2020-04-26 20:33:39 +00:00
|
|
|
} else {
|
2022-01-27 12:48:39 +00:00
|
|
|
setLintMessages([]);
|
2020-04-26 20:33:39 +00:00
|
|
|
}
|
2022-01-27 12:48:39 +00:00
|
|
|
}, [contents]);
|
2020-04-26 20:33:39 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
const handleScrollToSelection = useCallback((notice: Notice) => {
|
|
|
|
if (!editor.current) {
|
|
|
|
return;
|
2020-04-26 20:33:39 +00:00
|
|
|
}
|
2022-01-27 12:48:39 +00:00
|
|
|
// @ts-expect-error Notice is not generic, and thus cannot be provided more data like we are doing elsewhere in this file
|
|
|
|
if (!notice.range) {
|
|
|
|
return;
|
2021-06-30 07:47:17 +00:00
|
|
|
}
|
2022-01-27 12:48:39 +00:00
|
|
|
// @ts-expect-error Notice is not generic, and thus cannot be provided more data like we are doing elsewhere in this file
|
|
|
|
const { start, end } = notice.range;
|
|
|
|
editor.current.scrollToSelection(start.character, end.character, start.line, end.line);
|
|
|
|
}, [editor]);
|
2021-06-30 07:47:17 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
if (!activeApiSpec) {
|
|
|
|
return null;
|
2020-11-19 00:13:24 +00:00
|
|
|
}
|
2020-04-26 20:33:39 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
return (
|
|
|
|
<div className="column tall theme--pane__body">
|
|
|
|
<div className="tall relative overflow-hidden">
|
|
|
|
<CodeEditor
|
|
|
|
manualPrettify
|
|
|
|
ref={editor}
|
|
|
|
lintOptions={{ delay: 1000 }}
|
|
|
|
mode="openapi"
|
|
|
|
defaultValue={contentsState}
|
|
|
|
onChange={setContentsState}
|
|
|
|
uniquenessKey={uniquenessKey}
|
|
|
|
/>
|
|
|
|
<DesignEmptyState
|
|
|
|
onUpdateContents={setContentsState}
|
|
|
|
/>
|
2020-11-19 00:13:24 +00:00
|
|
|
</div>
|
2022-01-27 12:48:39 +00:00
|
|
|
{lintMessages.length > 0 && (
|
|
|
|
<NoticeTable
|
|
|
|
notices={lintMessages}
|
|
|
|
onClick={handleScrollToSelection}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const RenderPreview: FC<Pick<Props, 'wrapperProps'>> = ({ wrapperProps }) => {
|
|
|
|
const { activeApiSpec, activeWorkspaceMeta } = wrapperProps;
|
|
|
|
|
|
|
|
if (!activeApiSpec || activeWorkspaceMeta?.previewHidden) {
|
|
|
|
return null;
|
2020-11-19 00:13:24 +00:00
|
|
|
}
|
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
if (!activeApiSpec.contents) {
|
2021-02-02 23:19:22 +00:00
|
|
|
return (
|
2022-01-27 12:48:39 +00:00
|
|
|
<EmptySpaceHelper>
|
|
|
|
Documentation for your OpenAPI spec will render here
|
|
|
|
</EmptySpaceHelper>
|
2021-02-02 23:19:22 +00:00
|
|
|
);
|
|
|
|
}
|
2020-11-19 00:13:24 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
let swaggerUiSpec: ParsedApiSpec['contents'] | null = null;
|
2021-06-30 07:47:17 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
try {
|
|
|
|
swaggerUiSpec = parseApiSpec(activeApiSpec.contents).contents;
|
|
|
|
} catch (err) {}
|
2021-06-30 07:47:17 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
if (!swaggerUiSpec) {
|
|
|
|
swaggerUiSpec = {};
|
|
|
|
}
|
2022-01-13 14:03:18 +00:00
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
return (
|
|
|
|
<div id="swagger-ui-wrapper">
|
2021-02-02 23:19:22 +00:00
|
|
|
<ErrorBoundary
|
|
|
|
invalidationKey={activeApiSpec.contents}
|
|
|
|
renderError={() => (
|
|
|
|
<div className="text-left margin pad">
|
2022-01-27 12:48:39 +00:00
|
|
|
<h3>An error occurred while trying to render Swagger UI</h3>
|
2021-02-02 23:19:22 +00:00
|
|
|
<p>
|
2022-01-27 12:48:39 +00:00
|
|
|
This preview will automatically refresh, once you have a valid specification that
|
|
|
|
can be previewed.
|
2021-02-02 23:19:22 +00:00
|
|
|
</p>
|
|
|
|
</div>
|
2021-08-07 08:03:56 +00:00
|
|
|
)}
|
|
|
|
>
|
2022-01-27 12:48:39 +00:00
|
|
|
<SwaggerUI
|
|
|
|
spec={swaggerUiSpec}
|
|
|
|
supportedSubmitMethods={[
|
|
|
|
'get',
|
|
|
|
'put',
|
|
|
|
'post',
|
|
|
|
'delete',
|
|
|
|
'options',
|
|
|
|
'head',
|
|
|
|
'patch',
|
|
|
|
'trace',
|
|
|
|
]}
|
|
|
|
/>
|
2021-02-02 23:19:22 +00:00
|
|
|
</ErrorBoundary>
|
2022-01-27 12:48:39 +00:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const RenderPageSidebar: FC<Pick<Props, 'wrapperProps'> & { editor: RefObject<UnconnectedCodeEditor>}> = ({
|
|
|
|
editor,
|
|
|
|
wrapperProps: { activeApiSpec },
|
|
|
|
}) => {
|
|
|
|
const handleScrollToSelection = useCallback((chStart: number, chEnd: number, lineStart: number, lineEnd: number) => {
|
|
|
|
if (!editor.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
editor.current.scrollToSelection(chStart, chEnd, lineStart, lineEnd);
|
|
|
|
}, [editor]);
|
|
|
|
|
|
|
|
if (!activeApiSpec) {
|
|
|
|
return null;
|
2021-02-02 23:19:22 +00:00
|
|
|
}
|
|
|
|
|
2022-01-27 12:48:39 +00:00
|
|
|
if (!activeApiSpec.contents) {
|
2020-04-26 20:33:39 +00:00
|
|
|
return (
|
2022-01-27 12:48:39 +00:00
|
|
|
<EmptySpaceHelper>
|
|
|
|
A spec navigator will render here
|
|
|
|
</EmptySpaceHelper>
|
2020-04-26 20:33:39 +00:00
|
|
|
);
|
|
|
|
}
|
2022-01-27 12:48:39 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<ErrorBoundary
|
|
|
|
invalidationKey={activeApiSpec.contents}
|
|
|
|
renderError={() => (
|
|
|
|
<div className="text-left margin pad">
|
|
|
|
<h4>An error occurred while trying to render your spec's navigation.</h4>
|
|
|
|
<p>
|
|
|
|
This navigation will automatically refresh, once you have a valid specification that
|
|
|
|
can be rendered.
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
<SpecEditorSidebar
|
|
|
|
apiSpec={activeApiSpec}
|
|
|
|
handleSetSelection={handleScrollToSelection}
|
|
|
|
/>
|
|
|
|
</ErrorBoundary>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
gitSyncDropdown: ReactNode;
|
|
|
|
handleActivityChange: (options: {workspaceId?: string; nextActivity: GlobalActivity}) => Promise<void>;
|
|
|
|
wrapperProps: WrapperProps;
|
2020-04-26 20:33:39 +00:00
|
|
|
}
|
2022-01-27 12:48:39 +00:00
|
|
|
|
|
|
|
export const WrapperDesign: FC<Props> = ({
|
|
|
|
gitSyncDropdown,
|
|
|
|
handleActivityChange,
|
|
|
|
wrapperProps,
|
|
|
|
}) => {
|
|
|
|
const editor = createRef<UnconnectedCodeEditor>();
|
|
|
|
|
|
|
|
const renderPageHeader = useCallback(() => (
|
|
|
|
<RenderPageHeader
|
|
|
|
gitSyncDropdown={gitSyncDropdown}
|
|
|
|
handleActivityChange={handleActivityChange}
|
|
|
|
wrapperProps={wrapperProps}
|
|
|
|
/>
|
|
|
|
), [gitSyncDropdown, handleActivityChange, wrapperProps]);
|
|
|
|
|
|
|
|
const renderEditor = useCallback(() => (
|
|
|
|
<RenderEditor
|
|
|
|
editor={editor}
|
|
|
|
wrapperProps={wrapperProps}
|
|
|
|
/>
|
|
|
|
), [editor, wrapperProps]);
|
|
|
|
|
|
|
|
const renderPreview = useCallback(() => (
|
|
|
|
<RenderPreview
|
|
|
|
wrapperProps={wrapperProps}
|
|
|
|
/>
|
|
|
|
), [wrapperProps]);
|
|
|
|
|
|
|
|
const renderPageSidebar = useCallback(() => (
|
|
|
|
<RenderPageSidebar
|
|
|
|
editor={editor}
|
|
|
|
wrapperProps={wrapperProps}
|
|
|
|
/>
|
|
|
|
), [editor, wrapperProps]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<PageLayout
|
|
|
|
wrapperProps={wrapperProps}
|
|
|
|
renderPageHeader={renderPageHeader}
|
|
|
|
renderPaneOne={renderEditor}
|
|
|
|
renderPaneTwo={renderPreview}
|
|
|
|
renderPageSidebar={renderPageSidebar}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|