Merge pull request #5489 from Budibase/feature/new-app-publish-workflow
Feature/new app publish workflow
This commit is contained in:
commit
89fa229140
|
@ -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
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let align = "left"
|
export let align = "left"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let dropdown
|
let dropdown
|
||||||
|
@ -36,7 +37,7 @@
|
||||||
<div use:getAnchor on:click={openMenu}>
|
<div use:getAnchor on:click={openMenu}>
|
||||||
<slot name="control" />
|
<slot name="control" />
|
||||||
</div>
|
</div>
|
||||||
<Popover bind:this={dropdown} {anchor} {align} {portalTarget}>
|
<Popover bind:this={dropdown} {anchor} {align} {portalTarget} {dataCy}>
|
||||||
<Menu>
|
<Menu>
|
||||||
<slot />
|
<slot />
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
|
||||||
export let direction = "n"
|
export let direction = "n"
|
||||||
export let name = "Add"
|
export let name = "Add"
|
||||||
export let hidden = false
|
export let hidden = false
|
||||||
|
@ -10,14 +13,24 @@
|
||||||
export let hoverable = false
|
export let hoverable = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let color
|
export let color
|
||||||
|
export let tooltip
|
||||||
|
|
||||||
$: rotation = getRotation(direction)
|
$: rotation = getRotation(direction)
|
||||||
|
|
||||||
|
let showTooltip = false
|
||||||
|
|
||||||
const getRotation = direction => {
|
const getRotation = direction => {
|
||||||
return directions.indexOf(direction) * 45
|
return directions.indexOf(direction) * 45
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
on:mouseover={() => (showTooltip = true)}
|
||||||
|
on:focus={() => (showTooltip = true)}
|
||||||
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
on:click={() => (showTooltip = false)}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
on:click
|
on:click
|
||||||
class:hoverable
|
class:hoverable
|
||||||
|
@ -30,10 +43,22 @@
|
||||||
color ? `color: ${color};` : ""
|
color ? `color: ${color};` : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-{name}" />
|
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-{name}" />
|
||||||
</svg>
|
</svg>
|
||||||
|
{#if tooltip && showTooltip}
|
||||||
|
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||||
|
<Tooltip textWrapping direction={"bottom"} text={tooltip} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.icon {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
svg.hoverable {
|
svg.hoverable {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
|
@ -47,4 +72,15 @@
|
||||||
color: var(--spectrum-global-color-gray-500) !important;
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
left: 50%;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 150px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
export let secondaryButtonText = undefined
|
export let secondaryButtonText = undefined
|
||||||
export let secondaryAction = undefined
|
export let secondaryAction = undefined
|
||||||
export let secondaryButtonWarning = false
|
export let secondaryButtonWarning = false
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
const { hide, cancel } = getContext(Context.Modal)
|
const { hide, cancel } = getContext(Context.Modal)
|
||||||
let loading = false
|
let loading = false
|
||||||
|
@ -63,21 +64,26 @@
|
||||||
role="dialog"
|
role="dialog"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
<div class="spectrum-Dialog-grid">
|
<div class="spectrum-Dialog-grid">
|
||||||
{#if title}
|
{#if title || $$slots.header}
|
||||||
<h1
|
<h1
|
||||||
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
||||||
class:noDivider={!showDivider}
|
class:noDivider={!showDivider}
|
||||||
class:header-spacing={$$slots.header}
|
class:header-spacing={$$slots.header}
|
||||||
>
|
>
|
||||||
|
{#if title}
|
||||||
{title}
|
{title}
|
||||||
|
{:else if $$slots.header}
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
|
{/if}
|
||||||
</h1>
|
</h1>
|
||||||
{#if showDivider}
|
{#if showDivider}
|
||||||
<Divider size="M" />
|
<Divider size="M" />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/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 />
|
||||||
|
|
|
@ -10,6 +10,17 @@
|
||||||
export let anchor
|
export let anchor
|
||||||
export let align = "right"
|
export let align = "right"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
|
export let direction = "bottom"
|
||||||
|
export let showTip = false
|
||||||
|
|
||||||
|
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>'
|
||||||
|
|
||||||
|
$: tooltipClasses = showTip
|
||||||
|
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
|
||||||
|
: ""
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
dispatch("open")
|
dispatch("open")
|
||||||
|
@ -37,9 +48,14 @@
|
||||||
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 " + (tooltipClasses || "")}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
|
{#if showTip}
|
||||||
|
{@html tipSvg}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
@ -49,4 +65,13 @@
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
min-width: var(--spectrum-global-dimension-size-2000) !important;
|
min-width: var(--spectrum-global-dimension-size-2000) !important;
|
||||||
}
|
}
|
||||||
|
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
margin-left: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
:global(.spectrum-Popover--bottom .spectrum-Popover-tip),
|
||||||
|
:global(.spectrum-Popover--top .spectrum-Popover-tip) {
|
||||||
|
left: 90%;
|
||||||
|
margin-left: calc(var(--spectrum-global-dimension-size-150) * -1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(['all'], () => {
|
||||||
|
context("Publish Application Workflow", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the unpublished status correctly", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Unpublished")
|
||||||
|
cy.get("svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Preview")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should publish an application and correctly reflect that", () => {
|
||||||
|
//Assuming the previous test was run and the unpublished app is open in edit mode.
|
||||||
|
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.wait(1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
//Verify that the app url is presented correctly to the user
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
let appUrl = Cypress.config().baseUrl + '/app/cypress-tests'
|
||||||
|
cy.get("[data-cy='deployed-app-url'] input").should('have.value', appUrl)
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Published")
|
||||||
|
cy.get("svg[aria-label='Globe']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("View app")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist").click({ force: true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get("[data-cy='publish-popover-action']").should("exist")
|
||||||
|
cy.get("button").contains("View app").should("exist")
|
||||||
|
cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should unpublish an application from the top navigation and reflect the status change", () => {
|
||||||
|
//Assuming the previous test app exists and is published
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Published")
|
||||||
|
cy.get("svg[aria-label='Globe']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("View app")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
//The published status
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist")
|
||||||
|
.click({ force: true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
|
cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']")
|
||||||
|
.click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='unpublish-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".confirm-wrap button").click({ force: true }
|
||||||
|
)})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0).contains("Unpublished")
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -11,7 +11,7 @@ filterTests(['all'], () => {
|
||||||
cy.applicationInAppTable("Cypress Tests")
|
cy.applicationInAppTable("Cypress Tests")
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get(".app-row-actions-icon").eq(0).click()
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Menu").contains("Edit icon").click()
|
cy.get(".spectrum-Menu").contains("Edit icon").click()
|
||||||
// Select random icon
|
// Select random icon
|
||||||
|
@ -38,6 +38,7 @@ filterTests(['all'], () => {
|
||||||
cy.get(".title").children().children()
|
cy.get(".title").children().children()
|
||||||
.should('have.attr', 'style').and('contains', 'color')
|
.should('have.attr', 'style').and('contains', 'color')
|
||||||
})
|
})
|
||||||
|
cy.deleteAllApps()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.createTestTableWithData()
|
cy.createTestTableWithData()
|
||||||
cy.wait(2000)
|
cy.wait(2000)
|
||||||
cy.contains("Automate").click()
|
cy.contains("Automate").click()
|
||||||
cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
|
cy.get(".add-button .spectrum-Icon").click()
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
cy.get("input").type("Add Row")
|
cy.get("input").type("Add Row")
|
||||||
cy.contains("Row Created").click({ force: true })
|
cy.contains("Row Created").click({ force: true })
|
||||||
|
|
|
@ -125,7 +125,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
it("renames a view", () => {
|
it("renames a view", () => {
|
||||||
cy.contains(".nav-item", "Test View")
|
cy.contains(".nav-item", "Test View")
|
||||||
.find(".actions .icon")
|
.find(".actions .icon.open-popover")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
|
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
@ -138,7 +138,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
it("deletes a view", () => {
|
it("deletes a view", () => {
|
||||||
cy.contains(".nav-item", "Test View Updated")
|
cy.contains(".nav-item", "Test View Updated")
|
||||||
.find(".actions .icon")
|
.find(".actions .icon.open-popover")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.contains("Delete").click()
|
cy.contains("Delete").click()
|
||||||
cy.contains("Delete View").click()
|
cy.contains("Delete View").click()
|
||||||
|
|
|
@ -99,7 +99,7 @@ filterTests(['all'], () => {
|
||||||
cy.searchForApplication(originalName)
|
cy.searchForApplication(originalName)
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[aria-label='More']").eq(0).click()
|
||||||
})
|
})
|
||||||
// Check for when an app is published
|
// Check for when an app is published
|
||||||
if (published == true) {
|
if (published == true) {
|
||||||
|
@ -109,7 +109,9 @@ filterTests(['all'], () => {
|
||||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||||
}
|
}
|
||||||
cy.contains("Edit").click()
|
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Modal")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
if (noName == true) {
|
if (noName == true) {
|
||||||
|
|
|
@ -10,9 +10,9 @@ filterTests(['smoke', 'all'], () => {
|
||||||
it("should try to revert an unpublished app", () => {
|
it("should try to revert an unpublished app", () => {
|
||||||
// Click revert icon
|
// Click revert icon
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[aria-label='Revert']").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
// Enter app name before revert
|
// Enter app name before revert
|
||||||
cy.get("input").type("Cypress Tests")
|
cy.get("input").type("Cypress Tests")
|
||||||
cy.intercept('**/revert').as('revertApp')
|
cy.intercept('**/revert').as('revertApp')
|
||||||
|
@ -33,11 +33,15 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.get(".spectrum-ButtonGroup").within(() => {
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
})
|
})
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
// Add second component - Button
|
// Add second component - Button
|
||||||
cy.addComponent("Elements", "Button")
|
cy.addComponent("Elements", "Button")
|
||||||
// Click Revert
|
// Click Revert
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[aria-label='Revert']").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
// Click Revert
|
// Click Revert
|
||||||
|
@ -54,7 +58,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
it("should enter incorrect app name when reverting", () => {
|
it("should enter incorrect app name when reverting", () => {
|
||||||
// Click Revert
|
// Click Revert
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
cy.get("[aria-label='Revert']").click({ force: true })
|
||||||
})
|
})
|
||||||
// Enter incorrect app name
|
// Enter incorrect app name
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
|
|
@ -36,6 +36,7 @@ export const Events = {
|
||||||
CREATED: "budibase:app_created",
|
CREATED: "budibase:app_created",
|
||||||
PUBLISHED: "budibase:app_published",
|
PUBLISHED: "budibase:app_published",
|
||||||
UNPUBLISHED: "budibase:app_unpublished",
|
UNPUBLISHED: "budibase:app_unpublished",
|
||||||
|
VIEW_PUBLISHED: "budibase:view_published_app",
|
||||||
},
|
},
|
||||||
ANALYTICS: {
|
ANALYTICS: {
|
||||||
OPT_IN: "budibase:analytics_opt_in",
|
OPT_IN: "budibase:analytics_opt_in",
|
||||||
|
@ -51,3 +52,9 @@ export const Events = {
|
||||||
SAVED: "budibase:sso_saved",
|
SAVED: "budibase:sso_saved",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EventSource = {
|
||||||
|
PORTAL: "portal",
|
||||||
|
URL: "url",
|
||||||
|
NOTIFICATION: "notification",
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { API } from "api"
|
||||||
import PosthogClient from "./PosthogClient"
|
import PosthogClient from "./PosthogClient"
|
||||||
import IntercomClient from "./IntercomClient"
|
import IntercomClient from "./IntercomClient"
|
||||||
import SentryClient from "./SentryClient"
|
import SentryClient from "./SentryClient"
|
||||||
import { Events } from "./constants"
|
import { Events, EventSource } from "./constants"
|
||||||
|
|
||||||
const posthog = new PosthogClient(
|
const posthog = new PosthogClient(
|
||||||
process.env.POSTHOG_TOKEN,
|
process.env.POSTHOG_TOKEN,
|
||||||
|
@ -57,5 +57,5 @@ class AnalyticsHub {
|
||||||
|
|
||||||
const analytics = new AnalyticsHub()
|
const analytics = new AnalyticsHub()
|
||||||
|
|
||||||
export { Events }
|
export { Events, EventSource }
|
||||||
export default analytics
|
export default analytics
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
showConfirmButton={false}
|
showConfirmButton={false}
|
||||||
title="Test Results"
|
|
||||||
cancelText="Close"
|
cancelText="Close"
|
||||||
>
|
>
|
||||||
<div slot="header">
|
<div slot="header" class="result-modal-header">
|
||||||
<div style="float: right;">
|
<span>Test Results</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" />
|
||||||
|
@ -100,6 +100,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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="add-button" data-cy="new-screen">
|
<div class="add-button">
|
||||||
<Icon hoverable name="AddCircle" on:click={modal.show} />
|
<Icon hoverable name="AddCircle" on:click={modal.show} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionMenu>
|
<ActionMenu>
|
||||||
<div slot="control" class="icon">
|
<div slot="control" class="icon open-popover">
|
||||||
<Icon s hoverable name="MoreSmallList" />
|
<Icon s hoverable name="MoreSmallList" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
|
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
export let onCancel = undefined
|
export let onCancel = undefined
|
||||||
export let warning = true
|
export let warning = true
|
||||||
export let disabled
|
export let disabled
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
{cancelText}
|
{cancelText}
|
||||||
{warning}
|
{warning}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{dataCy}
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{body}
|
{body}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
export let label = null
|
export let label = null
|
||||||
export let value
|
export let value
|
||||||
export let copyValue
|
export let copyValue
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
const copyToClipboard = val => {
|
const copyToClipboard = val => {
|
||||||
const dummy = document.createElement("textarea")
|
const dummy = document.createElement("textarea")
|
||||||
|
@ -16,7 +17,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div data-cy={dataCy}>
|
||||||
<Input readonly {value} {label} />
|
<Input readonly {value} {label} />
|
||||||
<div class="icon" on:click={() => copyToClipboard(value || copyValue)}>
|
<div class="icon" on:click={() => copyToClipboard(value || copyValue)}>
|
||||||
<Icon size="S" name="Copy" />
|
<Icon size="S" name="Copy" />
|
||||||
|
|
|
@ -1,24 +1,61 @@
|
||||||
<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, EventSource } 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()
|
//In Progress
|
||||||
|
asyncModal.show()
|
||||||
|
publishModal.hide()
|
||||||
|
|
||||||
|
published = await API.deployAppChanges()
|
||||||
|
|
||||||
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) {
|
||||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: $store.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
|
window.open(publishedUrl, "_blank")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button secondary on:click={publishModal.show}>Publish</Button>
|
<Button secondary on:click={publishModal.show}>Publish</Button>
|
||||||
|
@ -30,11 +67,13 @@
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={publishModal}>
|
<Modal bind:this={publishModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Publish to Production"
|
title="Publish to Production"
|
||||||
confirmText="Publish"
|
confirmText="Publish"
|
||||||
onConfirm={deployApp}
|
onConfirm={deployApp}
|
||||||
|
dataCy={"deploy-app-modal"}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
>The changes you have made will be published to the production version of
|
>The changes you have made will be published to the production version of
|
||||||
|
@ -42,3 +81,57 @@
|
||||||
>
|
>
|
||||||
</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 -->
|
||||||
|
<Modal bind:this={publishCompleteModal}>
|
||||||
|
<ModalContent
|
||||||
|
confirmText="Done"
|
||||||
|
cancelText="View App"
|
||||||
|
onCancel={viewApp}
|
||||||
|
dataCy={"deploy-app-success-modal"}
|
||||||
|
>
|
||||||
|
<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:"
|
||||||
|
dataCy="deployed-app-url"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
notifications,
|
||||||
|
Popover,
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
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 DeployModal from "components/deploy/DeployModal.svelte"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
|
||||||
|
export let application
|
||||||
|
|
||||||
|
let publishPopover
|
||||||
|
let publishPopoverAnchor
|
||||||
|
let unpublishModal
|
||||||
|
|
||||||
|
$: filteredApps = $apps.filter(
|
||||||
|
app => app.devId === application && app.status === "published"
|
||||||
|
)
|
||||||
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
|
||||||
|
$: deployments = []
|
||||||
|
$: latestDeployments = deployments
|
||||||
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
|
$: isPublished = selectedApp && latestDeployments?.length > 0
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewApp = () => {
|
||||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: selectedApp.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
|
if (selectedApp.url) {
|
||||||
|
window.open(`/app${selectedApp.url}`)
|
||||||
|
} else {
|
||||||
|
window.open(`/${selectedApp.prodId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpublishApp = () => {
|
||||||
|
publishPopover.hide()
|
||||||
|
unpublishModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUnpublishApp = async () => {
|
||||||
|
if (!application || !isPublished) {
|
||||||
|
//confirm the app has loaded.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
analytics.captureEvent(Events.APP.UNPUBLISHED, {
|
||||||
|
appId: selectedApp.appId,
|
||||||
|
})
|
||||||
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
|
await apps.load()
|
||||||
|
notifications.success("App unpublished successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error unpublishing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const completePublish = async () => {
|
||||||
|
try {
|
||||||
|
await apps.load()
|
||||||
|
deployments = await fetchDeployments()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error refreshing app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$apps.length) {
|
||||||
|
await apps.load()
|
||||||
|
}
|
||||||
|
deployments = await fetchDeployments()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="deployment-top-nav">
|
||||||
|
{#if isPublished}
|
||||||
|
<div class="publish-popover">
|
||||||
|
<div bind:this={publishPopoverAnchor}>
|
||||||
|
<Icon
|
||||||
|
size="M"
|
||||||
|
hoverable
|
||||||
|
name="Globe"
|
||||||
|
tooltip="Your published app"
|
||||||
|
on:click={publishPopover.show()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={publishPopover}
|
||||||
|
align="right"
|
||||||
|
disabled={!isPublished}
|
||||||
|
dataCy="publish-popover-menu"
|
||||||
|
showTip={true}
|
||||||
|
anchor={publishPopoverAnchor}
|
||||||
|
>
|
||||||
|
<Layout gap="M">
|
||||||
|
<Heading size="XS">Your published app</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
<span class="publish-popover-message">
|
||||||
|
{processStringSync(
|
||||||
|
"Last published {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time:
|
||||||
|
new Date().getTime() -
|
||||||
|
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<div class="publish-popover-actions">
|
||||||
|
<Button
|
||||||
|
warning={true}
|
||||||
|
icon="GlobeStrike"
|
||||||
|
disabled={!isPublished}
|
||||||
|
on:click={unpublishApp}
|
||||||
|
dataCy="publish-popover-action"
|
||||||
|
>
|
||||||
|
Unpublish
|
||||||
|
</Button>
|
||||||
|
<Button cta on:click={viewApp}>View app</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !isPublished}
|
||||||
|
<Icon
|
||||||
|
size="M"
|
||||||
|
name="GlobeStrike"
|
||||||
|
disabled
|
||||||
|
tooltip="Your app has not been published yet"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={unpublishModal}
|
||||||
|
title="Confirm unpublish"
|
||||||
|
okText="Unpublish app"
|
||||||
|
onOk={confirmUnpublishApp}
|
||||||
|
dataCy={"unpublish-modal"}
|
||||||
|
>
|
||||||
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<DeployModal onOk={completePublish} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.publish-popover-actions :global([data-cy="publish-popover-action"]) {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
:global([data-cy="publish-popover-menu"]) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
</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(pendingDeployments[0].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
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -15,6 +15,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
|
||||||
|
@ -22,7 +23,7 @@
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div style="color: {app.icon?.color || ''}">
|
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
||||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||||
</div>
|
</div>
|
||||||
<div class="name" on:click={() => editApp(app)}>
|
<div class="name" on:click={() => editApp(app)}>
|
||||||
|
@ -57,26 +58,40 @@
|
||||||
</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}`}>
|
||||||
|
<div class="app-row-actions">
|
||||||
|
{#if app.deployed}
|
||||||
|
<Button size="S" secondary quiet on:click={() => viewApp(app)}
|
||||||
|
>View app
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button size="S" secondary quiet on:click={() => previewApp(app)}
|
||||||
|
>Preview
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
size="S"
|
size="S"
|
||||||
|
cta
|
||||||
disabled={app.lockedOther}
|
disabled={app.lockedOther}
|
||||||
on:click={() => editApp(app)}
|
on:click={() => editApp(app)}
|
||||||
secondary
|
|
||||||
>
|
>
|
||||||
Open
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
<ActionMenu align="right">
|
</div>
|
||||||
<Icon hoverable slot="control" name="More" />
|
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
||||||
{#if app.deployed}
|
<span slot="control" class="app-row-actions-icon">
|
||||||
<MenuItem on:click={() => viewApp(app)} icon="GlobeOutline">
|
<Icon hoverable name="More" />
|
||||||
View published app
|
</span>
|
||||||
</MenuItem>
|
|
||||||
{/if}
|
|
||||||
{#if app.lockedYou}
|
{#if app.lockedYou}
|
||||||
<MenuItem on:click={() => releaseLock(app)} icon="LockOpen">
|
<MenuItem on:click={() => releaseLock(app)} icon="LockOpen">
|
||||||
Release lock
|
Release lock
|
||||||
|
@ -97,6 +112,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.app-row-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;
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } 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 } from "@budibase/bbui"
|
||||||
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 DeployNavigation from "components/deploy/DeployNavigation.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin } from "stores/portal"
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
$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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
<VersionModal />
|
<VersionModal />
|
||||||
<RevertModal />
|
<RevertModal />
|
||||||
<Icon name="Play" hoverable on:click={previewApp} />
|
<Icon name="Play" hoverable on:click={previewApp} />
|
||||||
<DeployModal />
|
<DeployNavigation {application} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -128,7 +128,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import AppRow from "components/start/AppRow.svelte"
|
import AppRow from "components/start/AppRow.svelte"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import Logo from "assets/bb-space-man.svg"
|
import Logo from "assets/bb-space-man.svg"
|
||||||
|
|
||||||
let sortBy = "name"
|
let sortBy = "name"
|
||||||
|
@ -167,6 +167,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewApp = app => {
|
const viewApp = app => {
|
||||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
|
||||||
|
appId: app.appId,
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
if (app.url) {
|
if (app.url) {
|
||||||
window.open(`/app${app.url}`)
|
window.open(`/app${app.url}`)
|
||||||
} else {
|
} else {
|
||||||
|
@ -174,6 +178,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewApp = app => {
|
||||||
|
window.open(`/${app.devId}`)
|
||||||
|
}
|
||||||
|
|
||||||
const editApp = app => {
|
const editApp = app => {
|
||||||
if (app.lockedOther) {
|
if (app.lockedOther) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
|
@ -205,6 +213,9 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
analytics.captureEvent(Events.APP.UNPUBLISHED, {
|
||||||
|
appId: selectedApp.appId,
|
||||||
|
})
|
||||||
await API.unpublishApp(selectedApp.prodId)
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
await apps.load()
|
await apps.load()
|
||||||
notifications.success("App unpublished successfully")
|
notifications.success("App unpublished successfully")
|
||||||
|
@ -392,6 +403,7 @@
|
||||||
{exportApp}
|
{exportApp}
|
||||||
{deleteApp}
|
{deleteApp}
|
||||||
{updateApp}
|
{updateApp}
|
||||||
|
{previewApp}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -444,6 +456,7 @@
|
||||||
title="Confirm unpublish"
|
title="Confirm unpublish"
|
||||||
okText="Unpublish app"
|
okText="Unpublish app"
|
||||||
onOk={confirmUnpublishApp}
|
onOk={confirmUnpublishApp}
|
||||||
|
dataCy={"unpublish-modal"}
|
||||||
>
|
>
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
|
@ -386,24 +386,32 @@ export const revertClient = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const destroyApp = async (ctx: any) => {
|
const destroyApp = async (ctx: any) => {
|
||||||
const db = getAppDB()
|
let appId = ctx.params.appId
|
||||||
|
let isUnpublish = ctx.query && ctx.query.unpublish
|
||||||
|
|
||||||
|
if (isUnpublish) {
|
||||||
|
appId = getProdAppID(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = isUnpublish ? getProdAppDB() : getAppDB()
|
||||||
const result = await db.destroy()
|
const result = await db.destroy()
|
||||||
if (ctx.query?.unpublish) {
|
|
||||||
|
if (isUnpublish) {
|
||||||
await quotas.removePublishedApp()
|
await quotas.removePublishedApp()
|
||||||
} else {
|
} else {
|
||||||
await quotas.removeApp()
|
await quotas.removeApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (!env.isTest() && !ctx.query.unpublish) {
|
if (!env.isTest() && !isUnpublish) {
|
||||||
await deleteApp(ctx.params.appId)
|
await deleteApp(appId)
|
||||||
}
|
}
|
||||||
if (ctx.query && ctx.query.unpublish) {
|
if (isUnpublish) {
|
||||||
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)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,9 @@ async function deployApp(deployment: any) {
|
||||||
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