insomnia/packages/insomnia-app/app/ui/components/wrapper-design.tsx

299 lines
8.7 KiB
TypeScript
Raw Normal View History

import { IRuleResult } from '@stoplight/spectral';
import { Button, Notice, NoticeTable } from 'insomnia-components';
import React, { createRef, FC, Fragment, ReactNode, RefObject, useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
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';
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';
import { selectActiveApiSpec, selectActiveWorkspace, selectActiveWorkspaceMeta } from '../redux/selectors';
import { CodeEditor, UnconnectedCodeEditor } from './codemirror/code-editor';
2022-01-13 14:03:18 +00:00
import { DesignEmptyState } from './design-empty-state';
import { ErrorBoundary } from './error-boundary';
import { PageLayout } from './page-layout';
import { SpecEditorSidebar } from './spec-editor/spec-editor-sidebar';
import { WorkspacePageHeader } from './workspace-page-header';
import type { HandleActivityChange, 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',
});
const spectral = initializeSpectral();
2020-04-26 20:33:39 +00:00
const RenderPageHeader: FC<Pick<Props,
| 'gitSyncDropdown'
| 'handleActivityChange'
>> = ({
gitSyncDropdown,
handleActivityChange,
}) => {
const activeWorkspace = useSelector(selectActiveWorkspace);
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const previewHidden = Boolean(activeWorkspaceMeta?.previewHidden);
const handleTogglePreview = useCallback(async () => {
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 });
}, [activeWorkspace, previewHidden]);
return (
<WorkspacePageHeader
handleActivityChange={handleActivityChange}
gridRight={
<Fragment>
<Button variant="contained" onClick={handleTogglePreview}>
<img src={previewIcon} alt="Preview" width="15" />
&nbsp; {previewHidden ? 'Preview: Off' : 'Preview: On'}
</Button>
{gitSyncDropdown}
</Fragment>
}
/>
);
};
interface LintMessage {
message: string;
line: number;
type: 'error' | 'warning';
range: IRuleResult['range'];
}
const RenderEditor: FC<{ editor: RefObject<UnconnectedCodeEditor> }> = ({ editor }) => {
const activeApiSpec = useSelector(selectActiveApiSpec);
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 () => {
if (!activeApiSpec) {
return;
}
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
useAsync(async () => {
2020-04-26 20:33:39 +00:00
// Lint only if spec has content
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
range,
}));
setLintMessages(results);
2020-04-26 20:33:39 +00:00
} else {
setLintMessages([]);
2020-04-26 20:33:39 +00:00
}
}, [contents]);
2020-04-26 20:33:39 +00:00
const handleScrollToSelection = useCallback((notice: Notice) => {
if (!editor.current) {
return;
2020-04-26 20:33: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;
}
// @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]);
if (!activeApiSpec) {
return null;
}
2020-04-26 20:33: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}
/>
</div>
{lintMessages.length > 0 && (
<NoticeTable
notices={lintMessages}
onClick={handleScrollToSelection}
/>
)}
</div>
);
};
const RenderPreview: FC = () => {
const activeWorkspaceMeta = useSelector(selectActiveWorkspaceMeta);
const activeApiSpec = useSelector(selectActiveApiSpec);
if (!activeApiSpec || activeWorkspaceMeta?.previewHidden) {
return null;
}
if (!activeApiSpec.contents) {
return (
<EmptySpaceHelper>
Documentation for your OpenAPI spec will render here
</EmptySpaceHelper>
);
}
let swaggerUiSpec: ParsedApiSpec['contents'] | null = null;
try {
swaggerUiSpec = parseApiSpec(activeApiSpec.contents).contents;
} catch (err) {}
if (!swaggerUiSpec) {
swaggerUiSpec = {};
}
2022-01-13 14:03:18 +00:00
return (
<div id="swagger-ui-wrapper">
<ErrorBoundary
invalidationKey={activeApiSpec.contents}
renderError={() => (
<div className="text-left margin pad">
<h3>An error occurred while trying to render Swagger UI</h3>
<p>
This preview will automatically refresh, once you have a valid specification that
can be previewed.
</p>
</div>
2021-08-07 08:03:56 +00:00
)}
>
<SwaggerUI
spec={swaggerUiSpec}
supportedSubmitMethods={[
'get',
'put',
'post',
'delete',
'options',
'head',
'patch',
'trace',
]}
/>
</ErrorBoundary>
</div>
);
};
const RenderPageSidebar: FC<{ editor: RefObject<UnconnectedCodeEditor>}> = ({ editor }) => {
const activeApiSpec = useSelector(selectActiveApiSpec);
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;
}
if (!activeApiSpec.contents) {
2020-04-26 20:33:39 +00:00
return (
<EmptySpaceHelper>
A spec navigator will render here
</EmptySpaceHelper>
2020-04-26 20:33: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: HandleActivityChange;
wrapperProps: WrapperProps;
2020-04-26 20:33:39 +00:00
}
export const WrapperDesign: FC<Props> = ({
gitSyncDropdown,
handleActivityChange,
wrapperProps,
}) => {
const editor = createRef<UnconnectedCodeEditor>();
const renderPageHeader = useCallback(() => (
<RenderPageHeader
gitSyncDropdown={gitSyncDropdown}
handleActivityChange={handleActivityChange}
/>
), [gitSyncDropdown, handleActivityChange]);
const renderEditor = useCallback(() => (
<RenderEditor editor={editor} />
), [editor]);
const renderPreview = useCallback(() => (
<RenderPreview />
), []);
const renderPageSidebar = useCallback(() => (
<RenderPageSidebar editor={editor} />
), [editor]);
return (
<PageLayout
wrapperProps={wrapperProps}
renderPageHeader={renderPageHeader}
renderPaneOne={renderEditor}
renderPaneTwo={renderPreview}
renderPageSidebar={renderPageSidebar}
/>
);
};