Merge pull request #12914 from Budibase/budi-7664-sqs-self-host-ui-for-detecting-lack-of-sqs-support-2

Plumbing for showing a maintenance page when SQS is required but missing.
This commit is contained in:
Sam Rose 2024-03-14 14:53:47 +00:00 committed by GitHub
commit 7a7a30b8f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 288 additions and 107 deletions

View File

@ -71,6 +71,10 @@
await auth.getSelf() await auth.getSelf()
await admin.init() await admin.init()
if ($admin.maintenance.length > 0) {
$redirect("./maintenance")
}
if ($auth.user) { if ($auth.user) {
await licensing.init() await licensing.init()
} }

View File

@ -0,0 +1,83 @@
<script>
import { MaintenanceType } from "@budibase/types"
import { Heading, Body, Button, Layout } from "@budibase/bbui"
import { admin } from "stores/portal"
import BudibaseLogo from "../portal/_components/BudibaseLogo.svelte"
$: {
if ($admin.maintenance.length === 0) {
window.location = "/builder"
}
}
</script>
<div class="main">
<div class="content">
<div class="hero">
<BudibaseLogo />
</div>
<div class="inner-content">
{#each $admin.maintenance as maintenance}
{#if maintenance.type === MaintenanceType.SQS_MISSING}
<Layout>
<Heading>Please upgrade your Budibase installation</Heading>
<Body>
We've detected that the version of Budibase you're using depends
on a more recent version of the CouchDB database than what you
have installed.
</Body>
<Body>
To resolve this, you can either rollback to a previous version of
Budibase, or follow the migration guide to update to a later
version of CouchDB.
</Body>
</Layout>
<Button
on:click={() => (window.location = "https://docs.budibase.com")}
>Migration guide</Button
>
{/if}
{/each}
</div>
</div>
</div>
<style>
.main {
max-width: 700px;
margin: auto;
height: 100vh;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: var(--spacing-l);
}
.hero {
margin: var(--spacing-l);
}
.content {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: center;
gap: var(--spacing-m);
}
.inner-content {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-m);
}
@media only screen and (max-width: 600px) {
.content {
flex-direction: column;
align-items: flex-start;
}
.main {
height: auto;
}
}
</style>

View File

@ -0,0 +1,15 @@
<script>
import Logo from "assets/bb-emblem.svg"
import { goto } from "@roxi/routify"
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions-->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<img src={Logo} alt="Budibase Logo" on:click={() => $goto("./apps")} />
<style>
img {
width: 30px;
height: 30px;
}
</style>

View File

@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = {
adminUser: { checked: false }, adminUser: { checked: false },
sso: { checked: false }, sso: { checked: false },
}, },
maintenance: [],
offlineMode: false, offlineMode: false,
} }
@ -48,6 +49,7 @@ export function createAdminStore() {
store.isDev = environment.isDev store.isDev = environment.isDev
store.baseUrl = environment.baseUrl store.baseUrl = environment.baseUrl
store.offlineMode = environment.offlineMode store.offlineMode = environment.offlineMode
store.maintenance = environment.maintenance
return store return store
}) })
} }

View File

