Merge pull request #1029 from Budibase/qol-updates
Qol updates - Import/Export App From File
This commit is contained in:
commit
6b814fdb82
|
@ -0,0 +1 @@
|
|||
docker-compose --env-file hosting.properties pull && ./start.sh
|
|
@ -74,6 +74,7 @@
|
|||
"codemirror": "^5.59.0",
|
||||
"d3-selection": "^1.4.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"downloadjs": "^1.4.7",
|
||||
"fast-sort": "^2.2.0",
|
||||
"lodash": "^4.17.13",
|
||||
"posthog-js": "1.4.5",
|
||||
|
|
|
@ -2,7 +2,26 @@
|
|||
import { TextButton } from "@budibase/bbui"
|
||||
import { Heading } from "@budibase/bbui"
|
||||
import { Spacer } from "@budibase/bbui"
|
||||
import api from "builderStore/api"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import download from "downloadjs"
|
||||
|
||||
export let name, _id
|
||||
|
||||
let appExportLoading = false
|
||||
|
||||
async function exportApp() {
|
||||
appExportLoading = true
|
||||
try {
|
||||
download(`/api/backups/export?appId=${_id}`)
|
||||
notifier.success("App Export Complete.")
|
||||
} catch (err) {
|
||||
notifier.danger("App Export Failed.")
|
||||
} finally {
|
||||
appExportLoading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="apps-card">
|
||||
|
@ -14,6 +33,9 @@
|
|||
{name}
|
||||
→
|
||||
</TextButton>
|
||||
{#if appExportLoading}
|
||||
<Spinner size="10" />
|
||||
{:else}<i class="ri-folder-download-line" on:click={exportApp} />{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -31,7 +53,17 @@
|
|||
.card-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: var(--font-size-l);
|
||||
cursor: pointer;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
color: var(--blue);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,14 +1,51 @@
|
|||
<script>
|
||||
import { Label, Heading, Input } from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
|
||||
const BYTES_IN_MB = 1000000
|
||||
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
|
||||
|
||||
export let validationErrors
|
||||
export let template
|
||||
|
||||
let blurred = { appName: false }
|
||||
let file
|
||||
|
||||
function handleFile(evt) {
|
||||
const fileArray = Array.from(evt.target.files)
|
||||
if (fileArray.some(file => file.size >= FILE_SIZE_LIMIT)) {
|
||||
notifier.danger(
|
||||
`Files cannot exceed ${FILE_SIZE_LIMIT /
|
||||
BYTES_IN_MB}MB. Please try again with smaller files.`
|
||||
)
|
||||
return
|
||||
}
|
||||
file = fileArray[0]
|
||||
template.fileImportPath = file.path
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if template?.fromFile}
|
||||
<h2>Import Your Web App From A File</h2>
|
||||
{:else}
|
||||
<h2>Create your Web App</h2>
|
||||
{/if}
|
||||
<div class="container">
|
||||
{#if template}
|
||||
{#if template?.fromFile}
|
||||
<div class="template">
|
||||
<Label extraSmall grey>Import File</Label>
|
||||
<div class="dropzone">
|
||||
<input
|
||||
id="file-upload"
|
||||
accept=".txt"
|
||||
type="file"
|
||||
on:change={handleFile} />
|
||||
<label for="file-upload" class:uploaded={file}>
|
||||
{#if file}{file.name}{:else}Import{/if}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{:else if template}
|
||||
<div class="template">
|
||||
<Label extraSmall grey>Selected Template</Label>
|
||||
<Heading small>{template.name}</Heading>
|
||||
|
@ -33,4 +70,48 @@
|
|||
/* Fix layout due to LH 0 on heading */
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.uploaded {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-s);
|
||||
color: var(--ink);
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
transition: all 0.2s ease 0s;
|
||||
display: inline-flex;
|
||||
text-rendering: optimizeLegibility;
|
||||
min-width: auto;
|
||||
outline: none;
|
||||
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
|
||||
-webkit-box-align: center;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
background-color: var(--grey-2);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: normal;
|
||||
border: var(--border-transparent);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import AppList from "components/start/AppList.svelte"
|
||||
import { get } from "builderStore/api"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import { Button, Heading, Modal } from "@budibase/bbui"
|
||||
import { Button, Heading, Modal, Spacer } from "@budibase/bbui"
|
||||
import TemplateList from "components/start/TemplateList.svelte"
|
||||
import analytics from "analytics"
|
||||
|
||||
|
@ -43,14 +43,23 @@
|
|||
modal.show()
|
||||
}
|
||||
|
||||
function initiateAppImport() {
|
||||
template = { fromFile: true }
|
||||
modal.show()
|
||||
}
|
||||
|
||||
checkIfKeysAndApps()
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<Heading medium black>Welcome to the Budibase Beta</Heading>
|
||||
<div class="button-group">
|
||||
<Button secondary on:click={initiateAppImport}>Import Web App</Button>
|
||||
<Spacer medium />
|
||||
<Button primary on:click={modal.show}>Create New Web App</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner">
|
||||
<img src="/_builder/assets/orange-landscape.png" alt="rocket" />
|
||||
|
@ -103,4 +112,9 @@
|
|||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -105,10 +105,16 @@ async function createInstance(template) {
|
|||
|
||||
// replicate the template data to the instance DB
|
||||
if (template) {
|
||||
let dbDumpReadStream
|
||||
|
||||
if (template.fileImportPath) {
|
||||
dbDumpReadStream = fs.createReadStream(template.fileImportPath)
|
||||
} else {
|
||||
const templatePath = await downloadTemplate(...template.key.split("/"))
|
||||
const dbDumpReadStream = fs.createReadStream(
|
||||
dbDumpReadStream = fs.createReadStream(
|
||||
join(templatePath, "db", "dump.txt")
|
||||
)
|
||||
}
|
||||
const { ok } = await db.load(dbDumpReadStream)
|
||||
if (!ok) {
|
||||
throw "Error loading database dump from template."
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
const { performDump } = require("../../utilities/templates")
|
||||
const path = require("path")
|
||||
const os = require("os")
|
||||
const fs = require("fs-extra")
|
||||
|
||||
exports.exportAppDump = async function(ctx) {
|
||||
const { appId } = ctx.query
|
||||
|
||||
const backupsDir = path.join(os.homedir(), ".budibase", "backups")
|
||||
fs.ensureDirSync(backupsDir)
|
||||
|
||||
const backupIdentifier = `${appId} Backup: ${new Date()}.txt`
|
||||
|
||||
await performDump({
|
||||
dir: backupsDir,
|
||||
appId,
|
||||
name: backupIdentifier,
|
||||
})
|
||||
|
||||
ctx.status = 200
|
||||
|
||||
const backupFile = path.join(backupsDir, backupIdentifier)
|
||||
|
||||
ctx.attachment(backupIdentifier)
|
||||
ctx.body = fs.createReadStream(backupFile)
|
||||
// ctx.body = {
|
||||
// url: `/api/backups/download/${backupIdentifier}`,
|
||||
// }
|
||||
}
|
||||
|
||||
// exports.downloadAppDump = async function(ctx) {
|
||||
// const fileName = ctx.params.fileName
|
||||
|
||||
// const backupsDir = path.join(os.homedir(), ".budibase", "backups")
|
||||
// fs.ensureDirSync(backupsDir)
|
||||
|
||||
// const backupFile = path.join(backupsDir, fileName)
|
||||
|
||||
// ctx.attachment(fileName)
|
||||
// ctx.body = fs.createReadStream(backupFile)
|
||||
// }
|
|
@ -83,7 +83,7 @@ const controller = {
|
|||
ctx.message = `View ${ctx.params.viewName} saved successfully.`
|
||||
},
|
||||
exportView: async ctx => {
|
||||
const view = ctx.request.body
|
||||
const view = ctx.query.view
|
||||
const format = ctx.query.format
|
||||
|
||||
// Fetch view rows
|
||||
|
@ -102,14 +102,6 @@ const controller = {
|
|||
const filename = `${view.name}.${format}`
|
||||
fs.writeFileSync(join(os.tmpdir(), filename), exportedFile)
|
||||
|
||||
ctx.body = {
|
||||
url: `/api/views/export/download/${filename}`,
|
||||
name: view.name,
|
||||
}
|
||||
},
|
||||
downloadExport: async ctx => {
|
||||
const filename = ctx.params.fileName
|
||||
|
||||
ctx.attachment(filename)
|
||||
ctx.body = fs.createReadStream(join(os.tmpdir(), filename))
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ const zlib = require("zlib")
|
|||
const { budibaseAppsDir } = require("../utilities/budibaseDir")
|
||||
const { isDev } = require("../utilities")
|
||||
const { mainRoutes, authRoutes, staticRoutes } = require("./routes")
|
||||
const pkg = require("../../package.json")
|
||||
|
||||
const router = new Router()
|
||||
const env = require("../environment")
|
||||
|
@ -32,6 +33,7 @@ router
|
|||
await next()
|
||||
})
|
||||
.use("/health", ctx => (ctx.status = 200))
|
||||
.use("/version", ctx => (ctx.body = pkg.version))
|
||||
.use(authenticated)
|
||||
|
||||
// error handling middleware
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/backup")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/security/permissions")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get("/api/backups/export", authorized(BUILDER), controller.exportAppDump)
|
||||
// .get(
|
||||
// "/api/backups/download/:fileName",
|
||||
// authorized(BUILDER),
|
||||
// controller.downloadAppDump
|
||||
// )
|
||||
|
||||
module.exports = router
|
|
@ -21,6 +21,7 @@ const permissionRoutes = require("./permission")
|
|||
const datasourceRoutes = require("./datasource")
|
||||
const queryRoutes = require("./query")
|
||||
const hostingRoutes = require("./hosting")
|
||||
const backupRoutes = require("./backup")
|
||||
|
||||
exports.mainRoutes = [
|
||||
deployRoutes,
|
||||
|
@ -42,6 +43,7 @@ exports.mainRoutes = [
|
|||
datasourceRoutes,
|
||||
queryRoutes,
|
||||
hostingRoutes,
|
||||
backupRoutes,
|
||||
// these need to be handled last as they still use /api/:tableId
|
||||
// this could be breaking as koa may recognise other routes as this
|
||||
tableRoutes,
|
||||
|
|
|
@ -26,10 +26,5 @@ router
|
|||
)
|
||||
.post("/api/views", authorized(BUILDER), usage, viewController.save)
|
||||
.post("/api/views/export", authorized(BUILDER), viewController.exportView)
|
||||
.get(
|
||||
"/api/views/export/download/:fileName",
|
||||
authorized(BUILDER),
|
||||
viewController.downloadExport
|
||||
)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -10,7 +10,6 @@ const streamPipeline = promisify(stream.pipeline)
|
|||
const { budibaseAppsDir } = require("./budibaseDir")
|
||||
const env = require("../environment")
|
||||
const CouchDB = require("../db")
|
||||
const { DocumentTypes } = require("../db/utils")
|
||||
|
||||
const DEFAULT_TEMPLATES_BUCKET =
|
||||
"prod-budi-templates.s3-eu-west-1.amazonaws.com"
|
||||
|
@ -56,6 +55,15 @@ exports.downloadTemplate = async function(type, name) {
|
|||
return dirName
|
||||
}
|
||||
|
||||
async function performDump({ dir, appId, name = "dump.txt" }) {
|
||||
const writeStream = fs.createWriteStream(join(dir, name))
|
||||
// perform couch dump
|
||||
const instanceDb = new CouchDB(appId)
|
||||
await instanceDb.dump(writeStream, {})
|
||||
}
|
||||
|
||||
exports.performDump = performDump
|
||||
|
||||
exports.exportTemplateFromApp = async function({ templateName, appId }) {
|
||||
// Copy frontend files
|
||||
const templatesDir = join(
|
||||
|
@ -67,13 +75,6 @@ exports.exportTemplateFromApp = async function({ templateName, appId }) {
|
|||
"db"
|
||||
)
|
||||
fs.ensureDirSync(templatesDir)
|
||||
const writeStream = fs.createWriteStream(join(templatesDir, "dump.txt"))
|
||||
// perform couch dump
|
||||
const instanceDb = new CouchDB(appId)
|
||||
await instanceDb.dump(writeStream, {
|
||||
filter: doc => {
|
||||
return !doc._id.startsWith(DocumentTypes.USER)
|
||||
},
|
||||
})
|
||||
await performDump({ dir: templatesDir, appId })
|
||||
return templatesDir
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue