diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js index ba9a7831db..59c3dc9e16 100644 --- a/packages/backend-core/src/context/index.js +++ b/packages/backend-core/src/context/index.js @@ -174,9 +174,11 @@ function getDB(key, opts) { if (db && isEqual(opts, storedOpts)) { return db } + const appId = exports.getAppId() const CouchDB = getCouch() let toUseAppId + switch (key) { case ContextKeys.CURRENT_DB: toUseAppId = appId diff --git a/packages/bbui/src/ActionMenu/ActionMenu.svelte b/packages/bbui/src/ActionMenu/ActionMenu.svelte index 08425e8f59..c5602d6b0c 100644 --- a/packages/bbui/src/ActionMenu/ActionMenu.svelte +++ b/packages/bbui/src/ActionMenu/ActionMenu.svelte @@ -6,6 +6,7 @@ export let disabled = false export let align = "left" export let portalTarget + export let dataCy let anchor let dropdown @@ -36,7 +37,7 @@
- + diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index fbaac7098c..eee1d7fbae 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -3,6 +3,9 @@ - (showTooltip = true)} + on:focus={() => (showTooltip = true)} + on:mouseleave={() => (showTooltip = false)} + on:click={() => (showTooltip = false)} > - - + + + + {#if tooltip && showTooltip} +
+ +
+ {/if} + diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index 89c10bb625..10cd4b10ba 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -23,6 +23,7 @@ export let secondaryButtonText = undefined export let secondaryAction = undefined export let secondaryButtonWarning = false + export let dataCy = null const { hide, cancel } = getContext(Context.Modal) let loading = false @@ -63,21 +64,26 @@ role="dialog" tabindex="-1" aria-modal="true" + data-cy={dataCy} >
- {#if title} + {#if title || $$slots.header}

- {title} - + {#if title} + {title} + {:else if $$slots.header} + + {/if}

{#if showDivider} {/if} {/if} +
diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 6c9c6cc9a3..1017ef71fc 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -10,6 +10,17 @@ export let anchor export let align = "right" export let portalTarget + export let dataCy + + export let direction = "bottom" + export let showTip = false + + let tipSvg = + ' ' + + $: tooltipClasses = showTip + ? `spectrum-Popover--withTip spectrum-Popover--${direction}` + : "" export const show = () => { dispatch("open") @@ -37,9 +48,14 @@ use:positionDropdown={{ anchor, align }} use:clickOutside={hide} on:keydown={handleEscape} - class="spectrum-Popover is-open" + class={"spectrum-Popover is-open " + (tooltipClasses || "")} role="presentation" + data-cy={dataCy} > + {#if showTip} + {@html tipSvg} + {/if} +
@@ -49,4 +65,13 @@ .spectrum-Popover { 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); + } diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js new file mode 100644 index 0000000000..d18233e0e7 --- /dev/null +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -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") + + }) + }) +}) diff --git a/packages/builder/cypress/integration/changeAppIconAndColour.spec.js b/packages/builder/cypress/integration/changeAppIconAndColour.spec.js index 0db2d49e3f..0f623ddb04 100644 --- a/packages/builder/cypress/integration/changeAppIconAndColour.spec.js +++ b/packages/builder/cypress/integration/changeAppIconAndColour.spec.js @@ -11,7 +11,7 @@ filterTests(['all'], () => { cy.applicationInAppTable("Cypress Tests") cy.get(".appTable") .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() // Select random icon @@ -38,6 +38,7 @@ filterTests(['all'], () => { cy.get(".title").children().children() .should('have.attr', 'style').and('contains', 'color') }) + cy.deleteAllApps() }) }) }) diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index ff8065f544..69ef3f98a3 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -11,7 +11,7 @@ filterTests(['smoke', 'all'], () => { cy.createTestTableWithData() cy.wait(2000) 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("input").type("Add Row") cy.contains("Row Created").click({ force: true }) diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js index a8c3b03cee..feaf1c3b5f 100644 --- a/packages/builder/cypress/integration/createView.spec.js +++ b/packages/builder/cypress/integration/createView.spec.js @@ -125,7 +125,7 @@ filterTests(['smoke', 'all'], () => { it("renames a view", () => { cy.contains(".nav-item", "Test View") - .find(".actions .icon") + .find(".actions .icon.open-popover") .click({ force: true }) cy.get(".spectrum-Menu-itemLabel").contains("Edit").click() cy.get(".modal-inner-wrapper").within(() => { @@ -138,7 +138,7 @@ filterTests(['smoke', 'all'], () => { it("deletes a view", () => { cy.contains(".nav-item", "Test View Updated") - .find(".actions .icon") + .find(".actions .icon.open-popover") .click({ force: true }) cy.contains("Delete").click() cy.contains("Delete View").click() diff --git a/packages/builder/cypress/integration/renameAnApplication.spec.js b/packages/builder/cypress/integration/renameAnApplication.spec.js index f4899f98a0..120c0d54d7 100644 --- a/packages/builder/cypress/integration/renameAnApplication.spec.js +++ b/packages/builder/cypress/integration/renameAnApplication.spec.js @@ -99,30 +99,32 @@ filterTests(['all'], () => { cy.searchForApplication(originalName) cy.get(".appTable") .within(() => { - cy.get(".spectrum-Icon").eq(1).click() - }) - // Check for when an app is published - if (published == true) { - // Should not have Edit as option, will unpublish app - cy.should("not.have.value", "Edit") - cy.get(".spectrum-Menu").contains("Unpublish").click() - cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() - cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click() - } - cy.contains("Edit").click() - cy.get(".spectrum-Modal") - .within(() => { - if (noName == true) { - cy.get("input").clear() - cy.get(".spectrum-Dialog-grid").click() - .contains("App name must be letters, numbers and spaces only") - return cy - } + cy.get("[aria-label='More']").eq(0).click() + }) + // Check for when an app is published + if (published == true) { + // Should not have Edit as option, will unpublish app + cy.should("not.have.value", "Edit") + cy.get(".spectrum-Menu").contains("Unpublish").click() + cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() + cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").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") + .within(() => { + if (noName == true) { cy.get("input").clear() - cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur() - cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) - cy.wait(500) - }) - } + cy.get(".spectrum-Dialog-grid").click() + .contains("App name must be letters, numbers and spaces only") + return cy + } + cy.get("input").clear() + cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur() + cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) + cy.wait(500) + }) + } }) }) diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js index c64d19f230..9d5e4f0f63 100644 --- a/packages/builder/cypress/integration/revertApp.spec.js +++ b/packages/builder/cypress/integration/revertApp.spec.js @@ -10,9 +10,9 @@ filterTests(['smoke', 'all'], () => { it("should try to revert an unpublished app", () => { // Click revert icon 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 cy.get("input").type("Cypress Tests") cy.intercept('**/revert').as('revertApp') @@ -33,11 +33,15 @@ filterTests(['smoke', 'all'], () => { cy.get(".spectrum-ButtonGroup").within(() => { 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 cy.addComponent("Elements", "Button") // Click Revert 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(() => { // Click Revert @@ -54,7 +58,7 @@ filterTests(['smoke', 'all'], () => { it("should enter incorrect app name when reverting", () => { // Click Revert 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 cy.get(".spectrum-Dialog-grid").within(() => { diff --git a/packages/builder/src/analytics/constants.js b/packages/builder/src/analytics/constants.js index 98408b5cb3..300b1e058d 100644 --- a/packages/builder/src/analytics/constants.js +++ b/packages/builder/src/analytics/constants.js @@ -36,6 +36,7 @@ export const Events = { CREATED: "budibase:app_created", PUBLISHED: "budibase:app_published", UNPUBLISHED: "budibase:app_unpublished", + VIEW_PUBLISHED: "budibase:view_published_app", }, ANALYTICS: { OPT_IN: "budibase:analytics_opt_in", @@ -51,3 +52,9 @@ export const Events = { SAVED: "budibase:sso_saved", }, } + +export const EventSource = { + PORTAL: "portal", + URL: "url", + NOTIFICATION: "notification", +} diff --git a/packages/builder/src/analytics/index.js b/packages/builder/src/analytics/index.js index 3a4118347d..aa1ebf7e0e 100644 --- a/packages/builder/src/analytics/index.js +++ b/packages/builder/src/analytics/index.js @@ -2,7 +2,7 @@ import { API } from "api" import PosthogClient from "./PosthogClient" import IntercomClient from "./IntercomClient" import SentryClient from "./SentryClient" -import { Events } from "./constants" +import { Events, EventSource } from "./constants" const posthog = new PosthogClient( process.env.POSTHOG_TOKEN, @@ -57,5 +57,5 @@ class AnalyticsHub { const analytics = new AnalyticsHub() -export { Events } +export { Events, EventSource } export default analytics diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte index 67c7f493e8..9662bc8ade 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte @@ -10,11 +10,11 @@ -
-
+
+ Test Results +
{#if isTrigger || testResult[0].outputs.success}
@@ -100,6 +100,14 @@ diff --git a/packages/builder/src/components/deploy/DeployNavigation.svelte b/packages/builder/src/components/deploy/DeployNavigation.svelte new file mode 100644 index 0000000000..ded75652cc --- /dev/null +++ b/packages/builder/src/components/deploy/DeployNavigation.svelte @@ -0,0 +1,189 @@ + + +
+ {#if isPublished} +
+
+ +
+ + + Your published app + + + {processStringSync( + "Last published {{ duration time 'millisecond' }} ago", + { + time: + new Date().getTime() - + new Date(latestDeployments[0].updatedAt).getTime(), + } + )} + + +
+ + +
+
+
+
+ {/if} + + {#if !isPublished} + + {/if} +
+ + Are you sure you want to unpublish the app {selectedApp?.name}? + + + + + diff --git a/packages/builder/src/components/deploy/DeploymentHistory.svelte b/packages/builder/src/components/deploy/DeploymentHistory.svelte index e933142348..eb5c8953cc 100644 --- a/packages/builder/src/components/deploy/DeploymentHistory.svelte +++ b/packages/builder/src/components/deploy/DeploymentHistory.svelte @@ -7,12 +7,10 @@ import { notifications } from "@budibase/bbui" import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte" import { store } from "builderStore" - - const DeploymentStatus = { - SUCCESS: "SUCCESS", - PENDING: "PENDING", - FAILURE: "FAILURE", - } + import { + checkIncomingDeploymentStatus, + DeploymentStatus, + } from "components/deploy/utils" const DATE_OPTIONS = { fullDate: { @@ -42,30 +40,17 @@ const formatDate = (date, format) => 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() { try { const newDeployments = await API.getAppDeployments() if (deployments.length > 0) { - checkIncomingDeploymentStatus(deployments, newDeployments) + const pendingDeployments = checkIncomingDeploymentStatus( + deployments, + newDeployments + ) + if (pendingDeployments.length) { + showErrorReasonModal(pendingDeployments[0].err) + } } deployments = newDeployments } catch (err) { diff --git a/packages/builder/src/components/deploy/utils.js b/packages/builder/src/components/deploy/utils.js new file mode 100644 index 0000000000..cb254f0dbf --- /dev/null +++ b/packages/builder/src/components/deploy/utils.js @@ -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 + }, []) +} diff --git a/packages/builder/src/components/start/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte index d6dc4e1800..ea2f005216 100644 --- a/packages/builder/src/components/start/AppRow.svelte +++ b/packages/builder/src/components/start/AppRow.svelte @@ -15,6 +15,7 @@ export let editApp export let updateApp export let deleteApp + export let previewApp export let unpublishApp export let releaseLock export let editIcon @@ -22,7 +23,7 @@
-
+
editApp(app)}> @@ -57,26 +58,40 @@
- - {#if app.deployed}Published{:else}Unpublished{/if} - +
+ {#if app.deployed} + + Published + {:else} + + Unpublished + {/if} +
- - - +
{#if app.deployed} - viewApp(app)} icon="GlobeOutline"> - View published app - + + {:else} + {/if} + +
+ + + + {#if app.lockedYou} releaseLock(app)} icon="LockOpen"> Release lock @@ -97,6 +112,18 @@