import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { Button, NoticeTable } from 'insomnia-components'; import React, { Fragment, PureComponent, ReactNode } from 'react'; import SwaggerUI from 'swagger-ui-react'; import { parseApiSpec, ParsedApiSpec } from '../../common/api-specs'; import type { GlobalActivity } from '../../common/constants'; import { ACTIVITY_HOME, AUTOBIND_CFG } from '../../common/constants'; import { initializeSpectral, isLintError } from '../../common/spectral'; import type { ApiSpec } from '../../models/api-spec'; import * as models from '../../models/index'; import previewIcon from '../images/icn-eye.svg'; import { CodeEditor, UnconnectedCodeEditor } from './codemirror/code-editor'; 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 { WrapperProps } from './wrapper'; const spectral = initializeSpectral(); interface Props { gitSyncDropdown: ReactNode; handleActivityChange: (options: {workspaceId?: string; nextActivity: GlobalActivity}) => Promise; handleUpdateApiSpec: (s: ApiSpec) => Promise; wrapperProps: WrapperProps; } interface State { lintMessages: { message: string; line: number; type: 'error' | 'warning'; }[]; } @autoBindMethodsForReact(AUTOBIND_CFG) export class WrapperDesign extends PureComponent { editor: UnconnectedCodeEditor | null = null; debounceTimeout: NodeJS.Timeout | null = null; constructor(props: Props) { super(props); this.state = { lintMessages: [], }; } // Defining it here instead of in render() so it won't act as a changed prop // when being passed to again static lintOptions = { delay: 1000, }; _setEditorRef(n: UnconnectedCodeEditor) { this.editor = n; } async _handleTogglePreview() { const { activeWorkspace } = this.props.wrapperProps; if (!activeWorkspace) { return; } const workspaceId = activeWorkspace._id; const previewHidden = Boolean(this.props.wrapperProps.activeWorkspaceMeta?.previewHidden); await models.workspaceMeta.updateByParentId(workspaceId, { previewHidden: !previewHidden }); } _handleOnChange(v: string) { const { wrapperProps: { activeApiSpec }, handleUpdateApiSpec, } = this.props; if (!activeApiSpec) { return; } // TODO: this seems strange, should the timeout be set and cleared on every change?? // Debounce the update because these specs can get pretty large if (this.debounceTimeout !== null) { 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; } editor.scrollToSelection(chStart, chEnd, lineStart, lineEnd); } _handleLintClick(notice) { // TODO: Export Notice from insomnia-components and use here, instead of {} 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 && activeApiSpec.contents.length !== 0) { const results = (await spectral.run(activeApiSpec.contents)).filter(isLintError); 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: [], }); } } _handleBreadcrumb() { this.props.wrapperProps.handleSetActiveActivity(ACTIVITY_HOME); } async componentDidMount() { 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(); } } _renderEditor() { const { activeApiSpec, settings } = this.props.wrapperProps; const { lintMessages } = this.state; if (!activeApiSpec) { return null; } return (
{lintMessages.length > 0 && ( )}
); } _renderPreview() { const { activeApiSpec, activeWorkspaceMeta } = this.props.wrapperProps; if (!activeApiSpec || activeWorkspaceMeta?.previewHidden) { return null; } let swaggerUiSpec: ParsedApiSpec['contents'] | null = null; try { swaggerUiSpec = parseApiSpec(activeApiSpec.contents).contents; } catch (err) {} if (!swaggerUiSpec) { swaggerUiSpec = {}; } return (
(

An error occurred while trying to render Swagger UI 😢

This preview will automatically refresh, once you have a valid specification that can be previewed.

)} >
); } _renderPageHeader() { const { wrapperProps, gitSyncDropdown, handleActivityChange } = this.props; const previewHidden = Boolean(wrapperProps.activeWorkspaceMeta?.previewHidden); return ( {gitSyncDropdown} } /> ); } _renderPageSidebar() { const { activeApiSpec } = this.props.wrapperProps; if (!activeApiSpec) { return null; } return ( (

An error occurred while trying to render your spec's navigation. 😢

This navigation will automatically refresh, once you have a valid specification that can be rendered.

)} >
); } render() { return ( ); } }