UNPKG

react-saasify

Version:

React components for Saasify web clients.

511 lines (453 loc) 13.8 kB
import React, { Component } from 'react' import PropTypes from 'prop-types' import theme from 'lib/theme' import mem from 'mem' import isDeepEqual from 'fast-deep-equal' import copyTextToClipboard from 'copy-text-to-clipboard' import mediumZoom from 'medium-zoom' import mime from 'mime-types' import fileType from 'file-type' import detectCsv from 'detect-csv' import isHtml from 'is-html' import download from 'downloadjs' import { observer, inject } from 'mobx-react' import { Button, Tooltip } from 'lib/antd' import { Link } from 'react-router-dom' import { CodeBlock } from '../CodeBlock' import { ImageZoom } from '../ImageZoom' import { ServiceForm } from '../ServiceForm' import getServiceExamples from 'lib/get-service-examples' import requestService from 'lib/request-service' import { getUpgradeLink } from 'lib/upgrade' import styles from './styles.module.css' @inject('auth') @observer export class LiveServiceDemo extends Component { static propTypes = { deployment: PropTypes.object.isRequired, service: PropTypes.object.isRequired, auth: PropTypes.object.isRequired } state = { selected: 'Playground', copiedTextToClipboard: false, running: null, values: {} } _zoom = mediumZoom({ background: '#fff', margin: 48 }) _onClickPlaygroundTabMem = mem(() => () => this._onClickTab({ label: 'Playground' }) ) _onClickTabMem = mem((i) => () => this._onClickTab(this._example.snippets[i])) componentWillUnmount() { if (this._copyTimeout) { clearTimeout(this._copyTimeout) this._copyTimeout = null } } render() { const { service, auth, deployment } = this.props const { codeBlockOutputFlush } = deployment.saas.theme const { selected, copiedTextToClipboard, running, output, outputUrl, outputContentType, outputError, downloaded, hitRateLimit } = this.state try { this._example = getServiceExamples( service, auth.consumer && auth.consumer.token, { method: service.httpMethod.toUpperCase() } ) } catch (err) { console.warn('error generating service examples', service, err) return null } if (!this._example) { return null } let renderedOutput = null if (outputError) { renderedOutput = ( <div className={theme(styles, 'error')}>{outputError}</div> ) } else if (hitRateLimit) { renderedOutput = ( <div className={theme(styles, 'output__cta')}> <div className={theme(styles, 'output__cta__message')}> You've hit our public rate limit. To keep using the API, please upgrade or try again later. </div> <div className={theme(styles, 'output__cta__button')}> <Link to={getUpgradeLink({ auth, deployment })}> <Button type='primary'>Upgrade</Button> </Link> </div> </div> ) } else if (outputContentType) { if (downloaded) { // TODO: gracefully handle other content-types renderedOutput = ( <div className={theme(styles, 'message')}> The resulting "{outputContentType}" file was downloaded. </div> ) } else if (outputUrl) { if (outputContentType.startsWith('image/')) { renderedOutput = ( <div className={theme(styles, 'img-wrapper')}> <ImageZoom src={outputUrl} alt={this._example.name || 'Example output'} zoom={this._zoom} /> </div> ) } } else if (output) { // TODO: switch to use type-is package here if (outputContentType.startsWith('text/')) { renderedOutput = ( <CodeBlock className={theme(styles, 'code')} language={outputContentType.slice('text/'.length)} value={output} /> ) } else if (outputContentType.startsWith('application/json')) { let language let value if (typeof output === 'string') { language = 'text' value = output } else { language = 'json' value = JSON.stringify(output, null, 2) } renderedOutput = ( <CodeBlock className={theme(styles, 'code')} language={language} value={value} /> ) } else if (outputContentType.startsWith('image/')) { const dataUrl = 'data:' + outputContentType + ';base64,' + output renderedOutput = ( <div className={theme(styles, 'img-wrapper')}> <ImageZoom src={dataUrl} alt={this._example.name || 'Example output'} zoom={this._zoom} /> </div> ) } else { // TODO: gracefully handle other content-types renderedOutput = ( <div className={theme(styles, 'message')}> The resulting "{outputContentType}" file preview is not supported. </div> ) } } } return ( <div className={theme(styles, 'live-service-demo')}> <div className={theme(styles, 'tabs')}> <div className={theme( styles, 'tab', selected === 'Playground' && theme(styles, 'selected-tab') )} onClick={this._onClickPlaygroundTabMem()} > Playground </div> {this._example.snippets.map((l, i) => ( <div className={theme( styles, 'tab', selected === l.label && theme(styles, 'selected-tab') )} key={i} onClick={this._onClickTabMem(i)} > {l.label} </div> ))} </div> <div className={theme(styles, 'tab-content')}> <div className={theme( styles, 'tab-pane', selected === 'Playground' && theme(styles, 'selected-tab-pane') )} > <div className={theme(styles, 'api-playground')}> <ServiceForm onChange={this._handlePlaygroundChange} restrictToFirstExample service={service} /> </div> </div> {this._example.snippets.map((l, i) => ( <div className={theme( styles, 'tab-pane', selected === l.label && theme(styles, 'selected-tab-pane') )} key={i} > <CodeBlock className={theme(styles, 'code')} language={l.language} value={l.code} /> </div> ))} </div> <div className={theme(styles, 'footer')}> <div className={theme(styles, 'footer__service')}> {this._example.description && ( <div className={theme( styles, 'footer__service__example-description' )} > Example - {this._example.description} </div> )} <div className={theme(styles, 'footer__service__path')}> <div className={theme( styles, 'footer__service__badge', theme( styles, `footer__service__badge--${service.httpMethod.toUpperCase()}` ) )} > {service.httpMethod.toUpperCase()} </div> / <div className={theme(styles, 'footer__service__name')}> {service.path.slice(1)} </div> </div> </div> <div className={theme(styles, 'footer__actions')}> {selected !== 'Playground' && ( <div className={theme(styles, 'footer__action')}> <Tooltip placement='top' title={ copiedTextToClipboard ? 'Copied!' : 'Copy to clipboard' } > <Button icon='copy' type='primary' className={theme(styles, 'copy')} onClick={this._onClickCopy} /> </Tooltip> </div> )} <div className={theme(styles, 'footer__action')}> <Button onClick={this._onClickRun} type='secondary' loading={running} > Run example </Button> </div> </div> </div> {renderedOutput && ( <div className={theme( styles, 'output', hitRateLimit && theme(styles, 'output--hit-rate-limit'), codeBlockOutputFlush && theme(styles, 'output--flush') )} > {renderedOutput} </div> )} </div> ) } _onClickTab = (language) => { this.setState({ selected: language.label }) } _onClickCopy = () => { const { selected } = this.state const snippet = this._example.snippets.find((l) => l.label === selected) copyTextToClipboard(snippet.code) this.setState({ copiedTextToClipboard: true }) this._clearCopyTimeout() this._copyTimeout = setTimeout(this._onCopyTimeout, 3000) } _onCopyTimeout = () => { this._clearCopyTimeout() this.setState({ copiedTextToClipboard: false }) } _clearCopyTimeout = () => { if (this._copyTimeout) { clearTimeout(this._copyTimeout) this._copyTimeout = null } } _onClickRun = async () => { const { auth, service } = this.props this.setState({ running: true, output: null, outputUrl: null, outputContentType: null, outputError: null, downloaded: false, hitRateLimit: null }) const data = { ...this._example.input, ...this.state.values } const builtInExample = service.examples.find( (e) => isDeepEqual(e.input, data) && e.outputContentType && (e.outputUrl || e.output) ) let result console.log({ data, builtInExample }) async function handleResult() { if (!result.hitRateLimit && result.outputContentType) { if (result.outputUrl) { if (result.outputContentType.startsWith('image/')) { // TODO: clean this up } else { window.open(result.outputUrl) } } else { if (result.outputContentType.startsWith('application/octet-stream')) { const overrides = await getContentTypeForBuffer(result.output) if (overrides) { // TODO: clean this up result.origOutput = result.output result = { ...result, ...overrides } } } if ( result.outputContentType.startsWith('text/') && !result.outputContentType.startsWith('text/html') && !result.outputContentType.startsWith('text/csv') ) { // TODO: clean this up } else if (result.outputContentType.startsWith('application/json')) { // TODO: clean this up } else if (result.outputContentType.startsWith('image/')) { // TODO: clean this up } else { const blob = new window.Blob([result.origOutput || result.output], { type: result.outputContentType }) const ext = mime.extension(result.outputContentType) const filename = ext ? `example.${ext}` : `example` result.downloaded = true download(blob, filename, result.outputContentType) } } } } if (builtInExample) { result = { output: builtInExample.output, outputUrl: builtInExample.outputUrl, outputContentType: builtInExample.outputContentType } await new Promise((resolve) => { setTimeout(resolve, 750) }) await handleResult() } else { result = await requestService({ auth, service, data }) await handleResult() } this.setState({ ...result, running: false }) } _handlePlaygroundChange = (values) => { this.setState({ values }) } } // TODO: move this stuff into a separate library shared between here and the backend async function getContentTypeForBuffer(buffer) { try { const ft = await fileType.fromBuffer(buffer) if (ft) { return { outputContentType: ft.mime } } const str = buffer.toString('utf8') if (isHtml(str)) { return { outputContentType: 'text/html', output: str } } try { const json = JSON.parse(str) return { outputContentType: 'application/json', output: json } } catch (err) { // purposefully ignore } // TODO: this CSV parser is way too lenient const csv = detectCsv(str) if (csv) { return { outputContentType: 'text/csv', output: str } } } catch (err) { console.warn('getContentTypeForBuffer error', err) } }