Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs SSR #834

Merged
merged 16 commits into from
Dec 9, 2019
Merged
17 changes: 17 additions & 0 deletions pages/_document.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ export default class Page extends Document {
type="text/css"
href="/static/fonts/fonts.css"
/>
<link
rel="stylesheet"
type="text/css"
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/3.0.1/github-markdown.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/docsearch.min.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/css/perfect-scrollbar.min.css"
/>
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn/docsearch.min.js"
/>
{this.props.styleTags}
</Head>
<body>
Expand Down
312 changes: 122 additions & 190 deletions pages/doc.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
/* global docsearch:readonly */

import React, { Component } from 'react'
import React, { useCallback, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import Error from 'next/error'
import Router from 'next/router'
// components
import Page from '../src/Page'
import { HeadInjector } from '../src/Documentation/HeadInjector'
import Hamburger from '../src/Hamburger'
import SearchForm from '../src/SearchForm'
import SidebarMenu from '../src/Documentation/SidebarMenu/SidebarMenu'
import Loader from '../src/Loader/Loader'
shcheklein marked this conversation as resolved.
Show resolved Hide resolved
import Page404 from '../src/Page404'
import Markdown from '../src/Documentation/Markdown/Markdown'
import RightPanel from '../src/Documentation/RightPanel/RightPanel'
// utils
import fetch from 'isomorphic-fetch'
import kebabCase from 'lodash.kebabcase'
// constants
import { HEADER } from '../src/consts'
shcheklein marked this conversation as resolved.
Show resolved Hide resolved
// sidebar data and helpers
import sidebar, { getItemByPath } from '../src/Documentation/SidebarMenu/helper'
// styles
Expand All @@ -25,216 +24,149 @@ import { media } from '../src/styles'
const ROOT_ELEMENT = 'bodybag'
const SIDEBAR_MENU = 'sidebar-menu'

export default class Documentation extends Component {
constructor() {
super()
this.state = {
currentItem: {},
headings: [],
isLoading: false,
isMenuOpen: false,
isSmoothScrollEnabled: true,
search: false,
markdown: '',
pageNotFound: false
}
const parseHeadings = text => {
const headingRegex = /\n(## \s*)(.*)/g
const matches = []
let match
do {
match = headingRegex.exec(text)
if (match)
matches.push({
text: match[2],
slug: kebabCase(match[2])
})
} while (match)

return matches
}

export default function Documentation({ item, headings, markdown, errorCode }) {
if (errorCode) {
return <Error statusCode={errorCode} />
}

componentDidMount() {
this.loadStateFromURL()
const { source, path, label, next, prev, tutorials } = item

const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isSearchAvaible, setIsSearchAvaible] = useState(false)

const toggleMenu = useCallback(() => setIsMenuOpen(!isMenuOpen), [isMenuOpen])

useEffect(() => {
try {
docsearch
this.setState(
{
search: true
},
() => {
this.initDocsearch()
}
)

setIsSearchAvaible(true)

if (isSearchAvaible) {
docsearch({
apiKey: '755929839e113a981f481601c4f52082',
indexName: 'dvc',
inputSelector: '#doc-search',
debug: false // Set debug to true if you want to inspect the dropdown
})
}
} catch (ReferenceError) {
this.setState({
search: false
})
// nothing there
}
}, [isSearchAvaible])

window.addEventListener('popstate', this.loadStateFromURL)
}
useEffect(() => {
const handleRouteChange = () => {
const rootElement = document.getElementById(ROOT_ELEMENT)
if (rootElement) {
rootElement.scrollTop = 0
}
}

componentWillUnmount() {
window.removeEventListener('popstate', this.loadStateFromURL)
}
Router.events.on('routeChangeComplete', handleRouteChange)

initDocsearch = () => {
docsearch({
apiKey: '755929839e113a981f481601c4f52082',
indexName: 'dvc',
inputSelector: '#doc-search',
debug: false // Set debug to true if you want to inspect the dropdown
})
}
return () => Router.events.off('routeChangeComplete', handleRouteChange)
}, [])

onNavigate = (path, e) => {
if (e && (e.ctrlKey || e.metaKey)) return
const githubLink = `https://github.com/iterative/dvc.org/blob/master${source}`

if (e) e.preventDefault()
return (
<Page stickHeader={true}>
<HeadInjector sectionName={label} />
<Container>
<Backdrop onClick={toggleMenu} visible={isMenuOpen} />

window.history.pushState(null, null, path)
this.loadPath(path)
}
<SideToggle onClick={toggleMenu} isMenuOpen={isMenuOpen}>
<Hamburger />
</SideToggle>

loadStateFromURL = () => this.loadPath(window.location.pathname)

loadPath = path => {
const { currentItem } = this.state
const item = getItemByPath(path)
const isPageChanged = currentItem !== item
const isFirstPage = !currentItem.path

if (!item) {
this.setState({ pageNotFound: true, currentItem: {} })
} else if (!isFirstPage && !isPageChanged) {
this.updateScroll(isPageChanged)
} else {
this.setState({ isLoading: true, headings: [] })
fetch(item.source)
.then(res => {
res.text().then(text => {
this.setState(
{
currentItem: item,
headings: this.parseHeadings(text),
isLoading: false,
isMenuOpen: false,
markdown: text,
pageNotFound: false
},
() => this.updateScroll(!isFirstPage && isPageChanged)
)
})
})
.catch(() => {
window.location.reload()
})
}
}
<Side isOpen={isMenuOpen}>
{isSearchAvaible && (
<SearchArea>
<SearchForm />
</SearchArea>
)}

<SidebarMenu
sidebar={sidebar}
currentPath={path}
id={SIDEBAR_MENU}
onClick={toggleMenu}
/>
</Side>

<Markdown
markdown={markdown}
githubLink={githubLink}
prev={prev}
next={next}
tutorials={tutorials}
/>

<RightPanel
headings={headings}
githubLink={githubLink}
tutorials={tutorials}
/>
</Container>
</Page>
)
}

updateScroll(isPageChanged) {
const { hash } = window.location
Documentation.getInitialProps = async ({ asPath, req }) => {
const item = getItemByPath(asPath)

if (isPageChanged) {
this.setState({ isSmoothScrollEnabled: false }, () => {
this.scrollTop()
this.setState({ isSmoothScrollEnabled: true }, () => {
if (hash) this.scrollToLink(hash)
})
})
} else if (hash) {
this.scrollToLink(hash)
if (!item) {
return {
errorCode: 404
}
}

parseHeadings = text => {
const headingRegex = /\n(## \s*)(.*)/g
const matches = []
let match
do {
match = headingRegex.exec(text)
if (match)
matches.push({
text: match[2],
slug: kebabCase(match[2])
})
} while (match)

return matches
}
const host = req ? req.headers['host'] : window.location.host
const protocol = host.indexOf('localhost') > -1 ? 'http:' : 'https:'

scrollToLink = hash => {
const element = document.getElementById(hash.replace(/^#/, ''))
try {
const res = await fetch(`${protocol}//${host}${item.source}`)

if (element) {
const headerHeight = document.getElementById(HEADER).offsetHeight
const elementBoundary = element.getBoundingClientRect()
const rootElement = document.getElementById(ROOT_ELEMENT)
rootElement.scroll({ top: elementBoundary.top - headerHeight })
if (res.status !== 200) {
return {
errorCode: res.status
}
Comment on lines +147 to +150
Copy link
Contributor

@jorgeorpinel jorgeorpinel Dec 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just return a 504 here. Or some other generic status code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe with an error message including the res.status.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not leave actual code there? It can help us debug things. Do you see some case there showing real code can be bad?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this fetch is for the internal document? Not some sort of gateway? In that case I think returning actual code makes sense indeed. Although a generic code with a message explaining what happened (and the source error code) would make ever more sense I think. This is merged and works though, so whatevs.

}
}

scrollTop = () => {
const rootElement = document.getElementById(ROOT_ELEMENT)
if (rootElement) {
rootElement.scrollTop = 0
}
}
const text = await res.text()

toggleMenu = () => {
this.setState(prevState => ({
isMenuOpen: !prevState.isMenuOpen
}))
return {
item: item,
headings: parseHeadings(text),
markdown: text
}
} catch {
window.location.reload()
Comment on lines +160 to +161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this go into an infinite loop? (Blocked by the browser probably.)

Continuation of #834 (review)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If page loads again then yes, but this error usually means that the internet is down and we will just show browser error page there. Maybe there are cases then this error is thrown in other situations, but I don't know about them.

}
}

render() {
const {
currentItem: { source, path, label, tutorials, next, prev },
headings,
markdown,
pageNotFound,
isLoading,
isMenuOpen,
isSmoothScrollEnabled
} = this.state

const githubLink = `https://github.com/iterative/dvc.org/blob/master${source}`

return (
<Page stickHeader={true} enableSmoothScroll={isSmoothScrollEnabled}>
<HeadInjector sectionName={label} />
<Container>
<Backdrop onClick={this.toggleMenu} visible={isMenuOpen} />

<SideToggle onClick={this.toggleMenu} isMenuOpen={isMenuOpen}>
<Hamburger />
</SideToggle>

<Side isOpen={isMenuOpen}>
{this.state.search && (
<SearchArea>
<SearchForm />
</SearchArea>
)}

<SidebarMenu
sidebar={sidebar}
currentPath={path}
onNavigate={this.onNavigate}
id={SIDEBAR_MENU}
/>
</Side>

{isLoading ? (
<Loader />
) : pageNotFound ? (
<Page404 />
) : (
<Markdown
markdown={markdown}
githubLink={githubLink}
tutorials={tutorials}
prev={prev}
next={next}
onNavigate={this.onNavigate}
/>
)}
<RightPanel
headings={headings}
tutorials={tutorials}
githubLink={githubLink}
/>
</Container>
</Page>
)
}
Documentation.propTypes = {
item: PropTypes.object,
headings: PropTypes.array,
markdown: PropTypes.string,
errorCode: PropTypes.bool
}

const Container = styled.div`
Expand Down
Loading