2020-04-26 20:33:39 +00:00
|
|
|
// @flow
|
|
|
|
import * as React from 'react';
|
2021-02-02 22:23:42 +00:00
|
|
|
import { autoBindMethodsForReact } from 'class-autobind-decorator';
|
2020-04-26 20:33:39 +00:00
|
|
|
import type { WrapperProps } from './wrapper';
|
|
|
|
import PageLayout from './page-layout';
|
2020-06-30 23:54:56 +00:00
|
|
|
import { Breadcrumb, Button, Header, NoticeTable } from 'insomnia-components';
|
2020-04-26 20:33:39 +00:00
|
|
|
import ErrorBoundary from './error-boundary';
|
|
|
|
import SpecEditorSidebar from './spec-editor/spec-editor-sidebar';
|
|
|
|
import CodeEditor from './codemirror/code-editor';
|
|
|
|
import { Spectral } from '@stoplight/spectral';
|
|
|
|
import { showModal } from './modals';
|
|
|
|
import GenerateConfigModal from './modals/generate-config-modal';
|
|
|
|
import SwaggerUI from 'swagger-ui-react';
|
|
|
|
import type { ApiSpec } from '../../models/api-spec';
|
|
|
|
import designerLogo from '../images/insomnia-designer-logo.svg';
|
|
|
|
import previewIcon from '../images/icn-eye.svg';
|
|
|
|
import generateConfigIcon from '../images/icn-gear.svg';
|
|
|
|
import * as models from '../../models/index';
|
|
|
|
import { parseApiSpec } from '../../common/api-specs';
|
|
|
|
import { getConfigGenerators } from '../../plugins';
|
2020-06-30 23:54:56 +00:00
|
|
|
import type { GlobalActivity } from '../../common/constants';
|
2021-02-02 22:23:42 +00:00
|
|
|
import { ACTIVITY_HOME, AUTOBIND_CFG } from '../../common/constants';
|
2020-06-30 23:54:56 +00:00
|
|
|
import ActivityToggle from './activity-toggle';
|
2020-04-26 20:33:39 +00:00
|
|
|
|
|
|
|
const spectral = new Spectral();
|
|
|
|
|
|
|
|
type Props = {|
|
|
|
|
gitSyncDropdown: React.Node,
|
2020-06-30 23:54:56 +00:00
|
|
|
handleActivityChange: (workspaceId: string, activity: GlobalActivity) => Promise<void>,
|
2020-06-03 05:32:36 +00:00
|
|
|
handleUpdateApiSpec: (s: ApiSpec) => Promise<void>,
|
2020-06-30 23:54:56 +00:00
|
|
|
wrapperProps: WrapperProps,
|
2020-04-26 20:33:39 +00:00
|
|
|
|};
|
|
|
|
|
|
|
|
type State = {|
|
|
|
|
previewHidden: boolean,
|
|
|
|
hasConfigPlugins: boolean,
|
|
|
|
lintMessages: Array<{
|
|
|
|
message: string,
|
|
|
|
line: number,
|
|
|
|
type: 'error' | 'warning',
|
|
|
|
}>,
|
|
|
|
|};
|
|
|
|
|
2021-02-02 22:23:42 +00:00
|
|
|
@autoBindMethodsForReact(AUTOBIND_CFG)
|
2020-04-26 20:33:39 +00:00
|
|
|
class WrapperDesign extends React.PureComponent<Props, State> {
|
|
|
|
editor: ?CodeEditor;
|
|
|
|
debounceTimeout: IntervalID;
|
|
|
|
|
|
|
|
constructor(props: Props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
previewHidden: props.wrapperProps.activeWorkspaceMeta.previewHidden || false,
|
|
|
|
lintMessages: [],
|
2020-06-03 05:32:36 +00:00
|
|
|
hasConfigPlugins: false,
|
2020-04-26 20:33:39 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Defining it here instead of in render() so it won't act as a changed prop
|
|
|
|
// when being passed to <CodeEditor> again
|
|
|
|
static lintOptions = {
|
|
|
|
delay: 1000,
|
|
|
|
};
|
|
|
|
|
|
|
|
_setEditorRef(n: ?CodeEditor) {
|
|
|
|
this.editor = n;
|
|
|
|
}
|
|
|
|
|
|
|
|
async _handleGenerateConfig() {
|
|
|
|
const { activeApiSpec } = this.props.wrapperProps;
|
|
|
|
showModal(GenerateConfigModal, { apiSpec: activeApiSpec });
|
|
|
|
}
|
|
|
|
|
|
|
|
async _handleTogglePreview() {
|
|
|
|
await this.setState(
|
|
|
|
prevState => ({ previewHidden: !prevState.previewHidden }),
|
|
|
|
async () => {
|
|
|
|
const workspaceId = this.props.wrapperProps.activeWorkspace._id;
|
|
|
|
const previewHidden = this.state.previewHidden;
|
|
|
|
await models.workspaceMeta.updateByParentId(workspaceId, { previewHidden });
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleOnChange(v: string) {
|
2020-05-12 22:05:29 +00:00
|
|
|
const {
|
|
|
|
wrapperProps: { activeApiSpec },
|
|
|
|
handleUpdateApiSpec,
|
|
|
|
} = this.props;
|
2020-04-26 20:33:39 +00:00
|
|
|
|
|
|
|
// Debounce the update because these specs can get pretty large
|
|
|
|
clearTimeout(this.debounceTimeout);
|
|
|
|
this.debounceTimeout = setTimeout(async () => {
|
|
|
|
await handleUpdateApiSpec({ ...activeApiSpec, contents: v });
|
|
|
|
}, 500);
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleSetSelection(chStart: number, chEnd: number, lineStart: number, lineEnd: number) {
|
|
|
|
const editor = this.editor;
|
|
|
|
|
|
|
|
if (!editor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
Sidebar code mirror interactions (#2433)
* Lifting path clicked up to sidebar root
* Abstracting out path click to story (and app)
* Adding click to scroll, updating sidebar components
* Scroll fix for sidebar
* Cleaning up log
* PR feedback
* PR Feedback, fix schemas & request body rendering
* PR feedback & cleanup
* Prop value checking, selection positioning
* Pulling remote import
* Styled components into insomnia-app, obj destructuring, typing
* Refactoring item path mapping
* Abstracting the mapping of specs for position, bumping down styled components version.
* Directly passing ... rest args initial work
* Pulling logs, removing array concatenation
* Pulling click handler out of render method
* Rolling position mapping into scroll positining method
* Opening up type, could be number/string/array
* Update package-lock.json
* Grabbing height from window, typing API vars, checking path
* Creating scroll method dedicated to design mode
* Moving typing of spec related content to Props def.
* Type checking on sections, invalid section component
2020-08-03 17:57:12 +00:00
|
|
|
editor.scrollToSelection(chStart, chEnd, lineStart, lineEnd);
|
2020-04-26 20:33:39 +00:00
|
|
|
}
|
|
|
|
|
2020-05-12 22:05:29 +00:00
|
|
|
_handleLintClick(notice: {}) {
|
|
|
|
// TODO: Export Notice from insomnia-components and use here, instead of {}
|
2020-04-26 20:33:39 +00:00
|
|
|
const { start, end } = notice._range;
|
|
|
|
this._handleSetSelection(start.character, end.character, start.line, end.line);
|
|
|
|
}
|
|
|
|
|
|
|
|
async _reLint() {
|
|
|
|
const { activeApiSpec } = this.props.wrapperProps;
|
|
|
|
|
|
|
|
// Lint only if spec has content
|
|
|
|
if (activeApiSpec.contents.length !== 0) {
|
|
|
|
const results = await spectral.run(activeApiSpec.contents);
|
|
|
|
this.setState({
|
|
|
|
lintMessages: results.map(r => ({
|
|
|
|
type: r.severity === 0 ? 'error' : 'warning',
|
|
|
|
message: `${r.code} ${r.message}`,
|
|
|
|
line: r.range.start.line,
|
|
|
|
|
|
|
|
// Attach range that will be returned to our click handler
|
|
|
|
_range: r.range,
|
|
|
|
})),
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
lintMessages: [],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 18:29:51 +00:00
|
|
|
_handleBreadcrumb() {
|
2020-04-26 20:33:39 +00:00
|
|
|
this.props.wrapperProps.handleSetActiveActivity(ACTIVITY_HOME);
|
|
|
|
}
|
|
|
|
|
|
|
|
async componentDidMount() {
|
|
|
|
const generateConfigPlugins = await getConfigGenerators();
|
|
|
|
this.setState({ hasConfigPlugins: generateConfigPlugins.length > 0 });
|
|
|
|
|
|
|
|
await this._reLint();
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
|
|
const { activeApiSpec } = this.props.wrapperProps;
|
|
|
|
|
|
|
|
// Re-lint if content changed
|
|
|
|
if (activeApiSpec.contents !== prevProps.wrapperProps.activeApiSpec.contents) {
|
|
|
|
this._reLint();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-19 00:13:24 +00:00
|
|
|
_renderEditor(): React.Node {
|
|
|
|
const { activeApiSpec, settings } = this.props.wrapperProps;
|
|
|
|
const { lintMessages } = this.state;
|
|
|
|
|
|
|
|
return (
|
2021-01-21 20:24:02 +00:00
|
|
|
<div className="column tall theme--pane__body">
|
|
|
|
<div className="tall">
|
|
|
|
<CodeEditor
|
|
|
|
manualPrettify
|
|
|
|
ref={this._setEditorRef}
|
|
|
|
fontSize={settings.editorFontSize}
|
|
|
|
indentSize={settings.editorIndentSize}
|
|
|
|
lineWrapping={settings.lineWrapping}
|
|
|
|
keyMap={settings.editorKeyMap}
|
|
|
|
lintOptions={WrapperDesign.lintOptions}
|
|
|
|
mode="openapi"
|
|
|
|
defaultValue={activeApiSpec.contents}
|
|
|
|
onChange={this._handleOnChange}
|
|
|
|
uniquenessKey={activeApiSpec._id}
|
|
|
|
/>
|
|
|
|
</div>
|
2020-11-19 00:13:24 +00:00
|
|
|
{lintMessages.length > 0 && (
|
|
|
|
<NoticeTable notices={lintMessages} onClick={this._handleLintClick} />
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2020-04-26 20:33:39 +00:00
|
|
|
|
2020-11-19 00:13:24 +00:00
|
|
|
_renderPreview(): React.Node {
|
|
|
|
const { activeApiSpec } = this.props.wrapperProps;
|
|
|
|
const { previewHidden } = this.state;
|
2020-04-26 20:33:39 +00:00
|
|
|
|
2020-11-19 00:13:24 +00:00
|
|
|
if (previewHidden) {
|
|
|
|
return null;
|
|
|
|
}
|
2020-04-26 20:33:39 +00:00
|
|
|
|
|
|
|
let swaggerUiSpec;
|
|
|
|
try {
|
2020-06-30 23:54:56 +00:00
|
|
|
swaggerUiSpec = parseApiSpec(activeApiSpec.contents).contents;
|
|
|
|
} catch (err) {}
|
|
|
|
|
|
|
|
if (!swaggerUiSpec) {
|
2020-04-26 20:33:39 +00:00
|
|
|
swaggerUiSpec = {};
|
|
|
|
}
|
|
|
|
|
2020-11-19 00:13:24 +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>
|
|
|
|
)}>
|
|
|
|
<SwaggerUI
|
|
|
|
spec={swaggerUiSpec}
|
|
|
|
supportedSubmitMethods={[
|
|
|
|
'get',
|
|
|
|
'put',
|
|
|
|
'post',
|
|
|
|
'delete',
|
|
|
|
'options',
|
|
|
|
'head',
|
|
|
|
'patch',
|
|
|
|
'trace',
|
|
|
|
]}
|
|
|
|
/>
|
|
|
|
</ErrorBoundary>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const { gitSyncDropdown, wrapperProps, handleActivityChange } = this.props;
|
|
|
|
|
|
|
|
const { activeApiSpec, activity, activeWorkspace } = wrapperProps;
|
|
|
|
|
|
|
|
const { previewHidden, hasConfigPlugins } = this.state;
|
|
|
|
|
2020-04-26 20:33:39 +00:00
|
|
|
return (
|
|
|
|
<PageLayout
|
|
|
|
wrapperProps={this.props.wrapperProps}
|
|
|
|
renderPageHeader={() => (
|
|
|
|
<Header
|
|
|
|
className="app-header"
|
|
|
|
gridLeft={
|
|
|
|
<React.Fragment>
|
2020-04-28 20:38:31 +00:00
|
|
|
<img src={designerLogo} alt="Insomnia" width="32" height="32" />
|
2020-05-12 22:05:29 +00:00
|
|
|
<Breadcrumb
|
|
|
|
className="breadcrumb"
|
|
|
|
crumbs={['Documents', activeApiSpec.fileName]}
|
|
|
|
onClick={this._handleBreadcrumb}
|
|
|
|
/>
|
2020-04-26 20:33:39 +00:00
|
|
|
</React.Fragment>
|
|
|
|
}
|
|
|
|
gridCenter={
|
2020-06-30 23:54:56 +00:00
|
|
|
<ActivityToggle
|
|
|
|
activity={activity}
|
|
|
|
handleActivityChange={handleActivityChange}
|
|
|
|
workspace={activeWorkspace}
|
2020-04-26 20:33:39 +00:00
|
|
|
/>
|
|
|
|
}
|
|
|
|
gridRight={
|
|
|
|
<React.Fragment>
|
2020-05-02 18:56:39 +00:00
|
|
|
<Button variant="contained" onClick={this._handleTogglePreview}>
|
2020-05-12 22:05:29 +00:00
|
|
|
<img src={previewIcon} alt="Preview" width="15" />
|
|
|
|
{previewHidden ? 'Preview: Off' : 'Preview: On'}
|
2020-04-26 20:33:39 +00:00
|
|
|
</Button>
|
|
|
|
{hasConfigPlugins && (
|
2020-05-12 22:05:29 +00:00
|
|
|
<Button
|
|
|
|
variant="contained"
|
|
|
|
onClick={this._handleGenerateConfig}
|
|
|
|
className="margin-left">
|
|
|
|
<img src={generateConfigIcon} alt="Generate Config" width="15" />
|
|
|
|
Generate Config
|
2020-04-26 20:33:39 +00:00
|
|
|
</Button>
|
|
|
|
)}
|
|
|
|
{gitSyncDropdown}
|
|
|
|
</React.Fragment>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
)}
|
2020-11-19 00:13:24 +00:00
|
|
|
renderPaneOne={this._renderEditor}
|
|
|
|
renderPaneTwo={this._renderPreview}
|
2020-04-26 20:33:39 +00:00
|
|
|
renderPageSidebar={() => (
|
2020-08-05 12:47:28 +00:00
|
|
|
<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>
|
|
|
|
)}>
|
2020-04-26 20:33:39 +00:00
|
|
|
<SpecEditorSidebar
|
|
|
|
apiSpec={activeApiSpec}
|
|
|
|
handleSetSelection={this._handleSetSelection}
|
|
|
|
/>
|
|
|
|
</ErrorBoundary>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default WrapperDesign;
|