forked from tsai/budibase
committed by
GitHub
27 changed files with 667 additions and 118 deletions
@ -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") |
||||
|
|
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -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> |
||||
@ -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 |
||||
|
}, []) |
||||
|
} |
||||
Loading…
Reference in new issue