@ -17,6 +17,7 @@
appStore, appStore,
devToolsStore, devToolsStore,
devToolsEnabled, devToolsEnabled,
environmentStore,
} from "stores" } from "stores"
import NotificationDisplay from "components/overlay/NotificationDisplay.svelte" import NotificationDisplay from "components/overlay/NotificationDisplay.svelte"
import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte" import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte"
@ -36,6 +37,7 @@
import DevToolsHeader from "components/devtools/DevToolsHeader.svelte" import DevToolsHeader from "components/devtools/DevToolsHeader.svelte"
import DevTools from "components/devtools/DevTools.svelte" import DevTools from "components/devtools/DevTools.svelte"
import FreeFooter from "components/FreeFooter.svelte" import FreeFooter from "components/FreeFooter.svelte"
import MaintenanceScreen from "components/MaintenanceScreen.svelte"
import licensing from "../licensing" import licensing from "../licensing"
// Provide contexts // Provide contexts
@ -111,122 +113,128 @@
class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}" class="spectrum spectrum--medium {$themeStore.baseTheme} {$themeStore.theme}"
class:builder={$builderStore.inBuilder} class:builder={$builderStore.inBuilder}
> >
<DeviceBindingsProvider> {#if $environmentStore.maintenance.length > 0}
<UserBindingsProvider> <MaintenanceScreen maintenanceList={$environmentStore.maintenance} />
<StateBindingsProvider> {:else}
<RowSelectionProvider> <DeviceBindingsProvider>
<QueryParamsProvider> <UserBindingsProvider>
<!-- Settings bar can be rendered outside of device preview --> <StateBindingsProvider>
<!-- Key block needs to be outside the if statement or it breaks --> <RowSelectionProvider>
{#key $builderStore.selectedComponentId} <QueryParamsProvider>
{#if $builderStore.inBuilder} <!-- Settings bar can be rendered outside of device preview -->
<SettingsBar /> <!-- Key block needs to be outside the if statement or it breaks -->
{/if} {#key $builderStore.selectedComponentId}
{/key} {#if $builderStore.inBuilder}
<SettingsBar />
<!-- 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} {/if}
{/key}
<div id="app-body"> <!-- Clip boundary for selection indicators -->
{#if permissionError} <div
<div class="error"> id="clip-root"
<Layout justifyItems="center" gap="S"> class:preview={$builderStore.inBuilder}
<!-- eslint-disable-next-line svelte/no-at-html-tags --> class:tablet-preview={$builderStore.previewDevice ===
{@html ErrorSVG} "tablet"}
<Heading size="L"> class:mobile-preview={$builderStore.previewDevice ===
You don't have permission to use this app "mobile"}
</Heading> >
<Body size="S"> <!-- Actual app -->
Ask your administrator to grant you access <div id="app-root">
</Body> {#if showDevTools}
</Layout> <DevToolsHeader />
</div> {/if}
{: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}
<!-- <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. Flatpickr needs to be inside the theme wrapper.
It also needs its own container because otherwise it hijacks It also needs its own container because otherwise it hijacks
key events on the whole page. It is painful to work with. key events on the whole page. It is painful to work with.
--> -->
<div id="flatpickr-root" /> <div id="flatpickr-root" />
<!-- Modal container to ensure they sit on top --> <!-- Modal container to ensure they sit on top -->
<div class="modal-container" /> <div class="modal-container" />
<!-- Layers on top of app --> <!-- Layers on top of app -->
<NotificationDisplay /> <NotificationDisplay />
<ConfirmationDisplay /> <ConfirmationDisplay />
<PeekScreenDisplay /> <PeekScreenDisplay />
</CustomThemeWrapper> </CustomThemeWrapper>
{/if} {/if}
{#if showDevTools} {#if showDevTools}
<DevTools /> <DevTools />
{/if}
</div>
{#if !$builderStore.inBuilder && licensing.logoEnabled()}
<FreeFooter />
{/if} {/if}
</div> </div>
{#if !$builderStore.inBuilder && licensing.logoEnabled()} <!-- Preview and dev tools utilities -->
<FreeFooter /> {#if $appStore.isDevApp}
<SelectionIndicator />
{/if}
{#if $builderStore.inBuilder || $devToolsStore.allowSelection}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if} {/if}
</div> </div>
</QueryParamsProvider>
<!-- Preview and dev tools utilities --> </RowSelectionProvider>
{#if $appStore.isDevApp} </StateBindingsProvider>
<SelectionIndicator /> </UserBindingsProvider>
{/if} </DeviceBindingsProvider>
{#if $builderStore.inBuilder || $devToolsStore.allowSelection} {/if}
<HoverIndicator />
{/if}
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
{/if}
</div>
</QueryParamsProvider>
</RowSelectionProvider>
</StateBindingsProvider>
</UserBindingsProvider>
</DeviceBindingsProvider>
</div> </div>
<KeyboardManager /> <KeyboardManager />
{/if} {/if}

View File

@ -0,0 +1,53 @@
<!--
This is the public facing maintenance screen. It is displayed when there is
required maintenance to be done on the Budibase installation. We only use this
if we detect that the Budibase installation is in a state where the vast
majority of apps would not function correctly.
The builder-facing maintenance screen is in
packages/builder/src/pages/builder/maintenance/index.svelte, and tends to
contain more detailed information and actions for the installation owner to
take.
-->
<script>
import { MaintenanceType } from "@budibase/types"
import { Heading, Body, Layout } from "@budibase/bbui"
export let maintenanceList
</script>
<div class="content">
{#each maintenanceList as maintenance}
{#if maintenance.type === MaintenanceType.SQS_MISSING}
<Layout>
<Heading>Budibase installation requires maintenance</Heading>
<Body>
The administrator of this Budibase installation needs to take actions
to update components that are out of date. Please contact them and
show them this warning. More information will be available when they
log into their account.
</Body>
</Layout>
{/if}
{/each}
</div>
<style>
.content {
max-width: 700px;
margin: auto;
height: 100vh;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: var(--spacing-l);
}
@media (max-width: 640px) {
.content {
justify-content: flex-start;
align-items: flex-start;
}
}
</style>

View File

@ -2,3 +2,7 @@ export enum ServiceType {
WORKER = "worker", WORKER = "worker",
APPS = "apps", APPS = "apps",
} }
export enum MaintenanceType {
SQS_MISSING = "sqs_missing",
}

View File

@ -1,10 +1,18 @@
import { Ctx } from "@budibase/types" import { Ctx, MaintenanceType } from "@budibase/types"
import env from "../../../environment" import env from "../../../environment"
import { env as coreEnv } from "@budibase/backend-core" import { env as coreEnv } from "@budibase/backend-core"
import nodeFetch from "node-fetch" import nodeFetch from "node-fetch"
// When we come to move to SQS fully and move away from Clouseau, we will need
// to flip this to true (or remove it entirely). This will then be used to
// determine if we should show the maintenance page that links to the SQS
// migration docs.
const sqsRequired = false
let sqsAvailable: boolean let sqsAvailable: boolean
async function isSqsAvailable() { async function isSqsAvailable() {
// We cache this value for the duration of the Node process because we don't
// want every page load to be making this relatively expensive check.
if (sqsAvailable !== undefined) { if (sqsAvailable !== undefined) {
return sqsAvailable return sqsAvailable
} }
@ -21,6 +29,10 @@ async function isSqsAvailable() {
} }
} }
async function isSqsMissing() {
return sqsRequired && !(await isSqsAvailable())
}
export const fetch = async (ctx: Ctx) => { export const fetch = async (ctx: Ctx) => {
ctx.body = { ctx.body = {
multiTenancy: !!env.MULTI_TENANCY, multiTenancy: !!env.MULTI_TENANCY,
@ -30,11 +42,12 @@ export const fetch = async (ctx: Ctx) => {
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL, disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
baseUrl: env.PLATFORM_URL, baseUrl: env.PLATFORM_URL,
isDev: env.isDev() && !env.isTest(), isDev: env.isDev() && !env.isTest(),
maintenance: [],
} }
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
ctx.body.infrastructure = { if (await isSqsMissing()) {
sqs: await isSqsAvailable(), ctx.body.maintenance.push({ type: MaintenanceType.SQS_MISSING })
} }
} }
} }

View File

@ -27,6 +27,7 @@ describe("/api/system/environment", () => {
multiTenancy: true, multiTenancy: true,
baseUrl: "http://localhost:10000", baseUrl: "http://localhost:10000",
offlineMode: false, offlineMode: false,
maintenance: [],
}) })
}) })
@ -40,9 +41,7 @@ describe("/api/system/environment", () => {
multiTenancy: true, multiTenancy: true,
baseUrl: "http://localhost:10000", baseUrl: "http://localhost:10000",
offlineMode: false, offlineMode: false,
infrastructure: { maintenance: [],
sqs: false,
},
}) })
}) })
}) })