forked from tsai/budibase
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" |
import filterTests from "../support/filterTests" |
||||
|
|
||||
filterTests(['smoke', 'all'], () => { |
filterTests(['smoke', 'all'], () => { |
||||
context("Auto Screens UI", () => { |
context("Auto Screens UI", () => { |
||||
before(() => { |
before(() => { |
||||
cy.login() |
cy.login() |
||||
cy.createTestApp() |
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') |
|
||||
}) |
|
||||
} |
|
||||
}) |
}) |
||||
|
|
||||
|
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