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
309 changes: 119 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,146 @@ 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>
)}

updateScroll(isPageChanged) {
const { hash } = window.location
<SidebarMenu sidebar={sidebar} currentPath={path} id={SIDEBAR_MENU} />
</Side>

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

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

if (isPageChanged) {
this.setState({ isSmoothScrollEnabled: false }, () => {
this.scrollTop()
this.setState({ isSmoothScrollEnabled: true }, () => {
if (hash) this.scrollToLink(hash)
})
})
} else if (hash) {
this.scrollToLink(hash)
Documentation.getInitialProps = async ({ asPath, req }) => {
const item = getItemByPath(asPath)

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 (e) {
return {
errorCode: 404
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm. what kind of errors can fetch throw that would get caught here? Can't easily tell from https://github.com/matthew-andrews/isomorphic-fetch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fetch is a promise. It can return either Promise.resolve or Promise.reject. Resolve is the thing that we will receive in the most cases. It have response object and we can check it's status field. But if, for example, your internet connection is lost, the browser will not resolve it, but will instead throw error which we are catching there.

Try to set connection to offline in Network tab of Developers Tools and then try to click on the internal link, it will throw error.

Copy link
Contributor

@jorgeorpinel jorgeorpinel Dec 11, 2019

Choose a reason for hiding this comment

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

Hmmm OK but why 404? I see this is changed though... Will re-ask.

UPDATE: See #834 (review)

}

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
25 changes: 15 additions & 10 deletions src/Diagram/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/* eslint jsx-a11y/anchor-is-valid: off */

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import NextLink from 'next/link'
import styled from 'styled-components'
import {
media,
Expand All @@ -13,16 +16,18 @@ import { Element } from 'react-scroll'
import Slider from 'react-slick'

const LearnMore = ({ href }) => (
<LearnMoreArea href={href}>
<a href={href}>
<span>Learn&nbsp;more</span>
<img
src="/static/img/learn_more_arrow.svg"
width={18}
height={18}
alt=""
/>
</a>
<LearnMoreArea>
<NextLink href={href}>
jorgeorpinel marked this conversation as resolved.
Show resolved Hide resolved
<a>
<span>Learn&nbsp;more</span>
<img
src="/static/img/learn_more_arrow.svg"
width={18}
height={18}
alt=""
/>
</a>
</NextLink>
</LearnMoreArea>
)

Expand Down
Loading