2022-01-27 12:48:39 +00:00
import { IRuleResult } from '@stoplight/spectral' ;
import { Button , Notice , NoticeTable } from 'insomnia-components' ;
2022-02-07 19:00:31 +00:00
import React , { createRef , FC , Fragment , ReactNode , RefObject , useCallback , useEffect , useMemo , useState } from 'react' ;
2022-02-02 15:54:05 +00:00
import { useSelector } from 'react-redux' ;
2022-02-07 19:00:31 +00:00
import { useAsync } 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' ;
2022-02-07 19:00:31 +00:00
import { debounce } from '../../common/misc' ;
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' ;
2022-02-02 15:54:05 +00:00
import { selectActiveApiSpec , selectActiveWorkspace , selectActiveWorkspaceMeta } from '../redux/selectors' ;
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' ;
2022-02-02 15:54:05 +00:00
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' ,
} ) ;
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'
>> = ( {
gitSyncDropdown ,
handleActivityChange ,
} ) = > {
2022-02-02 15:54:05 +00:00
const activeWorkspace = useSelector ( selectActiveWorkspace ) ;
const activeWorkspaceMeta = useSelector ( selectActiveWorkspaceMeta ) ;
2022-01-27 12:48:39 +00:00
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
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 >
}
/ >
) ;
} ;
2022-03-09 13:17:14 +00:00
interface LintMessage extends Notice {
2022-01-27 12:48:39 +00:00
range : IRuleResult [ 'range' ] ;
}
2021-06-30 07:47:17 +00:00
2022-02-07 19:00:31 +00:00
const useDesignEmptyState = ( ) = > {
2022-02-02 15:54:05 +00:00
const activeApiSpec = useSelector ( selectActiveApiSpec ) ;
2022-02-07 19:00:31 +00:00
const contents = activeApiSpec ? . contents ;
2022-01-27 12:48:39 +00:00
const [ forceRefreshCounter , setForceRefreshCounter ] = useState ( 0 ) ;
2022-02-07 19:00:31 +00:00
const [ shouldIncrementCounter , setShouldIncrementCounter ] = useState ( false ) ;
2022-01-27 12:48:39 +00:00
2022-02-07 19:00:31 +00:00
useEffect ( ( ) = > {
if ( contents && shouldIncrementCounter ) {
setForceRefreshCounter ( forceRefreshCounter = > forceRefreshCounter + 1 ) ;
setShouldIncrementCounter ( false ) ;
}
} , [ contents , shouldIncrementCounter ] ) ;
2022-01-27 12:48:39 +00:00
2022-02-07 19:00:31 +00:00
const onUpdateContents = useCallback ( ( value : string ) = > {
2021-06-30 07:47:17 +00:00
if ( ! activeApiSpec ) {
return ;
}
2022-02-07 19:00:31 +00:00
const fn = async ( ) = > {
await models . apiSpec . update ( { . . . activeApiSpec , contents : value } ) ;
} ;
fn ( ) ;
// Because we can't await activeApiSpec.contents to have propageted to redux, we flip a toggle to decide if we should do something when redux does eventually change
setShouldIncrementCounter ( true ) ;
} , [ activeApiSpec ] ) ;
const emptyStateNode = contents ? null : (
< DesignEmptyState
onUpdateContents = { onUpdateContents }
/ >
) ;
const uniquenessKey = ` ${ forceRefreshCounter } :: ${ activeApiSpec ? . _id } ` ;
return { uniquenessKey , emptyStateNode } ;
} ;
const RenderEditor : FC < { editor : RefObject < UnconnectedCodeEditor > } > = ( { editor } ) = > {
const activeApiSpec = useSelector ( selectActiveApiSpec ) ;
const [ lintMessages , setLintMessages ] = useState < LintMessage [ ] > ( [ ] ) ;
const contents = activeApiSpec ? . contents ? ? '' ;
const { uniquenessKey , emptyStateNode } = useDesignEmptyState ( ) ;
const onCodeEditorChange = useMemo ( ( ) = > {
const handler = ( contents : string ) = > {
const fn = async ( ) = > {
if ( ! activeApiSpec ) {
return ;
}
await models . apiSpec . update ( { . . . activeApiSpec , contents } ) ;
} ;
fn ( ) ;
} ;
return debounce ( handler , 500 ) ;
} , [ activeApiSpec ] ) ;
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-03-09 13:17:14 +00:00
const handleScrollToSelection = useCallback ( ( notice : LintMessage ) = > {
2022-01-27 12:48:39 +00:00
if ( ! editor . current ) {
return ;
2020-04-26 20:33:39 +00:00
}
2022-01-27 12:48:39 +00:00
if ( ! notice . range ) {
return ;
2021-06-30 07:47:17 +00:00
}
2022-01-27 12:48:39 +00:00
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"
2022-02-07 19:00:31 +00:00
defaultValue = { contents }
onChange = { onCodeEditorChange }
2022-01-27 12:48:39 +00:00
uniquenessKey = { uniquenessKey }
/ >
2022-02-07 19:00:31 +00:00
{ emptyStateNode }
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 >
) ;
} ;
2022-02-02 15:54:05 +00:00
const RenderPreview : FC = ( ) = > {
const activeWorkspaceMeta = useSelector ( selectActiveWorkspaceMeta ) ;
const activeApiSpec = useSelector ( selectActiveApiSpec ) ;
2022-01-27 12:48:39 +00:00
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 >
) ;
} ;
2022-02-02 15:54:05 +00:00
const RenderPageSidebar : FC < { editor : RefObject < UnconnectedCodeEditor > } > = ( { editor } ) = > {
const activeApiSpec = useSelector ( selectActiveApiSpec ) ;
2022-01-27 12:48:39 +00:00
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 ;
2022-02-02 15:54:05 +00:00
handleActivityChange : HandleActivityChange ;
2022-01-27 12:48:39 +00:00
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 }
/ >
2022-02-02 15:54:05 +00:00
) , [ gitSyncDropdown , handleActivityChange ] ) ;
2022-01-27 12:48:39 +00:00
const renderEditor = useCallback ( ( ) = > (
2022-02-02 15:54:05 +00:00
< RenderEditor editor = { editor } / >
) , [ editor ] ) ;
2022-01-27 12:48:39 +00:00
const renderPreview = useCallback ( ( ) = > (
2022-02-02 15:54:05 +00:00
< RenderPreview / >
) , [ ] ) ;
2022-01-27 12:48:39 +00:00
const renderPageSidebar = useCallback ( ( ) = > (
2022-02-02 15:54:05 +00:00
< RenderPageSidebar editor = { editor } / >
) , [ editor ] ) ;
2022-01-27 12:48:39 +00:00
return (
< PageLayout
wrapperProps = { wrapperProps }
renderPageHeader = { renderPageHeader }
renderPaneOne = { renderEditor }
renderPaneTwo = { renderPreview }
renderPageSidebar = { renderPageSidebar }
/ >
) ;
} ;