Add client libary update management from inside the builder

This commit is contained in:
Andrew Kingston 2021-07-07 17:07:42 +01:00
parent 948ad092e5
commit a6b5861cbc
7 changed files with 158 additions and 52 deletions

View File

@ -75,6 +75,7 @@ export const getFrontendStore = () => {
appInstance: application.instance, appInstance: application.instance,
clientLibPath, clientLibPath,
previousTopNavPath: {}, previousTopNavPath: {},
version: application.version,
})) }))
await hostingStore.actions.fetch() await hostingStore.actions.fetch()

View File

@ -0,0 +1,83 @@
<script>
import {
Icon,
Modal,
notifications,
ModalContent,
Body,
} from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import clientPackage from "@budibase/client/package.json"
let updateModal
$: appId = $store.appId
$: updateAvailable = clientPackage.version !== $store.version
const update = async () => {
try {
const response = await api.post(
`/api/applications/${appId}/client/update`
)
const json = await response.json()
if (response.status !== 200) {
throw json.message
}
// Reset frontend state after revert
const applicationPkg = await api.get(
`/api/applications/${appId}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg)
} else {
throw new Error(pkg)
}
notifications.success(
`App updated successfully to version ${clientPackage.version}`
)
} catch (err) {
notifications.error(`Error updating app: ${err}`)
}
}
</script>
<div class="icon-wrapper" class:highlight={updateAvailable}>
<Icon name="Refresh" hoverable on:click={updateModal.show} />
</div>
<Modal bind:this={updateModal}>
<ModalContent
title="App version"
confirmText="Update"
cancelText={updateAvailable ? "Cancel" : "Close"}
onConfirm={update}
showConfirmButton={updateAvailable}
>
{#if updateAvailable}
<Body size="S">
This app is currently using version <b>{$store.version}</b>, but version
<b>{clientPackage.version}</b> is available. Updates can contain new
features, performance improvements and bug fixes.
<br /><br />
Would you like to update this app?
</Body>
{:else}
<Body size="S">
This app is currently using version <b>{$store.version}</b> which is the
latest version available.
</Body>
{/if}
</ModalContent>
</Modal>
<style>
.icon-wrapper {
display: contents;
}
.icon-wrapper.highlight :global(svg) {
color: var(--spectrum-global-color-blue-600);
}
</style>

View File

@ -4,6 +4,7 @@
import { Icon, ActionGroup, Tabs, Tab } from "@budibase/bbui" import { Icon, ActionGroup, Tabs, Tab } from "@budibase/bbui"
import DeployModal from "components/deploy/DeployModal.svelte" import DeployModal from "components/deploy/DeployModal.svelte"
import RevertModal from "components/deploy/RevertModal.svelte" import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte"
import { get } from "builderStore/api" import { get } from "builderStore/api"
import { isActive, goto, layout } from "@roxi/routify" import { isActive, goto, layout } from "@roxi/routify"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
@ -80,6 +81,7 @@
<ActionGroup /> <ActionGroup />
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<VersionModal />
<RevertModal /> <RevertModal />
<Icon <Icon
name="Play" name="Play"

View File

@ -33,6 +33,10 @@ const {
} = require("../../utilities/workerRequests") } = require("../../utilities/workerRequests")
const { clientLibraryPath } = require("../../utilities") const { clientLibraryPath } = require("../../utilities")
const { getAllLocks } = require("../../utilities/redis") const { getAllLocks } = require("../../utilities/redis")
const {
uploadClientLibrary,
downloadLibraries,
} = require("../../utilities/fileSystem/newApp")
const URL_REGEX_SLASH = /\/|\\/g const URL_REGEX_SLASH = /\/|\\/g
@ -231,27 +235,17 @@ exports.create = async function (ctx) {
} }
exports.update = async function (ctx) { exports.update = async function (ctx) {
const url = await getAppUrlIfNotInUse(ctx) const data = await updateAppPackage(ctx, ctx.request.body, ctx.params.appId)
const db = new CouchDB(ctx.params.appId)
const application = await db.get(DocumentTypes.APP_METADATA)
const data = ctx.request.body
const newData = { ...application, ...data, url }
if (ctx.request.body._rev !== application._rev) {
newData._rev = application._rev
}
// the locked by property is attached by server but generated from
// Redis, shouldn't ever store it
if (newData.lockedBy) {
delete newData.lockedBy
}
const response = await db.put(newData)
data._rev = response.rev
ctx.status = 200 ctx.status = 200
ctx.body = response ctx.body = data
}
exports.updateClient = async function (ctx) {
await uploadClientLibrary(ctx.params.appId)
const appPackageUpdates = { version: packageJson.version }
const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId)
ctx.status = 200
ctx.body = data
} }
exports.delete = async function (ctx) { exports.delete = async function (ctx) {
@ -269,6 +263,28 @@ exports.delete = async function (ctx) {
ctx.body = result ctx.body = result
} }
const updateAppPackage = async (ctx, appPackage, appId) => {
const url = await getAppUrlIfNotInUse(ctx)
const db = new CouchDB(appId)
const application = await db.get(DocumentTypes.APP_METADATA)
const newAppPackage = { ...application, ...appPackage, url }
if (appPackage._rev !== application._rev) {
newAppPackage._rev = application._rev
}
// the locked by property is attached by server but generated from
// Redis, shouldn't ever store it
if (newAppPackage.lockedBy) {
delete newAppPackage.lockedBy
}
const response = await db.put(newAppPackage)
console.log(response)
return response
}
const createEmptyAppPackage = async (ctx, app) => { const createEmptyAppPackage = async (ctx, app) => {
const db = new CouchDB(app.appId) const db = new CouchDB(app.appId)

View File

@ -11,6 +11,11 @@ router
.get("/api/applications/:appId/appPackage", controller.fetchAppPackage) .get("/api/applications/:appId/appPackage", controller.fetchAppPackage)
.put("/api/applications/:appId", authorized(BUILDER), controller.update) .put("/api/applications/:appId", authorized(BUILDER), controller.update)
.post("/api/applications", authorized(BUILDER), controller.create) .post("/api/applications", authorized(BUILDER), controller.create)
.post(
"/api/applications/:appId/client/update",
authorized(BUILDER),
controller.updateClient
)
.delete("/api/applications/:appId", authorized(BUILDER), controller.delete) .delete("/api/applications/:appId", authorized(BUILDER), controller.delete)
module.exports = router module.exports = router

View File

@ -13,7 +13,7 @@ const {
deleteFolder, deleteFolder,
downloadTarball, downloadTarball,
} = require("./utilities") } = require("./utilities")
const { downloadLibraries, uploadClientLibrary } = require("./newApp") const { uploadClientLibrary } = require("./newApp")
const download = require("download") const download = require("download")
const env = require("../../environment") const env = require("../../environment")
const { homedir } = require("os") const { homedir } = require("os")
@ -144,7 +144,6 @@ exports.performBackup = async (appId, backupName) => {
* @return {Promise<void>} once promise completes app resources should be ready in object store. * @return {Promise<void>} once promise completes app resources should be ready in object store.
*/ */
exports.createApp = async appId => { exports.createApp = async appId => {
await downloadLibraries(appId)
await uploadClientLibrary(appId) await uploadClientLibrary(appId)
} }
@ -193,8 +192,17 @@ exports.getComponentLibraryManifest = async (appId, library) => {
delete require.cache[require.resolve(path)] delete require.cache[require.resolve(path)]
return require(path) return require(path)
} }
let resp
try {
// Try to load the manifest from the new file location
const path = join(appId, filename)
resp = await retrieve(ObjectStoreBuckets.APPS, path)
} catch (error) {
// Fallback to loading it from the old location for old apps
const path = join(appId, "node_modules", library, "package", filename) const path = join(appId, "node_modules", library, "package", filename)
let resp = await retrieve(ObjectStoreBuckets.APPS, path) resp = await retrieve(ObjectStoreBuckets.APPS, path)
}
if (typeof resp !== "string") { if (typeof resp !== "string") {
resp = resp.toString("utf8") resp = resp.toString("utf8")
} }

View File

@ -1,36 +1,27 @@
const packageJson = require("../../../package.json")
const { join } = require("path") const { join } = require("path")
const { ObjectStoreBuckets } = require("../../constants") const { ObjectStoreBuckets } = require("../../constants")
const { streamUpload, downloadTarball } = require("./utilities") const { streamUpload } = require("./utilities")
const fs = require("fs") const fs = require("fs")
const BUCKET_NAME = ObjectStoreBuckets.APPS const BUCKET_NAME = ObjectStoreBuckets.APPS
// can't really test this due to the downloading nature of it, wouldn't be a great test case exports.uploadClientLibrary = async appId => {
/* istanbul ignore next */ await streamUpload(
exports.downloadLibraries = async appId => {
const LIBRARIES = ["standard-components"]
const paths = {}
// Need to download tarballs directly from NPM as our users may not have node on their machine
for (let lib of LIBRARIES) {
// download tarball
const registryUrl = `https://registry.npmjs.org/@budibase/${lib}/-/${lib}-${packageJson.version}.tgz`
const path = join(appId, "node_modules", "@budibase", lib)
paths[`@budibase/${lib}`] = await downloadTarball(
registryUrl,
BUCKET_NAME, BUCKET_NAME,
path join(appId, "budibase-client.js"),
fs.createReadStream(require.resolve("@budibase/client")),
{
ContentType: "application/javascript",
}
)
await streamUpload(
BUCKET_NAME,
join(appId, "manifest.json"),
fs.createReadStream(
require.resolve("@budibase/standard-components/manifest.json")
),
{
ContentType: "application/javascript",
}
) )
} }
return paths
}
exports.uploadClientLibrary = async appId => {
const sourcepath = require.resolve("@budibase/client")
const destPath = join(appId, "budibase-client.js")
await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath), {
ContentType: "application/javascript",
})
}