Sync publish and unpublish events between all users

This commit is contained in:
Andrew Kingston 2023-07-04 13:18:38 +01:00
parent 44efcee58e
commit 99bf0ca03b
14 changed files with 271 additions and 617 deletions

View File

@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
import { getTemporalStore } from "./store/temporal" import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { getUserStore } from "./store/users" import { getUserStore } from "./store/users"
import { getDeploymentStore } from "./store/deployments"
import { derived } from "svelte/store" import { derived } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
export const themeStore = getThemeStore() export const themeStore = getThemeStore()
export const temporalStore = getTemporalStore() export const temporalStore = getTemporalStore()
export const userStore = getUserStore() export const userStore = getUserStore()
export const deploymentStore = getDeploymentStore()
// Setup history for screens // Setup history for screens
export const screenHistoryStore = createHistoryStore({ export const screenHistoryStore = createHistoryStore({
@ -131,3 +133,7 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
}) })
return map return map
}) })
export const isOnlyUser = derived(userStore, $userStore => {
return $userStore.length === 1
})

View File

@ -0,0 +1,22 @@
import { writable } from "svelte/store"
import { API } from "api"
import { notifications } from "@budibase/bbui"
export const getDeploymentStore = () => {
let store = writable([])
const load = async () => {
try {
store.set(await API.getAppDeployments())
} catch (err) {
notifications.error("Error fetching deployments")
}
}
return {
subscribe: store.subscribe,
actions: {
load,
},
}
}

View File

@ -1402,6 +1402,15 @@ export const getFrontendStore = () => {
}) })
}, },
}, },
metadata: {
replace: metadata => {
console.log("NEW METADATA", metadata)
store.update(state => ({
...state,
...metadata,
}))
},
},
} }
return store return store

View File

