Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2304 from botpress/ya-version-gui
feat(versioning): minimal gui for push/pull archive
- Loading branch information
Showing
7 changed files
with
361 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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} /> | ||
} |
This file was deleted.
Oops, something went wrong.
34 changes: 34 additions & 0 deletions
34
src/bp/ui-admin/src/Pages/Server/Versioning/DownloadArchive.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
182
src/bp/ui-admin/src/Pages/Server/Versioning/UploadArchive.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.