mirror of https://github.com/Budibase/budibase.git
17 changed files with 319 additions and 377 deletions
@ -1,82 +0,0 @@ |
|||
<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" |
|||
export let dataCy = null |
|||
|
|||
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" data-cy={dataCy}> |
|||
<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> |
|||
@ -1,51 +1,51 @@ |
|||
import filterTests from "../support/filterTests" |
|||
|
|||
filterTests(['smoke', 'all'], () => { |
|||
context("Auto Screens UI", () => { |
|||
before(() => { |
|||
cy.login() |
|||
cy.createTestApp() |
|||
}) |
|||
|
|||
it("should generate internal table screens", () => { |
|||
// Create autogenerated screens from the internal table
|
|||
cy.createAutogeneratedScreens(["Cypress Tests"]) |
|||
// Confirm screens have been auto generated
|
|||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) |
|||
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id') |
|||
.and('contain', 'cypress-tests/new/row') |
|||
}) |
|||
|
|||
it("should generate multiple internal table screens at once", () => { |
|||
// Create a second internal table
|
|||
const initialTable = "Cypress Tests" |
|||
const secondTable = "Table Two" |
|||
cy.createTable(secondTable) |
|||
// Create autogenerated screens from the internal tables
|
|||
cy.createAutogeneratedScreens([initialTable, secondTable]) |
|||
// Confirm screens have been auto generated
|
|||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) |
|||
// Previously generated tables are suffixed with numbers - as expected
|
|||
cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id') |
|||
.and('contain', 'cypress-tests-2/new/row') |
|||
cy.get(".nav-items-container").contains("table-two").click() |
|||
cy.get(".nav-items-container").should('contain', 'table-two/:id') |
|||
.and('contain', 'table-two/new/row') |
|||
}) |
|||
|
|||
if (Cypress.env("TEST_ENV")) { |
|||
it("should generate data source screens", () => { |
|||
// Using MySQL data source for testing this
|
|||
const datasource = "MySQL" |
|||
// Select & configure MySQL data source
|
|||
cy.selectExternalDatasource(datasource) |
|||
cy.addDatasourceConfig(datasource) |
|||
// Create autogenerated screens from a MySQL table - MySQL contains books table
|
|||
cy.createAutogeneratedScreens(["books"]) |
|||
cy.get(".nav-items-container").contains("books").click() |
|||
cy.get(".nav-items-container").should('contain', 'books/:id') |
|||
.and('contain', 'books/new/row') |
|||
}) |
|||
} |
|||
context("Auto Screens UI", () => { |
|||
before(() => { |
|||
cy.login() |
|||
cy.createTestApp() |
|||
}) |
|||
|
|||
it("should generate internal table screens", () => { |
|||
// Create autogenerated screens from the internal table
|
|||
cy.createAutogeneratedScreens(["Cypress Tests"]) |
|||
// Confirm screens have been auto generated
|
|||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) |
|||
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id') |
|||
.and('contain', 'cypress-tests/new/row') |
|||
}) |
|||
|
|||
it("should generate multiple internal table screens at once", () => { |
|||
// Create a second internal table
|
|||
const initialTable = "Cypress Tests" |
|||
const secondTable = "Table Two" |
|||
cy.createTable(secondTable) |
|||
// Create autogenerated screens from the internal tables
|
|||
cy.createAutogeneratedScreens([initialTable, secondTable]) |
|||
// Confirm screens have been auto generated
|
|||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) |
|||
// Previously generated tables are suffixed with numbers - as expected
|
|||
cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id') |
|||
.and('contain', 'cypress-tests-2/new/row') |
|||
cy.get(".nav-items-container").contains("table-two").click() |
|||
cy.get(".nav-items-container").should('contain', 'table-two/:id') |
|||
.and('contain', 'table-two/new/row') |
|||
}) |
|||
|
|||
if (Cypress.env("TEST_ENV")) { |
|||
it("should generate data source screens", () => { |
|||
// Using MySQL data source for testing this
|
|||
const datasource = "MySQL" |
|||
// Select & configure MySQL data source
|
|||
cy.selectExternalDatasource(datasource) |
|||
cy.addDatasourceConfig(datasource) |
|||
// Create autogenerated screens from a MySQL table - MySQL contains books table
|
|||
cy.createAutogeneratedScreens(["books"]) |
|||
cy.get(".nav-items-container").contains("books").click() |
|||
cy.get(".nav-items-container").should('contain', 'books/:id') |
|||
.and('contain', 'books/new/row') |
|||
}) |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
@ -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: application.appId, |
|||
eventSource: EventSource.PORTAL, |
|||
}) |
|||
if (application.url) { |
|||
window.open(`/app${application.url}`) |
|||
} else { |
|||
window.open(`/${application.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: application.appId, |
|||
}) |
|||
await API.unpublishApp(application.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> |
|||
Loading…
Reference in new issue