Sync publish and unpublish events between all users
This commit is contained in:
parent
44efcee58e
commit
99bf0ca03b
|
@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
|||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { getUserStore } from "./store/users"
|
||||
import { getDeploymentStore } from "./store/deployments"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
|
|||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
export const userStore = getUserStore()
|
||||
export const deploymentStore = getDeploymentStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
|
@ -131,3 +133,7 @@ export const userSelectedResourceMap = derived(userStore, $userStore => {
|
|||
})
|
||||
return map
|
||||
})
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length === 1
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1402,6 +1402,15 @@ export const getFrontendStore = () => {
|
|||
})
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
replace: metadata => {
|
||||
console.log("NEW METADATA", metadata)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
...metadata,
|
||||
}))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return store
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import { userStore, store } from "builderStore"
|
||||
import { userStore, store, deploymentStore } from "builderStore"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { auth } from "stores/portal"
|
||||
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
||||
import { apps } from "stores/portal"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export const createBuilderWebsocket = appId => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
@ -31,7 +33,6 @@ export const createBuilderWebsocket = appId => {
|
|||
})
|
||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
notifications.success("You can now edit automations")
|
||||
store.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
|
@ -51,6 +52,20 @@ export const createBuilderWebsocket = appId => {
|
|||
socket.onOther(BuilderSocketEvent.ScreenChange, ({ 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
|
||||
}
|
||||
|
|
|
@ -18,11 +18,9 @@
|
|||
import { processStringSync } from "@budibase/string-templates"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import analytics, { Events, EventSource } from "analytics"
|
||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||
import { API } from "api"
|
||||
import { onMount } from "svelte"
|
||||
import { apps } from "stores/portal"
|
||||
import { store } from "builderStore"
|
||||
import { deploymentStore, store } from "builderStore"
|
||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
@ -34,37 +32,31 @@
|
|||
let updateAppModal
|
||||
let revertModal
|
||||
let versionModal
|
||||
|
||||
let appActionPopover
|
||||
let appActionPopoverOpen = false
|
||||
let appActionPopoverAnchor
|
||||
|
||||
let publishing = false
|
||||
|
||||
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||
|
||||
$: deployments = []
|
||||
$: latestDeployments = deployments
|
||||
$: latestDeployments = $deploymentStore
|
||||
.filter(deployment => deployment.status === "SUCCESS")
|
||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||
|
||||
$: isPublished =
|
||||
selectedApp?.status === "published" && latestDeployments?.length > 0
|
||||
|
||||
$: updateAvailable =
|
||||
$store.upgradableVersion &&
|
||||
$store.version &&
|
||||
$store.upgradableVersion !== $store.version
|
||||
|
||||
$: canPublish = !publishing && loaded
|
||||
$: lastDeployed = getLastDeployedString($deploymentStore)
|
||||
|
||||
const initialiseApp = async () => {
|
||||
const applicationPkg = await API.fetchAppPackage($store.devId)
|
||||
await store.actions.initialise(applicationPkg)
|
||||
}
|
||||
|
||||
const updateDeploymentString = () => {
|
||||
const getLastDeployedString = deployments => {
|
||||
return deployments?.length
|
||||
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||
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 = () => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
|
@ -116,14 +87,11 @@
|
|||
async function publishApp() {
|
||||
try {
|
||||
publishing = true
|
||||
|
||||
await API.publishAppChanges($store.appId)
|
||||
|
||||
notifications.send("App published", {
|
||||
type: "success",
|
||||
icon: "GlobeCheck",
|
||||
})
|
||||
|
||||
await completePublish()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -163,210 +131,191 @@
|
|||
const completePublish = async () => {
|
||||
try {
|
||||
await apps.load()
|
||||
deployments = await fetchDeployments()
|
||||
await deploymentStore.actions.load()
|
||||
} catch (err) {
|
||||
notifications.error("Error refreshing app")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!$apps.length) {
|
||||
await apps.load()
|
||||
}
|
||||
deployments = await fetchDeployments()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $store.hasLock}
|
||||
<div class="action-top-nav" class:has-lock={$store.hasLock}>
|
||||
<div class="action-buttons">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if updateAvailable}
|
||||
<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="action-top-nav">
|
||||
<div class="action-buttons">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if updateAvailable}
|
||||
<div class="app-action-button version" on:click={versionModal.show}>
|
||||
<div class="app-action">
|
||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||
Preview
|
||||
<ActionButton quiet>
|
||||
<StatusLight notice />
|
||||
Update
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="app-action-button publish app-action-popover"
|
||||
on:click={() => {
|
||||
if (!appActionPopoverOpen) {
|
||||
appActionPopover.show()
|
||||
} else {
|
||||
appActionPopover.hide()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div bind:this={appActionPopoverAnchor}>
|
||||
<div class="app-action">
|
||||
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||
<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>
|
||||
{/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>
|
||||
<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>
|
||||
</div>
|
||||
</TourWrap>
|
||||
|
||||
<Body size="S">
|
||||
<span class="publish-popover-status">
|
||||
{#if isPublished}
|
||||
<span class="status-text">
|
||||
{updateDeploymentString(deployments)}
|
||||
</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 class="app-action-button preview">
|
||||
<div class="app-action">
|
||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||
Preview
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="app-action-button publish app-action-popover"
|
||||
on:click={() => {
|
||||
if (!appActionPopoverOpen) {
|
||||
appActionPopover.show()
|
||||
} else {
|
||||
appActionPopover.hide()
|
||||
}
|
||||
}}
|
||||
onUpdateComplete={async () => {
|
||||
await initialiseApp()
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
>
|
||||
<div bind:this={appActionPopoverAnchor}>
|
||||
<div class="app-action">
|
||||
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||
<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} />
|
||||
<VersionModal hideIcon bind:this={versionModal} />
|
||||
{:else}
|
||||
<div class="app-action-button preview-locked">
|
||||
<div class="app-action">
|
||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||
Preview
|
||||
</ActionButton>
|
||||
<Body size="S">
|
||||
<span class="publish-popover-status">
|
||||
{#if isPublished}
|
||||
<span class="status-text">
|
||||
{lastDeployed}
|
||||
</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 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>
|
||||
{/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>
|
||||
.app-action-popover-content {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}, [])
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
<script>
|
||||
import { store, automationStore, userStore } from "builderStore"
|
||||
import {
|
||||
store,
|
||||
automationStore,
|
||||
userStore,
|
||||
deploymentStore,
|
||||
} from "builderStore"
|
||||
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 {
|
||||
Icon,
|
||||
|
@ -44,6 +49,8 @@
|
|||
await automationStore.actions.fetch()
|
||||
await roles.fetch()
|
||||
await flags.fetch()
|
||||
await apps.load()
|
||||
await deploymentStore.actions.load()
|
||||
loaded = true
|
||||
return pkg
|
||||
} catch (error) {
|
||||
|
@ -69,18 +76,13 @@
|
|||
|
||||
// Event handler for the command palette
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && $store.hasLock) {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
commandPaletteModal.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
const initTour = async () => {
|
||||
// Skip tour if we don't have the lock
|
||||
if (!$store.hasLock) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if onboarding is enabled.
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||
if (!$auth.user?.onboardedAt) {
|
||||
|
@ -140,7 +142,7 @@
|
|||
{/if}
|
||||
|
||||
<div class="root" class:blur={$store.showPreview}>
|
||||
<div class="top-nav" class:has-lock={$store.hasLock}>
|
||||
<div class="top-nav">
|
||||
{#if $store.initialised}
|
||||
<div class="topleftnav">
|
||||
<span class="back-to-apps">
|
||||
|
@ -169,7 +171,7 @@
|
|||
<Heading size="XS">{$store.name}</Heading>
|
||||
</div>
|
||||
<div class="toprightnav">
|
||||
<span class:nav-lock={!$store.hasLock}>
|
||||
<span>
|
||||
<UserAvatars users={$userStore} />
|
||||
</span>
|
||||
<AppActions {application} {loaded} />
|
||||
|
@ -236,10 +238,6 @@
|
|||
z-index: 2;
|
||||
}
|
||||
|
||||
.top-nav.has-lock {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.topcenternav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
}}
|
||||
disabled={appDeployed}
|
||||
tooltip={appDeployed
|
||||
? "You must unpublish your app to make changes to these settings"
|
||||
? "You must unpublish your app to make changes"
|
||||
: null}
|
||||
icon={appDeployed ? "HelpOutline" : null}
|
||||
>
|
||||
|
|
|
@ -53,6 +53,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||
import sdk from "../../sdk"
|
||||
import { builderSocket } from "../../websockets"
|
||||
|
||||
// utility function, need to do away with this
|
||||
async function getLayouts() {
|
||||
|
@ -439,6 +440,14 @@ export async function update(ctx: UserCtx) {
|
|||
await events.app.updated(app)
|
||||
ctx.status = 200
|
||||
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) {
|
||||
|
@ -569,6 +578,7 @@ export async function unpublish(ctx: UserCtx) {
|
|||
await unpublishApp(ctx)
|
||||
await postDestroyApp(ctx)
|
||||
ctx.status = 204
|
||||
builderSocket?.emitAppUnpublish(ctx)
|
||||
}
|
||||
|
||||
export async function sync(ctx: UserCtx) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { backups } from "@budibase/pro"
|
||||
import { AppBackupTrigger } from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import { builderSocket } from "../../../websockets"
|
||||
|
||||
// the max time we can wait for an invalidation to complete before considering it failed
|
||||
const MAX_PENDING_TIME_MS = 30 * 60000
|
||||
|
@ -201,4 +202,5 @@ export const publishApp = async function (ctx: any) {
|
|||
|
||||
await events.app.published(app)
|
||||
ctx.body = deployment
|
||||
builderSocket?.emitAppPublish(ctx)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
SocketSession,
|
||||
ContextUser,
|
||||
Screen,
|
||||
App,
|
||||
} from "@budibase/types"
|
||||
import { gridSocket } from "./index"
|
||||
import { clearLock, updateLock } from "../utilities/redis"
|
||||
|
@ -133,4 +134,24 @@ export default class BuilderSocket extends BaseSocket {
|
|||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ export enum BuilderSocketEvent {
|
|||
ScreenChange = "ScreenChange",
|
||||
AppMetadataChange = "AppMetadataChange",
|
||||
SelectResource = "SelectResource",
|
||||
AppPublishChange = "AppPublishChange",
|
||||
}
|
||||
|
||||
export const SocketSessionTTL = 60
|
||||
|
|
Loading…
Reference in New Issue