Skip to content

Commit

Permalink
Merge pull request #2304 from botpress/ya-version-gui
Browse files Browse the repository at this point in the history
feat(versioning): minimal gui for push/pull archive
  • Loading branch information
allardy committed Aug 29, 2019
2 parents 62e7661 + 39ce94a commit 3abfc4f
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 76 deletions.
4 changes: 4 additions & 0 deletions src/bp/core/routers/admin/versioning.ts
Expand Up @@ -74,6 +74,10 @@ export class VersioningRouter extends CustomRouter {
}
})
)

this.router.get('/bpfs_status', (req, res) => {
res.send({ isAvailable: process.BPFS_STORAGE !== 'disk' })
})
}

extractArchiveFromRequest = async (request, folder) => {
Expand Down
39 changes: 39 additions & 0 deletions src/bp/ui-admin/src/Pages/Components/Downloader.tsx
@@ -0,0 +1,39 @@
import ms from 'ms'
import React, { FC, useEffect, useRef, useState } from 'react'
import api from '~/api'

interface DownloadProps {
url?: string
filename: string
onDownloadCompleted?: () => void
}

export const Downloader: FC<DownloadProps> = props => {
const downloadLink = useRef(null)
const [content, setContent] = useState('')
const [filename, setFilename] = useState('')

const startDownload = async (url: string, filename: string, method: string = 'get') => {
const { data } = await api.getSecured({ timeout: ms('2m') })({
method,
url,
responseType: 'blob'
})

setContent(window.URL.createObjectURL(new Blob([data])))
setFilename(filename)

// @ts-ignore
downloadLink.current!.click()
props.onDownloadCompleted && props.onDownloadCompleted()
}

useEffect(() => {
if (props.url && props.filename) {
// tslint:disable-next-line: no-floating-promises
startDownload(props.url, props.filename)
}
}, [props.url])

return <a ref={downloadLink} href={content} download={filename} />
}
60 changes: 0 additions & 60 deletions src/bp/ui-admin/src/Pages/Server/Versioning.tsx

This file was deleted.

34 changes: 34 additions & 0 deletions src/bp/ui-admin/src/Pages/Server/Versioning/DownloadArchive.tsx
@@ -0,0 +1,34 @@
import { Button } from '@blueprintjs/core'
import _ from 'lodash'
import React, { useState } from 'react'
import { toastInfo } from '~/utils/toaster'
import { Downloader } from '~/Pages/Components/Downloader'

const DownloadArchive = () => {
const [downloadUrl, setDownloadUrl] = useState('')
const [isLoading, setIsLoading] = useState(false)

const downloadArchive = () => {
setIsLoading(true)
setDownloadUrl('/admin/versioning/export')
}

const downloadCompleted = () => {
setIsLoading(false)
toastInfo('Archive ready')
}

return (
<div>
<Button
id="btn-downloadArchive"
onClick={downloadArchive}
disabled={isLoading}
text={isLoading ? 'Please wait...' : 'Download archive'}
/>
<Downloader url={downloadUrl} filename={'archive.tgz'} onDownloadCompleted={downloadCompleted} />
</div>
)
}

export default DownloadArchive
182 changes: 182 additions & 0 deletions src/bp/ui-admin/src/Pages/Server/Versioning/UploadArchive.tsx
@@ -0,0 +1,182 @@
import { Button, Classes, Dialog, FileInput, FormGroup, H4, Intent, Switch, TextArea } from '@blueprintjs/core'
import _ from 'lodash'
import React, { Fragment, useState } from 'react'
import api from '~/api'
import { toastFailure, toastSuccess } from '~/utils/toaster'

const _uploadArchive = async (fileContent: any, doUpdate: boolean) => {
const { data } = await api
.getSecured({ timeout: 30000 })
.post(`/admin/versioning/${doUpdate ? 'update' : 'changes'}`, fileContent, {
headers: { 'Content-Type': 'application/tar+gzip' }
})
return data
}

const checkForChanges = (fileContent: any) => _uploadArchive(fileContent, false)
const sendArchive = (fileContent: any) => _uploadArchive(fileContent, true)

const processChanges = (data: any[]): any => {
const changeList = _.flatten(data.map(x => x.changes))
return {
localFiles: _.flatten(data.map(x => x.localFiles)),
blockingChanges: changeList.filter(x => ['del', 'edit'].includes(x.action)),
changeList
}
}

const prettyLine = ({ action, path, add, del }): string => {
if (action === 'add') {
return ` + ${path}`
} else if (action === 'del') {
return ` - ${path}`
} else if (action === 'edit') {
return ` o ${path} (+${add} / -${del})`
}
return ''
}

const UploadArchive = () => {
const [filePath, setFilePath] = useState('')
const [fileContent, setFileContent] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [useForce, setUseForce] = useState(false)
const [isDialogOpen, setDialogOpen] = useState(false)
const [changes, setChanges] = useState('')

const uploadArchive = async () => {
setIsLoading(true)
try {
if (useForce) {
await sendArchive(fileContent)
toastSuccess(`Changes pushed successfully!`)
return
}

const blockingChanges = processChanges(await checkForChanges(fileContent)).blockingChanges
if (blockingChanges.length) {
setChanges(blockingChanges.map(prettyLine).join('\n'))
return
}

await sendArchive(fileContent)
closeDialog()
toastSuccess(`Changes pushed successfully!`)
} catch (err) {
toastFailure(err)
} finally {
setIsLoading(false)
}
}

const readArchive = event => {
const files = (event.target as HTMLInputElement).files
if (!files) {
return
}

const fr = new FileReader()
fr.readAsArrayBuffer(files[0])
fr.onload = loadedEvent => {
setFileContent(_.get(loadedEvent, 'target.result'))
}
setFilePath(files[0].name)
}

const closeDialog = () => {
setFilePath('')
setFileContent('')
setChanges('')
setDialogOpen(false)
}

const renderUpload = () => {
return (
<Fragment>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={<span>Server Archive</span>}
labelFor="input-archive"
helperText={
<span>
Select an archive exported from another server. If there are conflicts, you will be able to review them
before pushing.
</span>
}
>
<FileInput text={filePath || 'Choose file...'} onChange={readArchive} fill={true} />
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
id="btn-push"
text={isLoading ? 'Please wait...' : 'Push changes'}
disabled={!filePath || !fileContent || isLoading}
onClick={uploadArchive}
intent={Intent.PRIMARY}
style={{ height: 20, marginLeft: 5 }}
/>
</div>
</div>
</Fragment>
)
}

const renderConflict = () => {
return (
<Fragment>
<div className={Classes.DIALOG_BODY}>
<div>
<H4>Conflict warning</H4>
<p>
Remote has changes that are not synced to your environment. Backup your changes and use "pull" to get
those changes on your file system. If you still want to overwrite remote changes, turn on the switch
"Force push my changes" then press the button
</p>
<TextArea value={changes} rows={22} cols={120} />
</div>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Switch
id="chk-useForce"
checked={useForce}
onChange={() => setUseForce(!useForce)}
label="Force push my changes"
style={{ margin: '3px 20px 0 20px' }}
/>
<Button
id="btn-upload"
text={isLoading ? 'Please wait...' : 'Upload'}
disabled={!useForce || isLoading}
onClick={uploadArchive}
intent={Intent.PRIMARY}
style={{ height: 20, marginLeft: 5 }}
/>
</div>
</div>
</div>
</Fragment>
)
}

return (
<Fragment>
<Button id="btn-uploadArchive" text="Upload archive" onClick={() => setDialogOpen(true)} />

<Dialog
isOpen={isDialogOpen}
onClose={closeDialog}
transitionDuration={0}
style={{ width: changes ? 800 : 500 }}
title="Upload Archive"
icon="import"
>
{!changes ? renderUpload() : renderConflict()}
</Dialog>
</Fragment>
)
}
export default UploadArchive

0 comments on commit 3abfc4f

Please sign in to comment.