@ -1,10 +1,12 @@
import { createWebsocket } from "@budibase/frontend-core" import { createWebsocket } from "@budibase/frontend-core"
import { userStore, store } from "builderStore" import { userStore, store, deploymentStore } from "builderStore"
import { datasources, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import { get } from "svelte/store" import { get } from "svelte/store"
import { auth } from "stores/portal" import { auth } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core" import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
import { apps } from "stores/portal"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export const createBuilderWebsocket = appId => { export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder") const socket = createWebsocket("/socket/builder")
@ -31,7 +33,6 @@ export const createBuilderWebsocket = appId => {
}) })
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => { socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
if (userId === get(auth)?.user?._id) { if (userId === get(auth)?.user?._id) {
notifications.success("You can now edit automations")
store.update(state => ({ store.update(state => ({
...state, ...state,
hasLock: true, hasLock: true,
@ -51,6 +52,20 @@ export const createBuilderWebsocket = appId => {
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => { socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
store.actions.screens.replace(id, screen) store.actions.screens.replace(id, screen)
}) })
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
store.actions.metadata.replace(metadata)
})
socket.onOther(
BuilderSocketEvent.AppPublishChange,
async ({ user, published }) => {
await apps.load()
if (published) {
await deploymentStore.actions.load()
}
const verb = published ? "published" : "unpublished"
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
}
)
return socket return socket
} }

View File

@ -18,11 +18,9 @@
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import analytics, { Events, EventSource } from "analytics" import analytics, { Events, EventSource } from "analytics"
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
import { API } from "api" import { API } from "api"
import { onMount } from "svelte"
import { apps } from "stores/portal" import { apps } from "stores/portal"
import { store } from "builderStore" import { deploymentStore, store } from "builderStore"
import TourWrap from "components/portal/onboarding/TourWrap.svelte" import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
@ -34,37 +32,31 @@
let updateAppModal let updateAppModal
let revertModal let revertModal
let versionModal let versionModal
let appActionPopover let appActionPopover
let appActionPopoverOpen = false let appActionPopoverOpen = false
let appActionPopoverAnchor let appActionPopoverAnchor
let publishing = false let publishing = false
$: filteredApps = $apps.filter(app => app.devId === application) $: filteredApps = $apps.filter(app => app.devId === application)
$: selectedApp = filteredApps?.length ? filteredApps[0] : null $: selectedApp = filteredApps?.length ? filteredApps[0] : null
$: latestDeployments = $deploymentStore
$: deployments = []
$: latestDeployments = deployments
.filter(deployment => deployment.status === "SUCCESS") .filter(deployment => deployment.status === "SUCCESS")
.sort((a, b) => a.updatedAt > b.updatedAt) .sort((a, b) => a.updatedAt > b.updatedAt)
$: isPublished = $: isPublished =
selectedApp?.status === "published" && latestDeployments?.length > 0 selectedApp?.status === "published" && latestDeployments?.length > 0
$: updateAvailable = $: updateAvailable =
$store.upgradableVersion && $store.upgradableVersion &&
$store.version && $store.version &&
$store.upgradableVersion !== $store.version $store.upgradableVersion !== $store.version
$: canPublish = !publishing && loaded $: canPublish = !publishing && loaded
$: lastDeployed = getLastDeployedString($deploymentStore)
const initialiseApp = async () => { const initialiseApp = async () => {
const applicationPkg = await API.fetchAppPackage($store.devId) const applicationPkg = await API.fetchAppPackage($store.devId)
await store.actions.initialise(applicationPkg) await store.actions.initialise(applicationPkg)
} }
const updateDeploymentString = () => { const getLastDeployedString = deployments => {
return deployments?.length return deployments?.length
? processStringSync("Published {{ duration time 'millisecond' }} ago", { ? processStringSync("Published {{ duration time 'millisecond' }} ago", {
time: time:
@ -73,27 +65,6 @@
: "" : ""
} }
const reviewPendingDeployments = (deployments, newDeployments) => {
if (deployments.length > 0) {
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
if (pending.length) {
notifications.warning(
"Deployment has been queued and will be processed shortly"
)
}
}
}
async function fetchDeployments() {
try {
const newDeployments = await API.getAppDeployments()
reviewPendingDeployments(deployments, newDeployments)
return newDeployments
} catch (err) {
notifications.error("Error fetching deployment overview")
}
}
const previewApp = () => { const previewApp = () => {
store.update(state => ({ store.update(state => ({
...state, ...state,
@ -116,14 +87,11 @@
async function publishApp() { async function publishApp() {
try { try {
publishing = true publishing = true
await API.publishAppChanges($store.appId) await API.publishAppChanges($store.appId)
notifications.send("App published", { notifications.send("App published", {
type: "success", type: "success",
icon: "GlobeCheck", icon: "GlobeCheck",
}) })
await completePublish() await completePublish()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -163,210 +131,191 @@
const completePublish = async () => { const completePublish = async () => {
try { try {
await apps.load() await apps.load()
deployments = await fetchDeployments() await deploymentStore.actions.load()
} catch (err) { } catch (err) {
notifications.error("Error refreshing app") notifications.error("Error refreshing app")
} }
} }
onMount(async () => {
if (!$apps.length) {
await apps.load()
}
deployments = await fetchDeployments()
})
</script> </script>
{#if $store.hasLock} <div class="action-top-nav">
<div class="action-top-nav" class:has-lock={$store.hasLock}> <div class="action-buttons">
<div class="action-buttons"> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-click-events-have-key-events --> {#if updateAvailable}
{#if updateAvailable} <div class="app-action-button version" on:click={versionModal.show}>
<div class="app-action-button version" on:click={versionModal.show}>
<div class="app-action">
<ActionButton quiet>
<StatusLight notice />
Update
</ActionButton>
</div>
</div>
{/if}
<TourWrap
tourStepKey={$store.onboarding
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
>
<div class="app-action-button users">
<div class="app-action" id="builder-app-users-button">
<ActionButton
quiet
icon="UserGroup"
on:click={() => {
store.update(state => {
state.builderSidePanel = true
return state
})
}}
>
Users
</ActionButton>
</div>
</div>
</TourWrap>
<div class="app-action-button preview">
<div class="app-action"> <div class="app-action">
<ActionButton quiet icon="PlayCircle" on:click={previewApp}> <ActionButton quiet>
Preview <StatusLight notice />
Update
</ActionButton> </ActionButton>
</div> </div>
</div> </div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events --> <TourWrap
<div tourStepKey={$store.onboarding
class="app-action-button publish app-action-popover" ? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
on:click={() => { : TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
if (!appActionPopoverOpen) { >
appActionPopover.show() <div class="app-action-button users">
} else { <div class="app-action" id="builder-app-users-button">
appActionPopover.hide() <ActionButton
} quiet
}} icon="UserGroup"
> on:click={() => {
<div bind:this={appActionPopoverAnchor}> store.update(state => {
<div class="app-action"> state.builderSidePanel = true
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} /> return state
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}> })
<span class="publish-open" id="builder-app-publish-button"> }}
Publish >
<Icon Users
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"} </ActionButton>
size="M"
/>
</span>
</TourWrap>
</div>
</div> </div>
<Popover </div>
bind:this={appActionPopover} </TourWrap>
align="right"
disabled={!isPublished}
anchor={appActionPopoverAnchor}
offset={35}
on:close={() => {
appActionPopoverOpen = false
}}
on:open={() => {
appActionPopoverOpen = true
}}
>
<div class="app-action-popover-content">
<Layout noPadding gap="M">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Body size="M">
<span
class="app-link"
on:click={() => {
if (isPublished) {
viewApp()
} else {
appActionPopover.hide()
updateAppModal.show()
}
}}
>
{$store.url}
{#if isPublished}
<Icon size="S" name="LinkOut" />
{:else}
<Icon size="S" name="Edit" />
{/if}
</span>
</Body>
<Body size="S"> <div class="app-action-button preview">
<span class="publish-popover-status"> <div class="app-action">
{#if isPublished} <ActionButton quiet icon="PlayCircle" on:click={previewApp}>
<span class="status-text"> Preview
{updateDeploymentString(deployments)} </ActionButton>
</span>
<span class="unpublish-link">
<Link quiet on:click={unpublishApp}>Unpublish</Link>
</span>
<span class="revert-link">
<Link quiet secondary on:click={revertApp}>Revert</Link>
</span>
{:else}
<span class="status-text unpublished">Not published</span>
{/if}
</span>
</Body>
<div class="action-buttons">
{#if $store.hasLock}
{#if isPublished}
<ActionButton
quiet
icon="Code"
on:click={() => {
$goto("./settings/embed")
appActionPopover.hide()
}}
>
Embed
</ActionButton>
{/if}
<Button
cta
on:click={publishApp}
id={"builder-app-publish-button"}
disabled={!canPublish}
>
Publish
</Button>
{/if}
</div>
</Layout>
</div>
</Popover>
</div> </div>
</div> </div>
</div>
<!-- Modals --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<ConfirmDialog <div
bind:this={unpublishModal} class="app-action-button publish app-action-popover"
title="Confirm unpublish" on:click={() => {
okText="Unpublish app" if (!appActionPopoverOpen) {
onOk={confirmUnpublishApp} appActionPopover.show()
> } else {
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>? appActionPopover.hide()
</ConfirmDialog> }
<Modal bind:this={updateAppModal} padding={false} width="600px">
<UpdateAppModal
app={{
name: $store.name,
url: $store.url,
icon: $store.icon,
appId: $store.appId,
}} }}
onUpdateComplete={async () => { >
await initialiseApp() <div bind:this={appActionPopoverAnchor}>
}} <div class="app-action">
/> <Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
</Modal> <TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
<span class="publish-open" id="builder-app-publish-button">
Publish
<Icon
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
size="M"
/>
</span>
</TourWrap>
</div>
</div>
<Popover
bind:this={appActionPopover}
align="right"
disabled={!isPublished}
anchor={appActionPopoverAnchor}
offset={35}
on:close={() => {
appActionPopoverOpen = false
}}
on:open={() => {
appActionPopoverOpen = true
}}
>
<div class="app-action-popover-content">
<Layout noPadding gap="M">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<Body size="M">
<span
class="app-link"
on:click={() => {
if (isPublished) {
viewApp()
} else {
appActionPopover.hide()
updateAppModal.show()
}
}}
>
{$store.url}
{#if isPublished}
<Icon size="S" name="LinkOut" />
{:else}
<Icon size="S" name="Edit" />
{/if}
</span>
</Body>
<RevertModal bind:this={revertModal} /> <Body size="S">
<VersionModal hideIcon bind:this={versionModal} /> <span class="publish-popover-status">
{:else} {#if isPublished}
<div class="app-action-button preview-locked"> <span class="status-text">
<div class="app-action"> {lastDeployed}
<ActionButton quiet icon="PlayCircle" on:click={previewApp}> </span>
Preview <span class="unpublish-link">
</ActionButton> <Link quiet on:click={unpublishApp}>Unpublish</Link>
</span>
<span class="revert-link">
<Link quiet secondary on:click={revertApp}>Revert</Link>
</span>
{:else}
<span class="status-text unpublished">Not published</span>
{/if}
</span>
</Body>
<div class="action-buttons">
{#if isPublished}
<ActionButton
quiet
icon="Code"
on:click={() => {
$goto("./settings/embed")
appActionPopover.hide()
}}
>
Embed
</ActionButton>
{/if}
<Button
cta
on:click={publishApp}
id={"builder-app-publish-button"}
disabled={!canPublish}
>
Publish
</Button>
</div>
</Layout>
</div>
</Popover>
</div> </div>
</div> </div>
{/if} </div>
<!-- Modals -->
<ConfirmDialog
bind:this={unpublishModal}
title="Confirm unpublish"
okText="Unpublish app"
onOk={confirmUnpublishApp}
>
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
</ConfirmDialog>
<Modal bind:this={updateAppModal} padding={false} width="600px">
<UpdateAppModal
app={{
name: $store.name,
url: $store.url,
icon: $store.icon,
appId: $store.appId,
}}
onUpdateComplete={async () => {
await initialiseApp()
}}
/>
</Modal>
<RevertModal bind:this={revertModal} />
<VersionModal hideIcon bind:this={versionModal} />
<style> <style>
.app-action-popover-content { .app-action-popover-content {

View File

@ -1,118 +0,0 @@
<script>
import {
Button,
Modal,
notifications,
ModalContent,
Layout,
ProgressCircle,
CopyInput,
} from "@budibase/bbui"
import { API } from "api"
import analytics, { Events, EventSource } from "analytics"
import { store } from "builderStore"
import TourWrap from "../portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "../portal/onboarding/tours.js"
let publishModal
let asyncModal
let publishCompleteModal
let published
$: publishedUrl = published ? `${window.origin}/app${published.appUrl}` : ""
export let onOk
async function publishApp() {
try {
//In Progress
asyncModal.show()
publishModal.hide()
published = await API.publishAppChanges($store.appId)
if (typeof onOk === "function") {
await onOk()
}
//Request completed
asyncModal.hide()
publishCompleteModal.show()
} catch (error) {
analytics.captureException(error)
notifications.error("Error publishing app")
}
}
const viewApp = () => {
if (published) {
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
appId: $store.appId,
eventSource: EventSource.PORTAL,
})
window.open(publishedUrl, "_blank")
}
}
</script>
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
<Button cta on:click={publishModal.show} id={"builder-app-publish-button"}>
Publish
</Button>
</TourWrap>
<Modal bind:this={publishModal}>
<ModalContent
title="Publish to production"
confirmText="Publish"
onConfirm={publishApp}
>
The changes you have made will be published to the production version of the
application.
</ModalContent>
</Modal>
<!-- Publish in progress -->
<Modal bind:this={asyncModal}>
<ModalContent
showCancelButton={false}
showConfirmButton={false}
showCloseIcon={false}
>
<Layout justifyItems="center">
<ProgressCircle size="XL" />
</Layout>
</ModalContent>
</Modal>
<!-- Publish complete -->
<Modal bind:this={publishCompleteModal}>
<ModalContent confirmText="Done" cancelText="View App" onCancel={viewApp}>
<div slot="header" class="app-published-header">
<svg
width="26px"
height="26px"
class="spectrum-Icon success-icon"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-GlobeCheck" />
</svg>
<span class="app-published-header-text">App Published!</span>
</div>
<CopyInput value={publishedUrl} label="You can view your app at:" />
</ModalContent>
</Modal>
<style>
.app-published-header {
display: flex;
flex-direction: row;
align-items: center;
}
.success-icon {
color: var(--spectrum-global-color-green-600);
}
.app-published-header .app-published-header-text {
padding-left: var(--spacing-l);
}
</style>

View File

@ -1,236 +0,0 @@
<script>
import { onMount, onDestroy } from "svelte"
import Spinner from "components/common/Spinner.svelte"
import { slide } from "svelte/transition"
import { Heading, Button, Modal, ModalContent } from "@budibase/bbui"
import { API } from "api"
import { notifications } from "@budibase/bbui"
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
import { store } from "builderStore"
import {
checkIncomingDeploymentStatus,
DeploymentStatus,
} from "components/deploy/utils"
const DATE_OPTIONS = {
fullDate: {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
},
timeOnly: {
hour: "numeric",
minute: "numeric",
hourCycle: "h12",
},
}
const POLL_INTERVAL = 5000
export let appId
let modal
let errorReasonModal
let errorReason
let poll
let deployments = []
let urlComponent = $store.url || `/${appId}`
let deploymentUrl = `${urlComponent}`
const formatDate = (date, format) =>
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
async function fetchDeployments() {
try {
const newDeployments = await API.getAppDeployments()
if (deployments.length > 0) {
const pendingDeployments = checkIncomingDeploymentStatus(
deployments,
newDeployments
)
if (pendingDeployments.length) {
showErrorReasonModal(pendingDeployments[0].err)
}
}
deployments = newDeployments
} catch (err) {
clearInterval(poll)
notifications.error("Error fetching deployment overview")
}
}
function showErrorReasonModal(err) {
if (!err) return
errorReason = err
errorReasonModal.show()
}
onMount(() => {
fetchDeployments()
poll = setInterval(fetchDeployments, POLL_INTERVAL)
})
onDestroy(() => clearInterval(poll))
</script>
{#if deployments.length > 0}
<section class="deployment-history" in:slide>
<header>
<Heading>Deployment History</Heading>
<div class="deploy-div">
{#if deployments.some(deployment => deployment.status === DeploymentStatus.SUCCESS)}
<a target="_blank" href={deploymentUrl}> View Your Deployed App </a>
<Button primary on:click={() => modal.show()}>View webhooks</Button>
{/if}
</div>
</header>
<div class="deployment-list">
{#each deployments as deployment}
<article class="deployment">
<div class="deployment-info">
<span class="deploy-date">
{formatDate(deployment.updatedAt, "fullDate")}
</span>
<span class="deploy-time">
{formatDate(deployment.updatedAt, "timeOnly")}
</span>
</div>
<div class="deployment-right">
{#if deployment.status.toLowerCase() === "pending"}
<Spinner size="10" />
{/if}
<div
on:click={() => showErrorReasonModal(deployment.err)}
class={`deployment-status ${deployment.status}`}
>
<span>
{deployment.status}
{#if deployment.status === DeploymentStatus.FAILURE}
<i class="ri-information-line" />
{/if}
</span>
</div>
</div>
</article>
{/each}
</div>
</section>
{/if}
<Modal bind:this={modal} width="30%">
<CreateWebhookDeploymentModal />
</Modal>
<Modal bind:this={errorReasonModal} width="30%">
<ModalContent
title="Deployment Error"
confirmText="OK"
showCancelButton={false}
>
{errorReason}
</ModalContent>
</Modal>
<style>
section {
padding: var(--spacing-xl) 0;
}
.deployment-list {
height: 40vh;
overflow-y: auto;
}
header {
padding-left: var(--spacing-l);
padding-bottom: var(--spacing-xl);
padding-right: var(--spacing-l);
border-bottom: var(--border-light);
}
.deploy-div {
display: flex;
justify-content: space-between;
align-items: center;
}
.deployment-history {
position: absolute;
bottom: 0;
width: 100%;
background: var(--background);
}
.deployment {
padding: var(--spacing-l);
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: var(--border-light);
}
.deployment:last-child {
border-bottom: none;
}
.deployment-info {
display: flex;
flex-direction: column;
margin-right: var(--spacing-s);
}
.deploy-date {
font-size: var(--font-size-m);
}
.deploy-time {
color: var(--grey-7);
font-weight: 600;
font-size: var(--font-size-s);
}
.deployment-right {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
}
.deployment-status {
font-size: var(--font-size-s);
padding: var(--spacing-s);
border-radius: var(--border-radius-s);
font-weight: 600;
text-transform: lowercase;
width: 80px;
text-align: center;
}
.deployment-status:first-letter {
text-transform: uppercase;
}
a {
color: var(--blue);
font-weight: 600;
font-size: var(--font-size-s);
}
.SUCCESS {
color: var(--green);
background: var(--green-light);
}
.PENDING {
color: var(--yellow);
background: var(--yellow-light);
}
.FAILURE {
color: var(--red);
background: var(--red-light);
cursor: pointer;
}
i {
position: relative;
top: 2px;
}
</style>

View File

@ -1,25 +0,0 @@
export const DeploymentStatus = {
SUCCESS: "SUCCESS",
PENDING: "PENDING",
FAILURE: "FAILURE",
}
// Required to check any updated deployment statuses between polls
export function checkIncomingDeploymentStatus(current, incoming) {
return incoming.reduce((acc, incomingDeployment) => {
if (incomingDeployment.status === DeploymentStatus.FAILURE) {
const currentDeployment = current.find(
deployment => deployment._id === incomingDeployment._id
)
//We have just been notified of an ongoing deployments failure
if (
!currentDeployment ||
currentDeployment.status === DeploymentStatus.PENDING
) {
acc.push(incomingDeployment)
}
}
return acc
}, [])
}

View File

@ -1,7 +1,12 @@
<script> <script>
import { store, automationStore, userStore } from "builderStore" import {
store,
automationStore,
userStore,
deploymentStore,
} from "builderStore"
import { roles, flags } from "stores/backend" import { roles, flags } from "stores/backend"
import { auth } from "stores/portal" import { auth, apps } from "stores/portal"
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags" import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
import { import {
Icon, Icon,
@ -44,6 +49,8 @@
await automationStore.actions.fetch() await automationStore.actions.fetch()
await roles.fetch() await roles.fetch()
await flags.fetch() await flags.fetch()
await apps.load()
await deploymentStore.actions.load()
loaded = true loaded = true
return pkg return pkg
} catch (error) { } catch (error) {
@ -69,18 +76,13 @@
// Event handler for the command palette // Event handler for the command palette
const handleKeyDown = e => { const handleKeyDown = e => {
if (e.key === "k" && (e.ctrlKey || e.metaKey) && $store.hasLock) { if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
e.preventDefault() e.preventDefault()
commandPaletteModal.toggle() commandPaletteModal.toggle()
} }
} }
const initTour = async () => { const initTour = async () => {
// Skip tour if we don't have the lock
if (!$store.hasLock) {
return
}
// Check if onboarding is enabled. // Check if onboarding is enabled.
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) { if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
if (!$auth.user?.onboardedAt) { if (!$auth.user?.onboardedAt) {
@ -140,7 +142,7 @@
{/if} {/if}
<div class="root" class:blur={$store.showPreview}> <div class="root" class:blur={$store.showPreview}>
<div class="top-nav" class:has-lock={$store.hasLock}> <div class="top-nav">
{#if $store.initialised} {#if $store.initialised}
<div class="topleftnav"> <div class="topleftnav">
<span class="back-to-apps"> <span class="back-to-apps">
@ -169,7 +171,7 @@
<Heading size="XS">{$store.name}</Heading> <Heading size="XS">{$store.name}</Heading>
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<span class:nav-lock={!$store.hasLock}> <span>
<UserAvatars users={$userStore} /> <UserAvatars users={$userStore} />
</span> </span>
<AppActions {application} {loaded} /> <AppActions {application} {loaded} />
@ -236,10 +238,6 @@
z-index: 2; z-index: 2;
} }
.top-nav.has-lock {
padding-right: 0px;
}
.topcenternav { .topcenternav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -63,7 +63,7 @@
}} }}
disabled={appDeployed} disabled={appDeployed}
tooltip={appDeployed tooltip={appDeployed
? "You must unpublish your app to make changes to these settings" ? "You must unpublish your app to make changes"
: null} : null}
icon={appDeployed ? "HelpOutline" : null} icon={appDeployed ? "HelpOutline" : null}
> >

View File

@ -53,6 +53,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets"
// utility function, need to do away with this // utility function, need to do away with this
async function getLayouts() { async function getLayouts() {
@ -439,6 +440,14 @@ export async function update(ctx: UserCtx) {
await events.app.updated(app) await events.app.updated(app)
ctx.status = 200 ctx.status = 200
ctx.body = app ctx.body = app
builderSocket?.emitAppMetadataUpdate(ctx, {
theme: app.theme,
customTheme: app.customTheme,
navigation: app.navigation,
name: app.name,
url: app.url,
icon: app.icon,
})
} }
export async function updateClient(ctx: UserCtx) { export async function updateClient(ctx: UserCtx) {
@ -569,6 +578,7 @@ export async function unpublish(ctx: UserCtx) {
await unpublishApp(ctx) await unpublishApp(ctx)
await postDestroyApp(ctx) await postDestroyApp(ctx)
ctx.status = 204 ctx.status = 204
builderSocket?.emitAppUnpublish(ctx)
} }
export async function sync(ctx: UserCtx) { export async function sync(ctx: UserCtx) {

View File

@ -9,6 +9,7 @@ import {
import { backups } from "@budibase/pro" import { backups } from "@budibase/pro"
import { AppBackupTrigger } from "@budibase/types" import { AppBackupTrigger } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { builderSocket } from "../../../websockets"
// the max time we can wait for an invalidation to complete before considering it failed // the max time we can wait for an invalidation to complete before considering it failed
const MAX_PENDING_TIME_MS = 30 * 60000 const MAX_PENDING_TIME_MS = 30 * 60000
@ -201,4 +202,5 @@ export const publishApp = async function (ctx: any) {
await events.app.published(app) await events.app.published(app)
ctx.body = deployment ctx.body = deployment
builderSocket?.emitAppPublish(ctx)
} }

View File

@ -9,6 +9,7 @@ import {
SocketSession, SocketSession,
ContextUser, ContextUser,
Screen, Screen,
App,
} from "@budibase/types" } from "@budibase/types"
import { gridSocket } from "./index" import { gridSocket } from "./index"
import { clearLock, updateLock } from "../utilities/redis" import { clearLock, updateLock } from "../utilities/redis"
@ -133,4 +134,24 @@ export default class BuilderSocket extends BaseSocket {
screen: null, screen: null,
}) })
} }
emitAppMetadataUpdate(ctx: any, metadata: Partial<App>) {
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppMetadataChange, {
metadata,
})
}
emitAppPublish(ctx: any) {
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppPublishChange, {
published: true,
user: ctx.user,
})
}
emitAppUnpublish(ctx: any) {
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.AppPublishChange, {
published: false,
user: ctx.user,
})
}
} }

View File

@ -89,6 +89,7 @@ export enum BuilderSocketEvent {
ScreenChange = "ScreenChange", ScreenChange = "ScreenChange",
AppMetadataChange = "AppMetadataChange", AppMetadataChange = "AppMetadataChange",
SelectResource = "SelectResource", SelectResource = "SelectResource",
AppPublishChange = "AppPublishChange",
} }
export const SocketSessionTTL = 60 export const SocketSessionTTL = 60