1
0
Fork 0
mirror of synced 2024-04-30 02:22:32 +12:00

Un-revert Skeleton Loader PR (#13180)

* wip

* wip

* wip

* client versions init

* wip

* wip

* wip

* wip

* wip

* linting

* remove log

* comment client version script

* lint

* skeleton loader type fix

* fix types

* lint

* fix types again

* fix manifest not being served locally

* remove preinstalled old client version

* add constant for dev client version

* linting

* Dean PR Feedback

* linting

* pr feedback

* wip

* wip

* clientVersions empty array

* delete from git

* empty array again

* fix tests

* pr feedback

---------

Co-authored-by: Andrew Kingston <andrew@kingston.dev>
This commit is contained in:
Gerard Burns 2024-03-25 16:39:42 +00:00 committed by GitHub
parent 030ee5a5c5
commit d9033b2636
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 850 additions and 246 deletions

View file

@ -12,4 +12,5 @@ packages/sdk/sdk
packages/account-portal/packages/server/build
packages/account-portal/packages/ui/.routify
packages/account-portal/packages/ui/build
**/*.ivm.bundle.js
**/*.ivm.bundle.js
packages/server/build/oldClientVersions/**/**

3
.gitignore vendored
View file

@ -5,6 +5,9 @@ packages/server/runtime_apps/
bb-airgapped.tar.gz
*.iml
packages/server/build/oldClientVersions/**/*
packages/builder/src/components/deploy/clientVersions.json
# Logs
logs
*.log

View file

@ -31,6 +31,7 @@
},
"scripts": {
"preinstall": "node scripts/syncProPackage.js",
"get-past-client-version": "node scripts/getPastClientVersion.js",
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
"build": "NODE_OPTIONS=--max-old-space-size=1500 lerna run build --stream",
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",

View file

@ -0,0 +1,33 @@
<script>
import { API } from "api"
import clientVersions from "./clientVersions.json"
import { appStore } from "stores/builder"
import { Select } from "@budibase/bbui"
export let revertableVersion
$: appId = $appStore.appId
const handleChange = e => {
const value = e.detail
if (value == null) return
API.setRevertableVersion(appId, value)
}
</script>
<div class="select">
<Select
autoWidth
value={revertableVersion}
options={clientVersions}
on:change={handleChange}
footer={"Older versions of the Budibase client can be acquired using `yarn get-past-client-version x.x.x`. This toggle is only available in dev mode."}
/>
</div>
<style>
.select {
width: 120px;
display: inline-block;
}
</style>

View file

@ -1,4 +1,5 @@
<script>
import { admin } from "stores/portal"
import {
Modal,
notifications,
@ -9,6 +10,7 @@
} from "@budibase/bbui"
import { appStore, initialise } from "stores/builder"
import { API } from "api"
import RevertModalVersionSelect from "./RevertModalVersionSelect.svelte"
export function show() {
updateModal.show()
@ -28,7 +30,9 @@
$appStore.upgradableVersion &&
$appStore.version &&
$appStore.upgradableVersion !== $appStore.version
$: revertAvailable = $appStore.revertableVersion != null
$: revertAvailable =
$appStore.revertableVersion != null ||
($admin.isDev && $appStore.version === "0.0.0")
const refreshAppPackage = async () => {
try {
@ -62,7 +66,9 @@
// Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage()
notifications.success(
`App reverted successfully to version ${$appStore.revertableVersion}`
$appStore.revertableVersion
? `App reverted successfully to version ${$appStore.revertableVersion}`
: "App reverted successfully"
)
} catch (err) {
notifications.error(`Error reverting app: ${err}`)
@ -103,7 +109,13 @@
{#if revertAvailable}
<Body size="S">
You can revert this app to version
<b>{$appStore.revertableVersion}</b>
{#if $admin.isDev}
<RevertModalVersionSelect
revertableVersion={$appStore.revertableVersion}
/>
{:else}
<b>{$appStore.revertableVersion}</b>
{/if}
if you're experiencing issues with the current version.
</Body>
{/if}

View file

@ -0,0 +1 @@
[]

View file

@ -14,17 +14,11 @@
snippets,
} from "stores/builder"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
ProgressCircle,
Layout,
Heading,
Body,
Icon,
notifications,
} from "@budibase/bbui"
import { Layout, Heading, Body, Icon, notifications } from "@budibase/bbui"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "helpers/components"
import { isActive, goto } from "@roxi/routify"
import { ClientAppSkeleton } from "@budibase/frontend-core"
let iframe
let layout
@ -254,8 +248,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="component-container">
{#if loading}
<div class="center">
<ProgressCircle />
<div
class={`loading ${$themeStore.theme}`}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
>
<ClientAppSkeleton
sideNav={$navigationStore?.navigation === "Left"}
hideFooter
hideDevTools
/>
</div>
{:else if error}
<div class="center error">
@ -272,8 +274,6 @@
bind:this={iframe}
src="/app/preview"
class:hidden={loading || error}
class:tablet={$previewStore.previewDevice === "tablet"}
class:mobile={$previewStore.previewDevice === "mobile"}
/>
<div
class="add-component"
@ -293,6 +293,25 @@
/>
<style>
.loading {
position: absolute;
container-type: inline-size;
width: 100%;
height: 100%;
border: 2px solid transparent;
box-sizing: border-box;
}
.loading.tablet {
width: calc(1024px + 6px);
max-height: calc(768px + 6px);
}
.loading.mobile {
width: calc(390px + 6px);
max-height: calc(844px + 6px);
}
.component-container {
grid-row-start: middle;
grid-column-start: middle;

View file

@ -1,6 +1,12 @@
<script>
import { onMount, onDestroy } from "svelte"
import { params, goto } from "@roxi/routify"
import { auth, sideBarCollapsed, enrichedApps } from "stores/portal"
import {
licensing,
auth,
sideBarCollapsed,
enrichedApps,
} from "stores/portal"
import AppRowContext from "components/start/AppRowContext.svelte"
import FavouriteAppButton from "../FavouriteAppButton.svelte"
import {
@ -14,12 +20,17 @@
import { sdk } from "@budibase/shared-core"
import { API } from "api"
import ErrorSVG from "./ErrorSVG.svelte"
import { ClientAppSkeleton } from "@budibase/frontend-core"
$: app = $enrichedApps.find(app => app.appId === $params.appId)
$: iframeUrl = getIframeURL(app)
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
let loading = true
const getIframeURL = app => {
loading = true
if (app.status === "published") {
return `/app${app.url}`
}
@ -37,6 +48,20 @@
}
$: fetchScreens(app?.devId)
const receiveMessage = async message => {
if (message.data.type === "docLoaded") {
loading = false
}
}
onMount(() => {
window.addEventListener("message", receiveMessage)
})
onDestroy(() => {
window.removeEventListener("message", receiveMessage)
})
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
@ -108,7 +133,24 @@
</Body>
</div>
{:else}
<iframe src={iframeUrl} title={app.name} />
<div
class:hide={!loading || !app?.features?.skeletonLoader}
class="loading"
>
<div class={`loadingThemeWrapper ${app.theme}`}>
<ClientAppSkeleton
noAnimation
hideDevTools={app?.status === "published"}
sideNav={app?.navigation.navigation === "Left"}
hideFooter={$licensing.brandingEnabled}
/>
</div>
</div>
<iframe
class:hide={loading && app?.features?.skeletonLoader}
src={iframeUrl}
title={app.name}
/>
{/if}
</div>
@ -139,6 +181,23 @@
flex: 0 0 50px;
}
.loading {
height: 100%;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: var(--spacing-s);
overflow: hidden;
}
.loadingThemeWrapper {
height: 100%;
container-type: inline-size;
}
.hide {
visibility: hidden;
height: 0;
border: none;
}
iframe {
flex: 1 1 auto;
border-radius: var(--spacing-s);

View file

@ -10,7 +10,8 @@
"rowSelection": true,
"continueIfAction": true,
"showNotificationAction": true,
"sidePanel": true
"sidePanel": true,
"skeletonLoader": true
},
"layout": {
"name": "Layout",

View file

@ -7,6 +7,7 @@
import Component from "./Component.svelte"
import SDK from "sdk"
import {
featuresStore,
createContextStore,
initialise,
screenStore,
@ -38,7 +39,6 @@
import DevTools from "components/devtools/DevTools.svelte"
import FreeFooter from "components/FreeFooter.svelte"
import MaintenanceScreen from "components/MaintenanceScreen.svelte"
import licensing from "../licensing"
import SnippetsProvider from "./context/SnippetsProvider.svelte"
// Provide contexts
@ -83,11 +83,18 @@
}
}
let fontsLoaded = false
// Load app config
onMount(async () => {
document.fonts.ready.then(() => {
fontsLoaded = true
})
await initialise()
await authStore.actions.fetchUser()
dataLoaded = true
if (get(builderStore).inBuilder) {
builderStore.actions.notifyLoaded()
} else {
@ -96,6 +103,12 @@
})
}
})
$: {
if (dataLoaded && fontsLoaded) {
document.getElementById("clientAppSkeletonLoader")?.remove()
}
}
</script>
<svelte:head>
@ -106,148 +119,148 @@
{/if}
</svelte:head>
{#if dataLoaded}
<div
id="spectrum-root"
lang="en"
dir="ltr"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class:builder={$builderStore.inBuilder}
>
{#if $environmentStore.maintenance.length > 0}
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
{:else}
<DeviceBindingsProvider>
<UserBindingsProvider>
<StateBindingsProvider>
<RowSelectionProvider>
<QueryParamsProvider>
<SnippetsProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<div
id="spectrum-root"
lang="en"
dir="ltr"
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class:builder={$builderStore.inBuilder}
class:show={fontsLoaded && dataLoaded}
>
{#if $environmentStore.maintenance.length > 0}
<MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
{:else}
<DeviceBindingsProvider>
<UserBindingsProvider>
<StateBindingsProvider>
<RowSelectionProvider>
<QueryParamsProvider>
<SnippetsProvider>
<!-- Settings bar can be rendered outside of device preview -->
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder}
<SettingsBar />
{/if}
{/key}
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice ===
"tablet"}
class:mobile-preview={$builderStore.previewDevice ===
"mobile"}
>
<!-- Actual app -->
<div id="app-root">
{#if showDevTools}
<DevToolsHeader />
<!-- Clip boundary for selection indicators -->
<div
id="clip-root"
class:preview={$builderStore.inBuilder}
class:tablet-preview={$builderStore.previewDevice ===
"tablet"}
class:mobile-preview={$builderStore.previewDevice ===
"mobile"}
>
<!-- Actual app -->
<div id="app-root">
{#if showDevTools}
<DevToolsHeader />
{/if}
<div id="app-body">
{#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if}
<div id="app-body">
{#if permissionError}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
You don't have permission to use this app
</Heading>
<Body size="S">
Ask your administrator to grant you access
</Body>
</Layout>
</div>
{:else if !$screenStore.activeLayout}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
Something went wrong rendering your app
</Heading>
<Body size="S">
Get in touch with support if this issue persists
</Body>
</Layout>
</div>
{:else if embedNoScreens}
<div class="error">
<Layout justifyItems="center" gap="S">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html ErrorSVG}
<Heading size="L">
This Budibase app is not publicly accessible
</Heading>
</Layout>
</div>
{:else}
<CustomThemeWrapper>
{#key $screenStore.activeLayout._id}
<Component
isLayout
instance={$screenStore.activeLayout.props}
/>
{/key}
<!--
Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with.
-->
<div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top -->
<div class="modal-container" />
<!-- Layers on top of app -->
<NotificationDisplay />
<ConfirmationDisplay />
<PeekScreenDisplay />
</CustomThemeWrapper>
{/if}
{#if showDevTools}
<DevTools />
{/if}
</div>
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
<FreeFooter />
{#if showDevTools}
<DevTools />
{/if}
</div>
<!-- Preview and dev tools utilities -->
{#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{#if !$builderStore.inBuilder && $featuresStore.logoEnabled}
<FreeFooter />
{/if}
</div>
</SnippetsProvider>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
{/if}
</div>
<KeyboardManager />
{/if}
<!-- Preview and dev tools utilities -->
{#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if}
</div>
</SnippetsProvider>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
{/if}
</div>
<KeyboardManager />
<style>
#spectrum-root {
height: 0;
visibility: hidden;
padding: 0;
margin: 0;
overflow: hidden;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
@ -268,6 +281,11 @@
background-color: transparent;
}
#spectrum-root.show {
height: 100%;
visibility: visible;
}
#app-root {
overflow: hidden;
height: 100%;

View file

@ -13,6 +13,7 @@
<style>
.free-footer {
min-height: 51px;
flex: 0 0 auto;
padding: 16px 20px;
border-top: 1px solid var(--spectrum-global-color-gray-300);

View file

@ -1,5 +0,0 @@
import { isFreePlan } from "./utils.js"
export const logoEnabled = () => {
return isFreePlan()
}

View file

@ -1,7 +0,0 @@
import * as features from "./features"
const licensing = {
...features,
}
export default licensing

View file

@ -1,32 +0,0 @@
import { authStore } from "../stores/auth.js"
import { appStore } from "../stores/app.js"
import { get } from "svelte/store"
import { Constants } from "@budibase/frontend-core"
const getUserLicense = () => {
const user = get(authStore)
if (user) {
return user.license
}
}
const getAppLicenseType = () => {
const appDef = get(appStore)
if (appDef?.licenseType) {
return appDef.licenseType
}
}
export const isFreePlan = () => {
let licenseType = getAppLicenseType()
if (!licenseType) {
const license = getUserLicense()
licenseType = license?.plan?.type
}
if (licenseType) {
return licenseType === Constants.PlanType.FREE
} else {
// safety net - no license means free plan
return true
}
}

View file

@ -0,0 +1,42 @@
import { derived } from "svelte/store"
import { appStore } from "./app"
import { authStore } from "./auth"
import { Constants } from "@budibase/frontend-core"
const createFeaturesStore = () => {
return derived([authStore, appStore], ([$authStore, $appStore]) => {
const getUserLicense = () => {
const user = $authStore
if (user) {
return user.license
}
}
const getAppLicenseType = () => {
const appDef = $appStore
if (appDef?.licenseType) {
return appDef.licenseType
}
}
const isFreePlan = () => {
let licenseType = getAppLicenseType()
if (!licenseType) {
const license = getUserLicense()
licenseType = license?.plan?.type
}
if (licenseType) {
return licenseType === Constants.PlanType.FREE
} else {
// safety net - no license means free plan
return true
}
}
return {
logoEnabled: isFreePlan(),
}
})
}
export const featuresStore = createFeaturesStore()

View file

@ -31,6 +31,7 @@ export { hoverStore } from "./hover"
// Context stores are layered and duplicated, so it is not a singleton
export { createContextStore } from "./context"
export { featuresStore } from "./features"
// Initialises an app by loading screens and routes
export { initialise } from "./initialise"

View file

@ -197,4 +197,13 @@ export const buildAppEndpoints = API => ({
url: `/api/applications/${appId}/sample`,
})
},
setRevertableVersion: async (appId, revertableVersion) => {
return await API.post({
url: `/api/applications/${appId}/setRevertableVersion`,
body: {
revertableVersion,
},
})
},
})

View file

@ -0,0 +1,244 @@
<script>
export let sideNav = false
export let hideDevTools = false
export let hideFooter = false
export let noAnimation = false
</script>
<div class:sideNav id="clientAppSkeletonLoader" class="skeleton">
<div class="animation" class:noAnimation />
{#if !hideDevTools}
<div class="devTools" />
{/if}
<div class="main">
<div class="nav" />
<div class="body">
<div class="bodyVerticalPadding" />
<div class="bodyHorizontal">
<div class="bodyHorizontalPadding" />
<svg
class="svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="240"
height="256"
>
<mask id="mask">
<rect x="0" y="0" width="240" height="256" fill="white" />
<rect x="0" y="0" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="56" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="112" width="240" height="32" rx="6" fill="black" />
<rect x="0" y="168" width="240" height="32" rx="6" fill="black" />
<rect x="71" y="224" width="98" height="32" rx="6" fill="black" />
</mask>
<rect
x="0"
y="0"
width="240"
height="256"
fill="black"
mask="url(#mask)"
/>
</svg>
<div class="bodyHorizontalPadding" />
</div>
<div class="bodyVerticalPadding" />
</div>
</div>
{#if !hideFooter}
<div class="footer" />
{/if}
</div>
<style>
.skeleton {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
border-radius: 4px;
overflow: hidden;
background-color: var(--spectrum-global-color-gray-200);
}
.animation {
position: absolute;
height: 100%;
width: 100%;
background: linear-gradient(
to right,
transparent 0%,
var(--spectrum-global-color-gray-300) 20%,
transparent 40%,
transparent 100%
);
animation-duration: 1.3s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: shimmer;
animation-timing-function: linear;
}
.noAnimation {
animation-name: none;
background: transparent;
}
.devTools {
display: flex;
box-sizing: border-box;
background-color: black;
height: 60px;
padding: 1px 24px 1px 20px;
display: flex;
align-items: center;
z-index: 1;
flex-shrink: 0;
color: white;
mix-blend-mode: multiply;
background: rgb(0 0 0);
font-size: 30px;
font-family: Source Sans Pro;
-webkit-font-smoothing: antialiased;
}
.main {
height: 100%;
display: flex;
flex-direction: column;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .main {
flex-direction: column;
width: initial;
}
}
.sideNav .main {
flex-direction: row;
width: 100%;
}
.nav {
flex-shrink: 0;
width: 100%;
height: 141px;
background-color: transparent;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .nav {
height: 61px;
width: initial;
}
}
.sideNav .nav {
height: 100%;
width: 251px;
}
.body {
z-index: 2;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .body {
width: initial;
height: 100%;
}
}
.sideNav .body {
width: 100%;
height: initial;
}
.body :global(svg > rect) {
fill: var(--spectrum-alias-background-color-primary);
}
.body :global(svg) {
flex-shrink: 0;
}
.bodyHorizontal {
display: flex;
flex-shrink: 0;
}
.bodyHorizontalPadding {
height: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.bodyVerticalPadding {
width: 100%;
flex-grow: 1;
background-color: var(--spectrum-alias-background-color-primary);
}
.footer {
flex-shrink: 0;
box-sizing: border-box;
z-index: 1;
height: 52px;
width: 100%;
}
@media (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
@container (max-width: 720px) {
#clientAppSkeletonLoader .footer {
border-top: none;
}
}
.sideNav .footer {
border-top: 3px solid var(--spectrum-alias-background-color-primary);
}
@keyframes shimmer {
0% {
left: -170%;
}
100% {
left: 170%;
}
}
</style>

View file

@ -5,3 +5,4 @@ export { default as UserAvatar } from "./UserAvatar.svelte"
export { default as UserAvatars } from "./UserAvatars.svelte"
export { default as Updating } from "./Updating.svelte"
export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"

View file

@ -18,4 +18,3 @@
--drop-shadow: rgba(0, 0, 0, 0.25) !important;
--spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
}

View file

@ -53,6 +53,7 @@
"@budibase/pro": "0.0.0",
"@budibase/shared-core": "0.0.0",
"@budibase/string-templates": "0.0.0",
"@budibase/frontend-core": "0.0.0",
"@budibase/types": "0.0.0",
"@bull-board/api": "5.10.2",
"@bull-board/koa": "5.10.2",

View file

@ -306,6 +306,7 @@ async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) {
features: {
componentValidation: true,
disableUserMetadata: true,
skeletonLoader: true,
},
}
@ -486,10 +487,11 @@ export async function updateClient(ctx: UserCtx) {
const application = await db.get<App>(DocumentType.APP_METADATA)
const currentVersion = application.version
let manifest
// Update client library and manifest
if (!env.isTest()) {
await backupClientLibrary(ctx.params.appId)
await updateClientLibrary(ctx.params.appId)
manifest = await updateClientLibrary(ctx.params.appId)
}
// Update versions in app package
@ -497,6 +499,10 @@ export async function updateClient(ctx: UserCtx) {
const appPackageUpdates = {
version: updatedToVersion,
revertableVersion: currentVersion,
features: {
...(application.features ?? {}),
skeletonLoader: manifest?.features?.skeletonLoader ?? false,
},
}
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
await events.app.versionUpdated(app, currentVersion, updatedToVersion)
@ -512,9 +518,10 @@ export async function revertClient(ctx: UserCtx) {
ctx.throw(400, "There is no version to revert to")
}
let manifest
// Update client library and manifest
if (!env.isTest()) {
await revertClientLibrary(ctx.params.appId)
manifest = await revertClientLibrary(ctx.params.appId)
}
// Update versions in app package
@ -523,6 +530,10 @@ export async function revertClient(ctx: UserCtx) {
const appPackageUpdates = {
version: revertedToVersion,
revertableVersion: undefined,
features: {
...(application.features ?? {}),
skeletonLoader: manifest?.features?.skeletonLoader ?? false,
},
}
const app = await updateAppPackage(appPackageUpdates, ctx.params.appId)
await events.app.versionReverted(app, currentVersion, revertedToVersion)
@ -729,6 +740,21 @@ export async function updateAppPackage(
})
}
export async function setRevertableVersion(
ctx: UserCtx<{ revertableVersion: string }, App>
) {
if (!env.isDev()) {
ctx.status = 403
return
}
const db = context.getAppDB()
const app = await db.get<App>(DocumentType.APP_METADATA)
app.revertableVersion = ctx.request.body.revertableVersion
await db.put(app)
ctx.status = 200
}
async function migrateAppNavigation() {
const db = context.getAppDB()
const existing: App = await db.get(DocumentType.APP_METADATA)

View file

@ -1,10 +1,8 @@
import { InvalidFileExtensions } from "@budibase/shared-core"
import AppComponent from "./templates/BudibaseApp.svelte"
import { join } from "../../../utilities/centralPath"
import * as uuid from "uuid"
import { ObjectStoreBuckets } from "../../../constants"
import { ObjectStoreBuckets, devClientVersion } from "../../../constants"
import { processString } from "@budibase/string-templates"
import {
loadHandlebarsFile,
@ -24,13 +22,20 @@ import AWS from "aws-sdk"
import fs from "fs"
import sdk from "../../../sdk"
import * as pro from "@budibase/pro"
import { App, Ctx, ProcessAttachmentResponse } from "@budibase/types"
import {
UserCtx,
App,
Ctx,
ProcessAttachmentResponse,
Feature,
} from "@budibase/types"
import {
getAppMigrationVersion,
getLatestMigrationId,
} from "../../../appMigrations"
import send from "koa-send"
import { getThemeVariables } from "../../../constants/themes"
export const toggleBetaUiFeature = async function (ctx: Ctx) {
const cookieName = `beta:${ctx.params.feature}`
@ -146,7 +151,7 @@ const requiresMigration = async (ctx: Ctx) => {
return requiresMigrations
}
export const serveApp = async function (ctx: Ctx) {
export const serveApp = async function (ctx: UserCtx) {
const needMigrations = await requiresMigration(ctx)
const bbHeaderEmbed =
@ -165,12 +170,23 @@ export const serveApp = async function (ctx: Ctx) {
try {
db = context.getAppDB({ skip_setup: true })
const appInfo = await db.get<any>(DocumentType.APP_METADATA)
let appId = context.getAppId()
const hideDevTools = !!ctx.params.appUrl
const sideNav = appInfo.navigation.navigation === "Left"
const hideFooter =
ctx?.user?.license?.features?.includes(Feature.BRANDING) || false
const themeVariables = getThemeVariables(appInfo?.theme)
if (!env.isJest()) {
const plugins = objectStore.enrichPluginURLs(appInfo.usedPlugins)
const { head, html, css } = AppComponent.render({
title: branding?.platformTitle || `${appInfo.name}`,
showSkeletonLoader: appInfo.features?.skeletonLoader ?? false,
hideDevTools,
sideNav,
hideFooter,
metaImage:
branding?.metaImageUrl ||
"https://res.cloudinary.com/daog6scxm/image/upload/v1698759482/meta-images/plain-branded-meta-image-coral_ocxmgu.png",
@ -195,7 +211,7 @@ export const serveApp = async function (ctx: Ctx) {
ctx.body = await processString(appHbs, {
head,
body: html,
style: css.code,
css: `:root{${themeVariables}} ${css.code}`,
appId,
embedded: bbHeaderEmbed,
})
@ -247,18 +263,20 @@ export const serveBuilderPreview = async function (ctx: Ctx) {
}
export const serveClientLibrary = async function (ctx: Ctx) {
const version = ctx.request.query.version
const appId = context.getAppId() || (ctx.request.query.appId as string)
let rootPath = join(NODE_MODULES_PATH, "@budibase", "client", "dist")
if (!appId) {
ctx.throw(400, "No app ID provided - cannot fetch client library.")
}
if (env.isProd()) {
if (env.isProd() || (env.isDev() && version !== devClientVersion)) {
ctx.body = await objectStore.getReadStream(
ObjectStoreBuckets.APPS,
objectStore.clientLibraryPath(appId!)
)
ctx.set("Content-Type", "application/javascript")
} else if (env.isDev()) {
} else if (env.isDev() && version === devClientVersion) {
// incase running from TS directly
const tsPath = join(require.resolve("@budibase/client"), "..")
return send(ctx, "budibase-client.js", {

View file

@ -1,4 +1,6 @@
<script>
import ClientAppSkeleton from "@budibase/frontend-core/src/components/ClientAppSkeleton.svelte"
export let title = ""
export let favicon = ""
@ -9,6 +11,11 @@
export let clientLibPath
export let usedPlugins
export let appMigrating
export let showSkeletonLoader = false
export let hideDevTools
export let sideNav
export let hideFooter
</script>
<svelte:head>
@ -96,6 +103,9 @@
</svelte:head>
<body id="app">
{#if showSkeletonLoader}
<ClientAppSkeleton {hideDevTools} {sideNav} {hideFooter} />
{/if}
<div id="error">
{#if clientLibPath}
<h1>There was an error loading your app</h1>

View file

@ -1,8 +1,12 @@
<html>
<script>
document.fonts.ready.then(() => {
window.parent.postMessage({ type: "docLoaded" });
})
</script>
<head>
{{{head}}}
<style>{{{style}}}</style>
<style>{{{css}}}</style>
</head>
<script>

View file

@ -68,5 +68,10 @@ router
authorized(permissions.BUILDER),
controller.importToApp
)
.post(
"/api/applications/:appId/setRevertableVersion",
authorized(permissions.BUILDER),
controller.setRevertableVersion
)
export default router

View file

@ -51,8 +51,8 @@ router
controller.deleteObjects
)
.get("/app/preview", authorized(BUILDER), controller.serveBuilderPreview)
.get("/:appId/:path*", controller.serveApp)
.get("/app/:appUrl/:path*", controller.serveApp)
.get("/:appId/:path*", controller.serveApp)
.post(
"/api/attachments/:datasourceId/url",
authorized(PermissionType.TABLE, PermissionLevel.READ),

View file

@ -165,6 +165,8 @@ export enum AutomationErrors {
FAILURE_CONDITION = "FAILURE_CONDITION_MET",
}
export const devClientVersion = "0.0.0"
// pass through the list from the auth/core lib
export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
export const MAX_AUTOMATION_RECURRING_ERRORS = 5

View file

@ -0,0 +1,54 @@
export const getThemeVariables = (theme: string) => {
if (theme === "spectrum--lightest") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(244, 244, 244);
--spectrum-global-color-gray-300: rgb(234, 234, 234);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--light") {
return `
--spectrum-global-color-gray-50: rgb(255, 255, 255);
--spectrum-global-color-gray-200: rgb(234, 234, 234);
--spectrum-global-color-gray-300: rgb(225, 225, 225);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-50);
`
}
if (theme === "spectrum--dark") {
return `
--spectrum-global-color-gray-100: rgb(50, 50, 50);
--spectrum-global-color-gray-200: rgb(62, 62, 62);
--spectrum-global-color-gray-300: rgb(74, 74, 74);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--darkest") {
return `
--spectrum-global-color-gray-100: rgb(30, 30, 30);
--spectrum-global-color-gray-200: rgb(44, 44, 44);
--spectrum-global-color-gray-300: rgb(57, 57, 57);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--nord") {
return `
--spectrum-global-color-gray-100: #3b4252;
--spectrum-global-color-gray-200: #424a5c;
--spectrum-global-color-gray-300: #4c566a;
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
if (theme === "spectrum--midnight") {
return `
--hue: 220;
--sat: 10%;
--spectrum-global-color-gray-100: hsl(var(--hue), var(--sat), 17%);
--spectrum-global-color-gray-200: hsl(var(--hue), var(--sat), 20%);
--spectrum-global-color-gray-300: hsl(var(--hue), var(--sat), 24%);
--spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-100);
`
}
}

View file

@ -1,11 +1,13 @@
import { budibaseTempDir } from "../budibaseDir"
import fs from "fs"
import { join } from "path"
import { ObjectStoreBuckets } from "../../constants"
import { ObjectStoreBuckets, devClientVersion } from "../../constants"
import { updateClientLibrary } from "./clientLibrary"
import env from "../../environment"
import { objectStore, context } from "@budibase/backend-core"
import { TOP_LEVEL_PATH } from "./filesystem"
import { DocumentType } from "../../db/utils"
import { App } from "@budibase/types"
export const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules")
@ -35,20 +37,25 @@ export const getComponentLibraryManifest = async (library: string) => {
const filename = "manifest.json"
if (env.isDev() || env.isTest()) {
const paths = [
join(TOP_LEVEL_PATH, "packages/client", filename),
join(process.cwd(), "client", filename),
]
for (let path of paths) {
if (fs.existsSync(path)) {
// always load from new so that updates are refreshed
delete require.cache[require.resolve(path)]
return require(path)
const db = context.getAppDB()
const app = await db.get<App>(DocumentType.APP_METADATA)
if (app.version === devClientVersion || env.isTest()) {
const paths = [
join(TOP_LEVEL_PATH, "packages/client", filename),
join(process.cwd(), "client", filename),
]
for (let path of paths) {
if (fs.existsSync(path)) {
// always load from new so that updates are refreshed
delete require.cache[require.resolve(path)]
return require(path)
}
}
throw new Error(
`Unable to find ${filename} in development environment (may need to build).`
)
}
throw new Error(
`Unable to find ${filename} in development environment (may need to build).`
)
}
if (!appId) {

View file

@ -1,10 +1,12 @@
import path, { join } from "path"
import { ObjectStoreBuckets } from "../../constants"
import fs from "fs"
import { objectStore } from "@budibase/backend-core"
import { context, objectStore } from "@budibase/backend-core"
import { resolve } from "../centralPath"
import env from "../../environment"
import { TOP_LEVEL_PATH } from "./filesystem"
import { DocumentType } from "../../db/utils"
import { App } from "@budibase/types"
export function devClientLibPath() {
return require.resolve("@budibase/client")
@ -120,7 +122,12 @@ export async function updateClientLibrary(appId: string) {
ContentType: "application/javascript",
}
)
await Promise.all([manifestUpload, clientUpload])
const manifestSrc = fs.promises.readFile(manifest, "utf8")
await Promise.all([manifestUpload, clientUpload, manifestSrc])
return JSON.parse(await manifestSrc)
}
/**
@ -130,30 +137,49 @@ export async function updateClientLibrary(appId: string) {
* @returns {Promise<void>}
*/
export async function revertClientLibrary(appId: string) {
// Copy backups manifest to tmp directory
const tmpManifestPath = await objectStore.retrieveToTmp(
ObjectStoreBuckets.APPS,
join(appId, "manifest.json.bak")
)
let manifestPath, clientPath
// Copy backup client lib to tmp
const tmpClientPath = await objectStore.retrieveToTmp(
ObjectStoreBuckets.APPS,
join(appId, "budibase-client.js.bak")
)
if (env.isDev()) {
const db = context.getAppDB()
const app = await db.get<App>(DocumentType.APP_METADATA)
clientPath = join(
__dirname,
`/oldClientVersions/${app.revertableVersion}/app.js`
)
manifestPath = join(
__dirname,
`/oldClientVersions/${app.revertableVersion}/manifest.json`
)
} else {
// Copy backups manifest to tmp directory
manifestPath = await objectStore.retrieveToTmp(
ObjectStoreBuckets.APPS,
join(appId, "manifest.json.bak")
)
// Copy backup client lib to tmp
clientPath = await objectStore.retrieveToTmp(
ObjectStoreBuckets.APPS,
join(appId, "budibase-client.js.bak")
)
}
const manifestSrc = fs.promises.readFile(manifestPath, "utf8")
// Upload backups as new versions
const manifestUpload = objectStore.upload({
bucket: ObjectStoreBuckets.APPS,
filename: join(appId, "manifest.json"),
path: tmpManifestPath,
path: manifestPath,
type: "application/json",
})
const clientUpload = objectStore.upload({
bucket: ObjectStoreBuckets.APPS,
filename: join(appId, "budibase-client.js"),
path: tmpClientPath,
path: clientPath,
type: "application/javascript",
})
await Promise.all([manifestUpload, clientUpload])
await Promise.all([manifestSrc, manifestUpload, clientUpload])
return JSON.parse(await manifestSrc)
}

View file

@ -71,6 +71,7 @@ export interface AppIcon {
export interface AppFeatures {
componentValidation?: boolean
disableUserMetadata?: boolean
skeletonLoader?: boolean
}
export interface AutomationSettings {

View file

@ -3,7 +3,7 @@
const start = Date.now()
const fs = require("fs")
const { readdir, copyFile, mkdir } = require('node:fs/promises');
const { cp, readdir, copyFile, mkdir } = require('node:fs/promises');
const path = require("path")
const { build } = require("esbuild")
@ -17,12 +17,6 @@ const { nodeExternalsPlugin } = require("esbuild-node-externals")
const svelteCompilePlugin = {
name: 'svelteCompile',
setup(build) {
// This resolve handler is necessary to bundle the Svelte runtime into the the final output,
// otherwise the bundled script will attempt to resolve it at runtime
build.onResolve({ filter: /svelte\/internal/ }, async () => {
return { path: `${process.cwd()}/../../node_modules/svelte/src/runtime/internal/ssr.js` }
})
// Compiles `.svelte` files into JS classes so that they can be directly imported into our
// Typescript packages
build.onLoad({ filter: /\.svelte$/ }, async (args) => {
@ -37,7 +31,7 @@ const svelteCompilePlugin = {
contents: js.code,
// The loader this is passed to, basically how the above provided content is "treated",
// the contents provided above will be transpiled and bundled like any other JS file.
loader: 'js',
loader: 'js',
// Where to resolve any imports present in the loaded file
resolveDir: dir
}
@ -80,11 +74,11 @@ async function runBuild(entry, outfile) {
plugins: [
svelteCompilePlugin,
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
nodeExternalsPlugin(),
nodeExternalsPlugin({
allowList: ["@budibase/frontend-core", "svelte"]
}),
],
preserveSymlinks: true,
loader: {
},
metafile: true,
external: [
"deasync",
@ -109,13 +103,23 @@ async function runBuild(entry, outfile) {
await Promise.all(fileCopyPromises)
})()
const oldClientVersions = (async () => {
try {
await cp('./build/oldClientVersions', './dist/oldClientVersions', { recursive: true });
} catch (e) {
if (e.code !== "EEXIST" && e.code !== "ENOENT") {
throw e;
}
}
})()
const mainBuild = build({
...sharedConfig,
platform: "node",
outfile,
})
await Promise.all([hbsFiles, mainBuild])
await Promise.all([hbsFiles, mainBuild, oldClientVersions])
fs.writeFileSync(
`dist/${path.basename(outfile)}.meta.json`,

View file

@ -0,0 +1,45 @@
const fs = require('node:fs/promises');
const util = require('node:util');
const { argv } = require('node:process');
const exec = util.promisify(require('node:child_process').exec);
const version = argv[2]
const getPastClientVersion = async () => {
const manifestRaw = await fetch(`https://api.github.com/repos/budibase/budibase/contents/packages/client/manifest.json?ref=v${version}`).then(r => r.json());
// GitHub response will contain a message field containing the following string if the version can't be found.
if (manifestRaw?.message?.includes("No commit found")) {
throw `Can't find a GitHub tag with version "${version}"`
}
const manifest = Buffer.from(manifestRaw.content, 'base64').toString('utf8')
await fs.mkdir(`packages/server/build/oldClientVersions/${version}`, { recursive: true });
await fs.writeFile(`packages/server/build/oldClientVersions/${version}/manifest.json`, manifest)
const npmRegistry = await fetch(`https://registry.npmjs.org/@budibase/client/${version}`).then(r => r.json());
// The json response from npm is just a string starting with the following if the version can't be found
if (typeof npmRegistry === "string" && npmRegistry.startsWith("version not found")) {
throw `Can't find @budibase/client with version "${version}" in npm registry`
}
// Create a temp directory to store the @budibase/client library in
await fs.mkdir("clientVersionTmp", { recursive: true });
// Get the tarball of the @budibase/client library and pipe it into tar to extract it
await exec(`curl -L ${npmRegistry.dist.tarball} --output - | tar -xvzf - -C clientVersionTmp`);
// Copy the compiled client version we want to the oldClientVersions dir and delete the temp directory
await fs.copyFile('./clientVersionTmp/package/dist/budibase-client.js', `./packages/server/build/oldClientVersions/${version}/app.js`);
await fs.rm("clientVersionTmp", { recursive: true });
// Check what client versions the user has locally and update the `clientVersions.json` file in the builder so that they can be selected
const rootDir = await fs.readdir('packages/server/build/oldClientVersions/', { withFileTypes: true });
const dirs = rootDir.filter(entry => entry.isDirectory()).map(dir => dir.name);
await fs.writeFile("packages/builder/src/components/deploy/clientVersions.json", JSON.stringify(dirs))
}
getPastClientVersion().catch(e => console.error(e));