Add client libary update management from inside the builder
This commit is contained in:
parent
0ff1f0fbe9
commit
0a44b1e3d8
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
/* istanbul ignore next */
|
|
||||||
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,
|
|
||||||
path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.uploadClientLibrary = async appId => {
|
exports.uploadClientLibrary = async appId => {
|
||||||
const sourcepath = require.resolve("@budibase/client")
|
await streamUpload(
|
||||||
const destPath = join(appId, "budibase-client.js")
|
BUCKET_NAME,
|
||||||
|
join(appId, "budibase-client.js"),
|
||||||
await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath), {
|
fs.createReadStream(require.resolve("@budibase/client")),
|
||||||
|
{
|
||||||
ContentType: "application/javascript",
|
ContentType: "application/javascript",
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
await streamUpload(
|
||||||
|
BUCKET_NAME,
|
||||||
|
join(appId, "manifest.json"),
|
||||||
|
fs.createReadStream(
|
||||||
|
require.resolve("@budibase/standard-components/manifest.json")
|
||||||
|
),
|
||||||
|
{
|
||||||
|
ContentType: "application/javascript",
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue