Unpublish refactored to stop development applications being mistakenly deleted. Minor updates to the modal content component to allow the replacement of the header. Further work to implement the publishing workflow changes
This commit is contained in:
parent
31d2ae8c6b
commit
3e74118f81
|
@ -174,9 +174,11 @@ function getDB(key, opts) {
|
||||||
if (db && isEqual(opts, storedOpts)) {
|
if (db && isEqual(opts, storedOpts)) {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
const appId = exports.getAppId()
|
const appId = exports.getAppId()
|
||||||
const CouchDB = getCouch()
|
const CouchDB = getCouch()
|
||||||
let toUseAppId
|
let toUseAppId
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case ContextKeys.CURRENT_DB:
|
case ContextKeys.CURRENT_DB:
|
||||||
toUseAppId = appId
|
toUseAppId = appId
|
||||||
|
|
|
@ -72,12 +72,20 @@
|
||||||
class:header-spacing={$$slots.header}
|
class:header-spacing={$$slots.header}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
|
</h1>
|
||||||
|
{:else if $$slots.header}
|
||||||
|
<h1
|
||||||
|
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
||||||
|
class:noDivider={!showDivider}
|
||||||
|
class:header-spacing={$$slots.header}
|
||||||
|
>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
</h1>
|
</h1>
|
||||||
{#if showDivider}
|
|
||||||
<Divider size="M" />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showDivider && (title || $$slots.header)}
|
||||||
|
<Divider size="M" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
|
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
|
||||||
<section class="spectrum-Dialog-content content-grid">
|
<section class="spectrum-Dialog-content content-grid">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
export let align = "right"
|
export let align = "right"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
|
||||||
|
let clazz
|
||||||
|
export { clazz as class }
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
dispatch("open")
|
dispatch("open")
|
||||||
open = true
|
open = true
|
||||||
|
@ -37,7 +40,7 @@
|
||||||
use:positionDropdown={{ anchor, align }}
|
use:positionDropdown={{ anchor, align }}
|
||||||
use:clickOutside={hide}
|
use:clickOutside={hide}
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class="spectrum-Popover is-open"
|
class={"spectrum-Popover is-open " + clazz}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script>
|
||||||
|
import { setContext } from "svelte"
|
||||||
|
import Popover from "../Popover/Popover.svelte"
|
||||||
|
|
||||||
|
export let disabled = false
|
||||||
|
export let align = "left"
|
||||||
|
|
||||||
|
export let anchor
|
||||||
|
export let showTip = true
|
||||||
|
export let direction = "bottom"
|
||||||
|
|
||||||
|
let dropdown
|
||||||
|
let tipSvg =
|
||||||
|
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
|
||||||
|
|
||||||
|
// This is needed because display: contents is considered "invisible".
|
||||||
|
// It should only ever be an action button, so should be fine.
|
||||||
|
function getAnchor(node) {
|
||||||
|
if (!anchor) {
|
||||||
|
anchor = node.firstChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//need this for the publish/view behaviours
|
||||||
|
export const hide = () => {
|
||||||
|
dropdown.hide()
|
||||||
|
}
|
||||||
|
export const show = () => {
|
||||||
|
dropdown.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openMenu = event => {
|
||||||
|
if (!disabled) {
|
||||||
|
event.stopPropagation()
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext("popoverMenu", { show, hide })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="popover-menu">
|
||||||
|
<div use:getAnchor on:click={openMenu}>
|
||||||
|
<slot name="control" />
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={dropdown}
|
||||||
|
{anchor}
|
||||||
|
{align}
|
||||||
|
class={showTip
|
||||||
|
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
{#if showTip}
|
||||||
|
{@html tipSvg}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="popover-container">
|
||||||
|
<div class="popover-menu-wrap">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.spectrum-Popover.is-open.spectrum-Popover--withTip) {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
margin-left: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.popover-menu-wrap {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.popover-menu :global(.icon) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
:global(.spectrum-Popover--bottom .spectrum-Popover-tip) {
|
||||||
|
left: 90%;
|
||||||
|
margin-left: calc(var(--spectrum-global-dimension-size-150) * -1);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -25,6 +25,7 @@ export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||||
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
||||||
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover } from "./Popover/Popover.svelte"
|
||||||
|
export { default as PopoverMenu } from "./Popover/PopoverMenu.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||||
export { default as ProgressCircle } from "./ProgressCircle/ProgressCircle.svelte"
|
export { default as ProgressCircle } from "./ProgressCircle/ProgressCircle.svelte"
|
||||||
export { default as Label } from "./Label/Label.svelte"
|
export { default as Label } from "./Label/Label.svelte"
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
showConfirmButton={false}
|
showConfirmButton={false}
|
||||||
title="Test Automation"
|
|
||||||
cancelText="Close"
|
cancelText="Close"
|
||||||
>
|
>
|
||||||
<div slot="header">
|
<div slot="header" class="result-modal-header">
|
||||||
<div style="float: right;">
|
<span>Test Automation</span>
|
||||||
|
<div>
|
||||||
{#if isTrigger || testResult[0].outputs.success}
|
{#if isTrigger || testResult[0].outputs.success}
|
||||||
<div class="iconSuccess">
|
<div class="iconSuccess">
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
<Icon size="S" name="CheckmarkCircle" />
|
||||||
|
@ -89,6 +89,14 @@
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.result-modal-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.iconSuccess {
|
.iconSuccess {
|
||||||
color: var(--spectrum-global-color-green-600);
|
color: var(--spectrum-global-color-green-600);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,57 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
ModalContent,
|
||||||
|
Layout,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import { ProgressCircle } from "@budibase/bbui"
|
||||||
|
import CopyInput from "components/common/inputs/CopyInput.svelte"
|
||||||
|
|
||||||
let feedbackModal
|
let feedbackModal
|
||||||
let publishModal
|
let publishModal
|
||||||
|
let asyncModal
|
||||||
|
let publishCompleteModal
|
||||||
|
|
||||||
|
let published
|
||||||
|
|
||||||
|
$: publishedUrl = published ? `${window.origin}/app${published.appUrl}` : ""
|
||||||
|
|
||||||
|
export let onOk
|
||||||
|
|
||||||
async function deployApp() {
|
async function deployApp() {
|
||||||
try {
|
try {
|
||||||
await API.deployAppChanges()
|
published = await API.deployAppChanges()
|
||||||
|
|
||||||
|
//In Progress
|
||||||
|
asyncModal.show()
|
||||||
|
publishModal.hide()
|
||||||
|
|
||||||
analytics.captureEvent(Events.APP.PUBLISHED, {
|
analytics.captureEvent(Events.APP.PUBLISHED, {
|
||||||
appId: $store.appId,
|
appId: $store.appId,
|
||||||
})
|
})
|
||||||
notifications.success("Application published successfully")
|
if (typeof onOk === "function") {
|
||||||
|
await onOk()
|
||||||
|
}
|
||||||
|
|
||||||
|
//Request completed
|
||||||
|
asyncModal.hide()
|
||||||
|
publishCompleteModal.show()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
analytics.captureException(error)
|
analytics.captureException(error)
|
||||||
notifications.error("Error publishing app")
|
notifications.error("Error publishing app")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewApp = () => {
|
||||||
|
if (published) {
|
||||||
|
window.open(publishedUrl, "_blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button secondary on:click={publishModal.show}>Publish</Button>
|
<Button secondary on:click={publishModal.show}>Publish</Button>
|
||||||
|
@ -30,6 +63,7 @@
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={publishModal}>
|
<Modal bind:this={publishModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Publish to Production"
|
title="Publish to Production"
|
||||||
|
@ -42,3 +76,50 @@
|
||||||
>
|
>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</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 -->
|
||||||
|
<span class="publish-modal-wrap">
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
|
@ -7,12 +7,10 @@
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import {
|
||||||
const DeploymentStatus = {
|
checkIncomingDeploymentStatus,
|
||||||
SUCCESS: "SUCCESS",
|
DeploymentStatus,
|
||||||
PENDING: "PENDING",
|
} from "components/deploy/utils"
|
||||||
FAILURE: "FAILURE",
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATE_OPTIONS = {
|
const DATE_OPTIONS = {
|
||||||
fullDate: {
|
fullDate: {
|
||||||
|
@ -42,30 +40,17 @@
|
||||||
const formatDate = (date, format) =>
|
const formatDate = (date, format) =>
|
||||||
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
||||||
|
|
||||||
// Required to check any updated deployment statuses between polls
|
|
||||||
function checkIncomingDeploymentStatus(current, incoming) {
|
|
||||||
for (let incomingDeployment of incoming) {
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
showErrorReasonModal(incomingDeployment.err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchDeployments() {
|
async function fetchDeployments() {
|
||||||
try {
|
try {
|
||||||
const newDeployments = await API.getAppDeployments()
|
const newDeployments = await API.getAppDeployments()
|
||||||
if (deployments.length > 0) {
|
if (deployments.length > 0) {
|
||||||
checkIncomingDeploymentStatus(deployments, newDeployments)
|
const pendingDeployments = checkIncomingDeploymentStatus(
|
||||||
|
deployments,
|
||||||
|
newDeployments
|
||||||
|
)
|
||||||
|
if (pendingDeployments.length) {
|
||||||
|
showErrorReasonModal(incomingDeployment.err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
deployments = newDeployments
|
deployments = newDeployments
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
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;
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
Icon,
|
Icon,
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
ButtonGroup,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
export let editApp
|
export let editApp
|
||||||
export let updateApp
|
export let updateApp
|
||||||
export let deleteApp
|
export let deleteApp
|
||||||
|
export let previewApp
|
||||||
export let unpublishApp
|
export let unpublishApp
|
||||||
export let releaseLock
|
export let releaseLock
|
||||||
export let editIcon
|
export let editIcon
|
||||||
|
@ -57,19 +59,36 @@
|
||||||
</StatusLight>
|
</StatusLight>
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<StatusLight active={app.deployed} neutral={!app.deployed}>
|
<div class="app-status">
|
||||||
{#if app.deployed}Published{:else}Unpublished{/if}
|
{#if app.deployed}
|
||||||
</StatusLight>
|
<Icon name="Globe" disabled={false} />
|
||||||
|
Published
|
||||||
|
{:else}
|
||||||
|
<Icon name="GlobeStrike" disabled={true} />
|
||||||
|
<span class="disabled"> Unpublished </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-cy={`row_actions_${app.appId}`}>
|
<div data-cy={`row_actions_${app.appId}`}>
|
||||||
<Button
|
<div class="app-actions">
|
||||||
size="S"
|
{#if app.deployed}
|
||||||
disabled={app.lockedOther}
|
<Button size="S" secondary quiet on:click={() => viewApp(app)}
|
||||||
on:click={() => editApp(app)}
|
>View app
|
||||||
secondary
|
</Button>
|
||||||
>
|
{:else}
|
||||||
Open
|
<Button size="S" secondary quiet on:click={() => previewApp(app)}
|
||||||
</Button>
|
>Preview
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
size="S"
|
||||||
|
cta
|
||||||
|
disabled={app.lockedOther}
|
||||||
|
on:click={() => editApp(app)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<Icon hoverable slot="control" name="More" />
|
<Icon hoverable slot="control" name="More" />
|
||||||
{#if app.deployed}
|
{#if app.deployed}
|
||||||
|
@ -97,6 +116,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.app-actions {
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 75px 75px;
|
||||||
|
}
|
||||||
|
.app-status {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px 100px;
|
||||||
|
}
|
||||||
|
.app-status span.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
.name {
|
.name {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -1,22 +1,69 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore, allScreens } from "builderStore"
|
||||||
import { roles, flags } from "stores/backend"
|
import { roles, flags } from "stores/backend"
|
||||||
import { Icon, ActionGroup, Tabs, Tab, notifications } from "@budibase/bbui"
|
import {
|
||||||
|
Icon,
|
||||||
|
ActionGroup,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
notifications,
|
||||||
|
PopoverMenu,
|
||||||
|
Layout,
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import DeployModal from "components/deploy/DeployModal.svelte"
|
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin, apps } from "stores/portal"
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import UpgradeModal from "components/upgrade/UpgradeModal.svelte"
|
import UpgradeModal from "components/upgrade/UpgradeModal.svelte"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
// Get Package and set store
|
// Get Package and set store
|
||||||
let promise = getPackage()
|
let promise = getPackage()
|
||||||
|
let unpublishModal
|
||||||
|
let publishPopover
|
||||||
|
let notPublishedPopover
|
||||||
|
|
||||||
|
$: enrichedApps = enrichApps($apps, $auth.user)
|
||||||
|
const enrichApps = (apps, user) => {
|
||||||
|
const enrichedApps = apps
|
||||||
|
.map(app => ({
|
||||||
|
...app,
|
||||||
|
deployed: app.status === "published",
|
||||||
|
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
|
||||||
|
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
|
||||||
|
}))
|
||||||
|
.filter(app => {
|
||||||
|
return app.devId === application
|
||||||
|
})
|
||||||
|
|
||||||
|
return enrichedApps
|
||||||
|
}
|
||||||
|
|
||||||
|
$: selectedApp = enrichedApps.length > 0 ? enrichedApps[0] : {}
|
||||||
|
|
||||||
|
$: deployments = []
|
||||||
|
$: latestDeployments = deployments
|
||||||
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
|
$: console.log("Deployments ", deployments)
|
||||||
|
$: console.log("Latest Deployments ", latestDeployments)
|
||||||
|
|
||||||
|
$: isPublished =
|
||||||
|
selectedApp.deployed && latestDeployments && latestDeployments?.length
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
|
||||||
// Sync once when you load the app
|
// Sync once when you load the app
|
||||||
let hasSynced = false
|
let hasSynced = false
|
||||||
|
@ -24,10 +71,18 @@
|
||||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||||
)
|
)
|
||||||
|
|
||||||
function previewApp() {
|
const previewApp = () => {
|
||||||
window.open(`/${application}`)
|
window.open(`/${application}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewApp = () => {
|
||||||
|
if (selectedApp.url) {
|
||||||
|
window.open(`/app${selectedApp.url}`)
|
||||||
|
} else {
|
||||||
|
window.open(`/${selectedApp.prodId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getPackage() {
|
async function getPackage() {
|
||||||
try {
|
try {
|
||||||
const pkg = await API.fetchAppPackage(application)
|
const pkg = await API.fetchAppPackage(application)
|
||||||
|
@ -58,20 +113,71 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 history")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!hasSynced && application) {
|
if (!hasSynced && application) {
|
||||||
try {
|
try {
|
||||||
await API.syncApp(application)
|
await API.syncApp(application)
|
||||||
|
await apps.load()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Failed to sync with production database")
|
notifications.error("Failed to sync with production database")
|
||||||
}
|
}
|
||||||
hasSynced = true
|
hasSynced = true
|
||||||
}
|
}
|
||||||
|
deployments = await fetchDeployments()
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
store.actions.reset()
|
store.actions.reset()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const unpublishApp = () => {
|
||||||
|
publishPopover.hide()
|
||||||
|
unpublishModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const completePublish = async () => {
|
||||||
|
try {
|
||||||
|
await apps.load()
|
||||||
|
deployments = await fetchDeployments()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error refreshing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUnpublishApp = async () => {
|
||||||
|
if (!application || !isPublished) {
|
||||||
|
//confirm the app has loaded.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
|
await apps.load()
|
||||||
|
notifications.success("App unpublished successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error unpublishing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await promise}
|
{#await promise}
|
||||||
|
@ -112,23 +218,76 @@
|
||||||
<VersionModal />
|
<VersionModal />
|
||||||
<RevertModal />
|
<RevertModal />
|
||||||
<Icon name="Play" hoverable on:click={previewApp} />
|
<Icon name="Play" hoverable on:click={previewApp} />
|
||||||
<DeployModal />
|
|
||||||
|
<PopoverMenu
|
||||||
|
bind:this={publishPopover}
|
||||||
|
align="right"
|
||||||
|
disabled={!isPublished}
|
||||||
|
>
|
||||||
|
<div slot="control" class="icon">
|
||||||
|
<Icon
|
||||||
|
size="M"
|
||||||
|
hoverable
|
||||||
|
name={isPublished ? "Globe" : "GlobeStrike"}
|
||||||
|
disabled={!isPublished}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Layout gap="M">
|
||||||
|
<Heading size="XS">Your app is live!</Heading>
|
||||||
|
<div class="publish-popover-message">
|
||||||
|
{#if isPublished}
|
||||||
|
{processStringSync(
|
||||||
|
"Last Published: {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="publish-popover-actions">
|
||||||
|
<Button
|
||||||
|
warning={true}
|
||||||
|
icon="Globe"
|
||||||
|
disabled={!isPublished}
|
||||||
|
on:click={unpublishApp}
|
||||||
|
dataCy="publish-popover-action"
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</Button>
|
||||||
|
<Button cta on:click={viewApp}>View App</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</PopoverMenu>
|
||||||
|
|
||||||
|
<DeployModal onOk={completePublish} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p>Something went wrong: {error.message}</p>
|
<p>Something went wrong: {error.message}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.publish-popover-actions :global([data-cy="publish-popover-action"]) {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
.loading {
|
.loading {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -174,6 +174,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewApp = app => {
|
||||||
|
window.open(`/${app.devId}`)
|
||||||
|
}
|
||||||
|
|
||||||
const editApp = app => {
|
const editApp = app => {
|
||||||
if (app.lockedOther) {
|
if (app.lockedOther) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
|
@ -392,6 +396,7 @@
|
||||||
{exportApp}
|
{exportApp}
|
||||||
{deleteApp}
|
{deleteApp}
|
||||||
{updateApp}
|
{updateApp}
|
||||||
|
{previewApp}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -350,20 +350,26 @@ exports.revertClient = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.delete = async ctx => {
|
exports.delete = async ctx => {
|
||||||
const db = getAppDB()
|
let appId = ctx.params.appId
|
||||||
|
|
||||||
|
if (ctx.query.unpublish) {
|
||||||
|
appId = getProdAppID(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = ctx.query.unpublish ? getProdAppDB() : getAppDB();
|
||||||
const result = await db.destroy()
|
const result = await db.destroy()
|
||||||
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (!env.isTest() && !ctx.query.unpublish) {
|
if (!env.isTest() && !ctx.query.unpublish) {
|
||||||
await deleteApp(ctx.params.appId)
|
await deleteApp(appId)
|
||||||
}
|
}
|
||||||
if (ctx.query && ctx.query.unpublish) {
|
if (ctx.query && ctx.query.unpublish) {
|
||||||
await cleanupAutomations(ctx.params.appId)
|
await cleanupAutomations(appId)
|
||||||
}
|
}
|
||||||
// make sure the app/role doesn't stick around after the app has been deleted
|
// make sure the app/role doesn't stick around after the app has been deleted
|
||||||
await removeAppFromUserRoles(ctx, ctx.params.appId)
|
await removeAppFromUserRoles(ctx, appId)
|
||||||
await appCache.invalidateAppMetadata(ctx.params.appId)
|
await appCache.invalidateAppMetadata(appId)
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,9 @@ async function deployApp(deployment) {
|
||||||
console.log("replication complete.. replacing app meta doc")
|
console.log("replication complete.. replacing app meta doc")
|
||||||
const db = getProdAppDB()
|
const db = getProdAppDB()
|
||||||
const appDoc = await db.get(DocumentTypes.APP_METADATA)
|
const appDoc = await db.get(DocumentTypes.APP_METADATA)
|
||||||
|
|
||||||
|
deployment.appUrl = appDoc.url
|
||||||
|
|
||||||
appDoc.appId = productionAppId
|
appDoc.appId = productionAppId
|
||||||
appDoc.instance._id = productionAppId
|
appDoc.instance._id = productionAppId
|
||||||
await db.put(appDoc)
|
await db.put(appDoc)
|
||||||
|
|
Loading…
Reference in New Issue