Merge remote-tracking branch 'origin/develop' into feature/required-field-focus

This commit is contained in:
Dean 2022-05-24 17:09:15 +01:00
commit 6114e37f19
33 changed files with 1696 additions and 155 deletions

View File

@ -1,5 +1,5 @@
{
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js",
"author": "Budibase",

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.177",
"@budibase/string-templates": "^1.0.178-alpha.0",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

View File

@ -40,6 +40,10 @@
padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl);
}
.paddingX-XXL {
padding-left: var(--spectrum-alias-grid-gutter-large);
padding-right: var(--spectrum-alias-grid-gutter-large);
}
.paddingY-S {
padding-top: var(--spacing-s);
padding-bottom: var(--spacing-s);
@ -56,6 +60,10 @@
padding-top: var(--spacing-xl);
padding-bottom: var(--spacing-xl);
}
.paddingY-XXL {
padding-top: var(--spectrum-alias-grid-gutter-large);
padding-bottom: var(--spectrum-alias-grid-gutter-large);
}
.gap-XXS {
grid-gap: var(--spacing-xs);
}

View File

@ -1,9 +1,10 @@
<script>
export let wide = false
export let maxWidth = "80ch"
export let noPadding = false
</script>
<div style="--max-width: {maxWidth}" class:wide>
<div style="--max-width: {maxWidth}" class:wide class:noPadding>
<slot />
</div>
@ -23,4 +24,9 @@
max-width: none;
margin: 0;
}
.noPadding {
padding: 0px;
margin: 0px;
}
</style>

View File

@ -7,6 +7,7 @@
export let icon = ""
export let selected = false
export let disabled = false
export let dataCy
</script>
<li
@ -14,6 +15,7 @@
class:is-selected={selected}
class:is-disabled={disabled}
on:click
data-cy={dataCy}
>
{#if heading}
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">

View File

@ -6,7 +6,7 @@
const dispatch = createEventDispatcher()
let selected = getContext("tab")
let tab
let tab_internal
let tabInfo
const setTabInfo = () => {
@ -16,7 +16,7 @@
// We just need to get this off the main thread to fix this, by using
// a 0ms timeout.
setTimeout(() => {
tabInfo = tab?.getBoundingClientRect()
tabInfo = tab_internal?.getBoundingClientRect()
if (tabInfo && $selected.title === title) {
$selected.info = tabInfo
}
@ -27,14 +27,30 @@
setTabInfo()
})
//Ensure that the underline is in the correct location
$: {
if ($selected.title === title && tab_internal) {
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
$selected = {
...$selected,
info: tab_internal.getBoundingClientRect(),
}
}
}
}
const onClick = () => {
$selected = { ...$selected, title, info: tab.getBoundingClientRect() }
$selected = {
...$selected,
title,
info: tab_internal.getBoundingClientRect(),
}
dispatch("click")
}
</script>
<div
bind:this={tab}
bind:this={tab_internal}
on:click={onClick}
class:is-selected={$selected.title === title}
class="spectrum-Tabs-item"

View File

@ -0,0 +1,346 @@
import filterTests from "../support/filterTests"
import clientPackage from "@budibase/client/package.json"
filterTests(['all'], () => {
context("Application Overview screen", () => {
before(() => {
cy.login()
cy.createTestApp()
})
it("Should be accessible from the applications list", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .title").eq(0)
.invoke('attr', 'data-cy')
.then(($dataCy) => {
const dataCy = $dataCy;
cy.get(".appTable .name").eq(0).click()
cy.location().should((loc) => {
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy)
})
})
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .title").eq(0)
.invoke('attr', 'data-cy')
.then(($dataCy) => {
const dataCy = $dataCy;
cy.get(".appTable .app-row-actions button").contains("View").click({force: true})
cy.location().should((loc) => {
expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy)
})
})
})
// Find a more suitable place for this.
it("Should allow unlocking in the app list", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click()
cy.unlockApp({ owned : true })
cy.get(".appTable").should("exist")
cy.get(".lock-status").should('not.be.visible')
})
it("Should allow unlocking in the app overview screen", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
cy.wait(1000)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".lock-status").eq(0).contains("Locked by you").click()
cy.unlockApp({ owned : true })
cy.get(".lock-status").should("not.be.visible")
})
it("Should reflect the deploy state of an app that hasn't been published.", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("be.disabled")
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview")
cy.get(".overview-tab").should("be.visible")
cy.get(".overview-tab [data-cy='app-status']").within(() => {
cy.get(".status-display").contains("Unpublished")
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
cy.get(".status-text").contains("-")
})
})
it("Should reflect the app deployment state", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
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)
});
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("not.be.disabled")
cy.get(".overview-tab [data-cy='app-status']").within(() => {
cy.get(".status-display").contains("Published")
cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should("exist")
cy.get(".status-text").contains("Last published a few seconds ago")
})
})
it("Should reflect an application that has been unpublished", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
cy.get(".deployment-top-nav svg[aria-label='Globe']")
.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.wait(1000)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".overview-tab [data-cy='app-status']").within(() => {
cy.get(".status-display").contains("Unpublished")
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
cy.get(".status-text").contains("Last published a few seconds ago")
})
})
it("Should allow the editing of the application icon", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".app-logo .edit-hover").should("exist").invoke("show").click()
cy.customiseAppIcon()
cy.get(".app-logo")
.within(() => {
cy.get('[aria-label]').eq(0).children()
.should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps')
cy.get(".app-icon")
.should('have.attr', 'style').and('contains', 'color')
})
})
it("Should reflect the last time the application was edited", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".header-right button").contains("Edit").click({ force: true });
cy.navigateToFrontend()
cy.addComponent("Elements", "Headline").then(componentId => {
cy.getComponent(componentId).should("exist")
})
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".overview-tab [data-cy='edited-by']").within(() => {
cy.get(".editor-name").contains("You")
cy.get(".last-edit-text").contains("Last edited a few seconds ago")
})
});
it("Should reflect application version is up-to-date", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".overview-tab [data-cy='app-version']").within(() => {
cy.get(".version-status").contains("You're running the latest!")
})
});
it("Should navigate to the settings tab when clicking the App Version card header", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".spectrum-Tabs-item.is-selected").contains("Overview")
cy.get(".overview-tab").should("be.visible")
cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ force : true })
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
cy.get(".settings-tab").should("be.visible")
cy.get(".overview-tab").should("not.exist")
});
it("Should allow the upgrading of an application, if available.", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.wait(500)
cy.location().then(loc => {
const params = loc.pathname.split("/")
const appId = params[params.length - 1]
cy.log(appId)
//Downgrade the app for the test
cy.alterAppVersion(appId, "0.0.1-alpha.0")
.then(()=>{
cy.reload()
cy.wait(1000)
cy.log("Current deployment version: " + clientPackage.version)
cy.get(".version-status a").contains("Update").click()
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
cy.get(".version-section .page-action button").contains("Update").click({ force: true })
cy.intercept('POST', '**/applications/**/client/update').as('updateVersion')
cy.get(".spectrum-Modal.is-open button").contains("Update").click({ force: true })
cy.wait("@updateVersion")
.its('response.statusCode').should('eq', 200)
.then(() => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".spectrum-Tabs-item").contains("Overview").click({ force: true })
cy.get(".overview-tab [data-cy='app-version']").within(() => {
cy.get(".spectrum-Heading").contains(clientPackage.version)
cy.get(".version-status").contains("You're running the latest!")
})
})
})
});
})
it("Should allow editing of the app details.", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".spectrum-Tabs-item").contains("Settings").click()
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
cy.get(".settings-tab").should("be.visible")
cy.get(".details-section .page-action button").contains("Edit").click({ force: true })
cy.updateAppName("sample name")
//publish and check its disabled
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
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)
});
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".spectrum-Tabs-item").contains("Settings").click()
cy.get(".spectrum-Tabs-item.is-selected").contains("Settings")
cy.get(".details-section .page-action .spectrum-Button").scrollIntoView()
cy.wait(1000)
cy.get(".details-section .page-action .spectrum-Button").should("be.disabled")
})
it("Should allow copying of the published application Id", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions").eq(0)
.within(() => {
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
})
cy.publishApp("sample-name")
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
cy.get(".spectrum-Menu-item").contains("Copy App ID").click({ force: true })
})
cy.get(".spectrum-Toast-content").contains("App ID copied to clipboard.").should("be.visible")
})
it("Should allow unpublishing of the application", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
cy.get(".spectrum-Menu-item").contains("Unpublish").click({ force: true })
cy.wait(500)
})
cy.get("[data-cy='unpublish-modal']").should("be.visible")
.within(() => {
cy.get(".confirm-wrap button").click({ force: true }
)})
cy.get(".overview-tab [data-cy='app-status']").within(() => {
cy.get(".status-display").contains("Unpublished")
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
})
})
it("Should allow deleting of the application", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true })
cy.wait(500)
})
//The test application was renamed earlier in the spec
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get("input").type("sample name")
cy.get(".spectrum-Button--warning").click()
})
cy.location().should((loc) => {
expect(loc.pathname).to.eq('/builder/portal/apps')
})
cy.get(".appTable").should("not.exist")
cy.get(".welcome .container h1").contains("Let's create your first app!")
})
after(() => {
cy.deleteAllApps()
})
})
})

View File

@ -19,7 +19,7 @@ filterTests(['all'], () => {
cy.get(".appTable .app-row-actions").eq(0)
.within(() => {
cy.get(".spectrum-Button").contains("Preview")
cy.get(".spectrum-Button").contains("View")
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
})
@ -29,22 +29,8 @@ filterTests(['all'], () => {
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.publishApp("cypress-tests")
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
@ -57,7 +43,7 @@ filterTests(['all'], () => {
cy.get(".appTable .app-row-actions").eq(0)
.within(() => {
cy.get(".spectrum-Button").contains("View app")
cy.get(".spectrum-Button").contains("View")
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
})
@ -66,7 +52,7 @@ filterTests(['all'], () => {
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("button").contains("View").should("exist")
cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago")
})
})
@ -84,7 +70,7 @@ filterTests(['all'], () => {
cy.get(".appTable .app-row-actions").eq(0)
.within(() => {
cy.get(".spectrum-Button").contains("View app")
cy.get(".spectrum-Button").contains("View")
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
})

View File

@ -112,19 +112,9 @@ filterTests(['all'], () => {
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(".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)
})
}
})
cy.updateAppName(changedName, noName)
}
})
})

View File

@ -32,6 +32,15 @@ Cypress.Commands.add("login", () => {
})
})
Cypress.Commands.add("logOut", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".user-dropdown .avatar > .icon").click({ force: true })
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
cy.get("li[data-cy='user-logout']").click({ force: true })
})
cy.wait(2000)
})
Cypress.Commands.add("closeModal", () => {
cy.get(".spectrum-Modal").within(() => {
cy.get(".close-icon").click()
@ -209,6 +218,109 @@ Cypress.Commands.add("deleteAllApps", () => {
})
})
Cypress.Commands.add("customiseAppIcon", () => {
// Select random icon
cy.get(".grid").within(() => {
cy.get(".icon-item")
.eq(Math.floor(Math.random() * 23) + 1)
.click()
})
// Select random colour
cy.get(".fill").click()
cy.get(".colors").within(() => {
cy.get(".color")
.eq(Math.floor(Math.random() * 33) + 1)
.click()
})
cy.intercept("**/applications/**").as("iconChange")
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.wait("@iconChange")
cy.get("@iconChange").its("response.statusCode").should("eq", 200)
cy.wait(1000)
})
Cypress.Commands.add("alterAppVersion", (appId, version) => {
return cy
.request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, {
version: version || "0.0.1-alpha.0",
})
.then(resp => {
expect(resp.status).to.eq(200)
})
})
Cypress.Commands.add("updateAppName", (changedName, noName) => {
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("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)
})
})
Cypress.Commands.add("unlockApp", unlock_config => {
let config = { ...unlock_config }
cy.get(".spectrum-Modal .spectrum-Dialog[data-cy='app-lock-modal']")
.should("be.visible")
.within(() => {
if (config.owned) {
cy.get(".spectrum-Dialog-heading").contains("Locked by you")
cy.get(".lock-expiry-body").contains(
"This lock will expire in 10 minutes from now"
)
cy.intercept("**/lock").as("unlockApp")
cy.get(".spectrum-Button")
.contains("Release Lock")
.click({ force: true })
cy.wait("@unlockApp")
cy.get("@unlockApp").its("response.statusCode").should("eq", 200)
cy.get("@unlockApp").its("response.body").should("deep.equal", {
message: "Lock released successfully.",
})
} else {
//Show the name ?
cy.get(".lock-expiry-body").should("not.be.visible")
cy.get(".spectrum-Button").contains("Done")
}
})
})
Cypress.Commands.add("publishApp", resolvedAppPath => {
//Assumes you have navigated to an application first
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/" + resolvedAppPath
cy.get("[data-cy='deployed-app-url'] input").should("have.value", appUrl)
cy.get(".spectrum-Button").contains("Done").click({ force: true })
})
})
Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests"
cy.deleteApp(appName)

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -69,10 +69,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.177",
"@budibase/client": "^1.0.177",
"@budibase/frontend-core": "^1.0.177",
"@budibase/string-templates": "^1.0.177",
"@budibase/bbui": "^1.0.178-alpha.0",
"@budibase/client": "^1.0.178-alpha.0",
"@budibase/frontend-core": "^1.0.178-alpha.0",
"@budibase/string-templates": "^1.0.178-alpha.0",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View File

@ -0,0 +1,144 @@
<script>
import {
Button,
ButtonGroup,
ModalContent,
Modal,
notifications,
ProgressCircle,
} from "@budibase/bbui"
import { auth, apps } from "stores/portal"
import { processStringSync } from "@budibase/string-templates"
import { API } from "api"
export let app
export let buttonSize = "M"
let APP_DEV_LOCK_SECONDS = 600 //common area for this?
let appLockModal
let processing = false
$: lockedBy = app?.lockedBy
$: lockedByYou = $auth.user.email === lockedBy?.email
$: lockIdentifer = `${
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email
}`
$: lockedByHeading =
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
const getExpiryDuration = app => {
if (!app?.lockedBy?.lockedAt) {
return -1
}
let expiry =
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000
return expiry - new Date().getTime()
}
const releaseLock = async () => {
processing = true
if (app) {
try {
await API.releaseAppLock(app.devId)
await apps.load()
notifications.success("Lock released successfully")
} catch (err) {
notifications.error("Error releasing lock")
}
} else {
notifications.error("No application is selected")
}
processing = false
}
</script>
<div class="lock-status">
{#if lockedBy}
<Button
quiet
secondary
icon="LockClosed"
size={buttonSize}
on:click={() => {
appLockModal.show()
}}
>
<span class="lock-status-text">
{lockedByHeading}
</span>
</Button>
{/if}
</div>
<Modal bind:this={appLockModal}>
<ModalContent
title={lockedByHeading}
dataCy={"app-lock-modal"}
showConfirmButton={false}
showCancelButton={false}
>
<p>
Apps are locked to prevent work from being lost from overlapping changes
between your team.
</p>
{#if lockedByYou && getExpiryDuration(app) > 0}
<span class="lock-expiry-body">
{processStringSync(
"This lock will expire in {{ duration time 'millisecond' }} from now",
{
time: getExpiryDuration(app),
}
)}
</span>
{/if}
<div class="lock-modal-actions">
<ButtonGroup>
<Button
secondary
quiet={lockedBy && lockedByYou}
disabled={processing}
on:click={() => {
appLockModal.hide()
}}
>
<span class="cancel"
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
>
</Button>
{#if lockedByYou}
<Button
secondary
disabled={processing}
on:click={() => {
releaseLock()
appLockModal.hide()
}}
>
{#if processing}
<ProgressCircle overBackground={true} size="S" />
{:else}
<span class="unlock">Release Lock</span>
{/if}
</Button>
{/if}
</ButtonGroup>
</div>
</ModalContent>
</Modal>
<style>
.lock-modal-actions {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-l);
gap: var(--spacing-xl);
}
.lock-status {
display: flex;
gap: var(--spacing-s);
max-width: 175px;
}
</style>

View File

@ -0,0 +1,55 @@
<script>
import { Icon, Detail } from "@budibase/bbui"
export let title = ""
export let actionIcon
export let action
export let dataCy
$: actionDefined = typeof action === "function"
</script>
<div class="dash-card" data-cy={dataCy}>
<div class="dash-card-header" class:active={actionDefined} on:click={action}>
<span class="dash-card-title">
<Detail size="M">{title}</Detail>
</span>
<span class="dash-card-action">
{#if actionDefined}
<Icon name={actionIcon || "ChevronRight"} />
{/if}
</span>
</div>
<div class="dash-card-body">
<slot />
</div>
</div>
<style>
.dash-card {
background: var(--spectrum-alias-background-color-primary);
border-radius: var(--border-radius-s);
overflow: hidden;
min-height: 150px;
}
.dash-card-header {
padding: var(--spacing-xl) var(--spectrum-global-dimension-static-size-400);
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
display: flex;
justify-content: space-between;
}
.dash-card-body {
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2);
}
.dash-card-title :global(.spectrum-Detail) {
color: var(
--spectrum-sidenav-heading-text-color,
var(--spectrum-global-color-gray-700)
);
display: inline-block;
}
.dash-card-header.active:hover {
background-color: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
</style>

View File

@ -0,0 +1,47 @@
<script>
import { Icon } from "@budibase/bbui"
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
export let name
export let size
export let app
let iconModal
</script>
<div class="editable-icon">
<div
class="edit-hover"
on:click={() => {
iconModal.show()
}}
>
<Icon name={"Edit"} size={"L"} />
</div>
<div class="app-icon">
<Icon {name} {size} />
</div>
</div>
<ChooseIconModal {app} bind:this={iconModal} />
<style>
.editable-icon:hover .app-icon {
opacity: 0;
}
.editable-icon {
position: relative;
}
.editable-icon:hover .edit-hover {
opacity: 1;
}
.edit-hover {
color: var(--spectrum-global-color-gray-600);
cursor: pointer;
z-index: 100;
width: 100%;
height: 100%;
position: absolute;
opacity: 0;
/* transition: opacity var(--spectrum-global-animation-duration-100) ease; */
}
</style>

View File

@ -11,6 +11,16 @@
import { API } from "api"
import clientPackage from "@budibase/client/package.json"
export function show() {
updateModal.show()
}
export function hide() {
updateModal.hide()
}
export let hideIcon = false
let updateModal
$: appId = $store.appId
@ -57,9 +67,11 @@
}
</script>
<div class="icon-wrapper" class:highlight={updateAvailable}>
<Icon name="Refresh" hoverable on:click={updateModal.show} />
</div>
{#if !hideIcon}
<div class="icon-wrapper" class:highlight={updateAvailable}>
<Icon name="Refresh" hoverable on:click={updateModal.show} />
</div>
{/if}
<Modal bind:this={updateModal}>
<ModalContent
title="App version"

View File

@ -1,33 +1,26 @@
<script>
import {
Heading,
Button,
Icon,
ActionMenu,
MenuItem,
StatusLight,
} from "@budibase/bbui"
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
import AppLockModal from "../common/AppLockModal.svelte"
import { processStringSync } from "@budibase/string-templates"
export let app
export let exportApp
export let viewApp
export let editApp
export let updateApp
export let deleteApp
export let previewApp
export let unpublishApp
export let appOverview
export let releaseLock
export let editIcon
export let copyAppId
</script>
<div class="title">
<div class="title" data-cy={`${app.devId}`}>
<div style="display: flex;">
<div class="app-icon" style="color: {app.icon?.color || ''}">
<Icon size="XL" name={app.icon?.name || "Apps"} />
</div>
<div class="name" on:click={() => editApp(app)}>
<div class="name" on:click={() => appOverview(app)}>
<Heading size="XS">
{app.name}
</Heading>
@ -44,19 +37,7 @@
{/if}
</div>
<div class="desktop">
<StatusLight
positive={!app.lockedYou && !app.lockedOther}
notice={app.lockedYou}
negative={app.lockedOther}
>
{#if app.lockedYou}
Locked by you
{:else if app.lockedOther}
Locked by {app.lockedBy.email}
{:else}
Open
{/if}
</StatusLight>
<AppLockModal {app} buttonSize="S" />
</div>
<div class="desktop">
<div class="app-status">
@ -71,23 +52,15 @@
</div>
<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
size="S"
cta
secondary
quiet
disabled={app.lockedOther}
on:click={() => editApp(app)}
>
Edit
>Edit
</Button>
<Button size="S" cta on:click={() => appOverview(app)}>View</Button>
</div>
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
<span slot="control" class="app-row-actions-icon">
@ -123,6 +96,7 @@
}
.app-status {
display: grid;
grid-gap: var(--spacing-s);
grid-template-columns: 24px 100px;
}
.app-status span.disabled {

View File

@ -203,7 +203,9 @@
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
Close developer mode
</MenuItem>
<MenuItem icon="LogOut" on:click={logout}>Log out</MenuItem>
<MenuItem dataCy="user-logout" icon="LogOut" on:click={logout}
>Log out
</MenuItem>
</ActionMenu>
</div>
</div>
@ -332,7 +334,7 @@
.mobile-toggle,
.user-dropdown {
flex: 1 1 0;
flex: 0 1 0;
}
/* Reduce BBUI page padding */

View File

@ -9,6 +9,7 @@
Body,
Modal,
Divider,
ActionButton,
} from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
@ -60,16 +61,15 @@
<Page wide>
<Layout noPadding gap="XL">
<span>
<Button
quiet
<ActionButton
secondary
icon={"ChevronLeft"}
icon={"ArrowLeft"}
on:click={() => {
$goto("../")
}}
>
Back
</Button>
</ActionButton>
</span>
<div class="title">

View File

@ -2,7 +2,6 @@
import {
Heading,
Layout,
Detail,
Button,
Input,
Select,
@ -11,7 +10,6 @@
notifications,
Body,
Search,
Divider,
Helpers,
} from "@budibase/bbui"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
@ -67,6 +65,9 @@
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
)
$: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther)
$: unlocked = lockedApps?.length == 0
const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({
...app,
@ -179,8 +180,8 @@
}
}
const previewApp = app => {
window.open(`/${app.devId}`)
const appOverview = app => {
$goto(`../overview/${app.devId}`)
}
const editApp = app => {
@ -304,7 +305,7 @@
</script>
<Page wide>
<Layout noPadding gap="XL">
<Layout noPadding gap="M">
{#if loaded}
<div class="title">
<div class="welcome">
@ -314,29 +315,17 @@
{welcomeBody}
</Body>
</Layout>
<div class="buttons">
<Button
dataCy="create-app-btn"
size="M"
icon="Add"
cta
on:click={initiateAppCreation}
>
{createAppButtonText}
</Button>
{#if $apps?.length > 0}
{#if !$apps?.length}
<div class="buttons">
<Button
icon="Experience"
dataCy="create-app-btn"
size="M"
quiet
secondary
on:click={$goto("/builder/portal/apps/templates")}
icon="Add"
cta
on:click={initiateAppCreation}
>
Templates
{createAppButtonText}
</Button>
{/if}
{#if !$apps?.length}
<Button
dataCy="import-app-btn"
icon="Import"
@ -347,15 +336,9 @@
>
Import app
</Button>
{/if}
</div>
</div>
{/if}
</div>
<div>
<Layout gap="S" justifyItems="center">
<img class="img-logo img-size" alt="logo" src={Logo} />
</Layout>
</div>
<Divider size="S" />
</div>
{#if !$apps?.length && $templates?.length}
@ -363,9 +346,42 @@
{/if}
{#if enrichedApps.length}
<Layout noPadding gap="S">
<Layout noPadding gap="L">
<div class="title">
<Detail size="L">Apps</Detail>
<div class="buttons">
<Button
dataCy="create-app-btn"
size="M"
icon="Add"
cta
on:click={initiateAppCreation}
>
{createAppButtonText}
</Button>
{#if $apps?.length > 0}
<Button
icon="Experience"
size="M"
quiet
secondary
on:click={$goto("/builder/portal/apps/templates")}
>
Templates
</Button>
{/if}
{#if !$apps?.length}
<Button
dataCy="import-app-btn"
icon="Import"
size="L"
quiet
secondary
on:click={initiateAppImport}
>
Import app
</Button>
{/if}
</div>
{#if enrichedApps.length > 1}
<div class="app-actions">
{#if cloud}
@ -397,7 +413,7 @@
{/if}
</div>
<div class="appTable">
<div class="appTable" class:unlocked>
{#each filteredApps as app (app.appId)}
<AppRow
{copyAppId}
@ -410,7 +426,7 @@
{exportApp}
{deleteApp}
{updateApp}
{previewApp}
{appOverview}
/>
{/each}
</div>
@ -471,6 +487,9 @@
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
<style>
.appTable {
border-top: var(--border-light);
}
.app-actions {
display: flex;
}
@ -478,7 +497,7 @@
margin-right: 10px;
}
.title .welcome > .buttons {
padding-top: 30px;
padding-top: var(--spacing-l);
}
.title {
display: flex;
@ -514,6 +533,11 @@
grid-template-columns: 1fr 1fr 1fr 1fr auto;
align-items: center;
}
.appTable.unlocked {
grid-template-columns: 1fr 1fr auto 1fr auto;
}
.appTable :global(> div) {
height: 70px;
display: grid;

View File

@ -1,6 +1,6 @@
<script>
import { goto } from "@roxi/routify"
import { Layout, Page, notifications, Button } from "@budibase/bbui"
import { Layout, Page, notifications, ActionButton } from "@budibase/bbui"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
import { onMount } from "svelte"
import { templates } from "stores/portal"
@ -25,16 +25,15 @@
<Page wide>
<Layout noPadding gap="XL">
<span>
<Button
quiet
<ActionButton
secondary
icon={"ChevronLeft"}
icon={"ArrowLeft"}
on:click={() => {
$goto("../")
}}
>
Back
</Button>
</ActionButton>
</span>
{#if loaded && $templates?.length}
<TemplateDisplay templates={$templates} />

View File

@ -0,0 +1,413 @@
<script>
import { goto } from "@roxi/routify"
import {
Layout,
Page,
Button,
ActionButton,
ButtonGroup,
Heading,
Tab,
Tabs,
notifications,
ProgressCircle,
Input,
ActionMenu,
MenuItem,
Icon,
Helpers,
} from "@budibase/bbui"
import OverviewTab from "../_components/OverviewTab.svelte"
import SettingsTab from "../_components/SettingsTab.svelte"
import { API } from "api"
import { store } from "builderStore"
import { apps, auth } from "stores/portal"
import analytics, { Events, EventSource } from "analytics"
import { AppStatus } from "constants"
import AppLockModal from "components/common/AppLockModal.svelte"
import EditableIcon from "components/common/EditableIcon.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
import { onDestroy, onMount } from "svelte"
export let application
let promise = getPackage()
let loaded = false
let deletionModal
let unpublishModal
let appName = ""
// App
$: filteredApps = $apps.filter(app => app.devId === application)
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
// Locking
$: lockedBy = selectedApp?.lockedBy
$: lockedByYou = $auth.user.email === lockedBy?.email
$: lockIdentifer = `${
lockedBy && Object.prototype.hasOwnProperty.call(lockedBy, "firstName")
? lockedBy?.firstName
: lockedBy?.email
}`
// App deployments
$: deployments = []
$: latestDeployments = deployments
.filter(
deployment =>
deployment.status === "SUCCESS" && application === deployment.appId
)
.sort((a, b) => a.updatedAt > b.updatedAt)
$: isPublished =
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
$: appUrl = `${window.origin}/app${selectedApp?.url}`
$: tabs = ["Overview", "Automation History", "Backups", "Settings"]
$: selectedTab = "Overview"
const backToAppList = () => {
$goto(`../../../portal/`)
}
const handleTabChange = tabKey => {
if (tabKey === selectedTab) {
return
} else if (tabKey && tabs.indexOf(tabKey) > -1) {
selectedTab = tabKey
} else {
notifications.error("Invalid tab key")
}
}
async function getPackage() {
try {
const pkg = await API.fetchAppPackage(application)
await store.actions.initialise(pkg)
loaded = true
return pkg
} catch (error) {
notifications.error(`Error initialising app: ${error?.message}`)
}
}
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 = () => {
if (isPublished) {
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, {
appId: $store.appId,
eventSource: EventSource.PORTAL,
})
window.open(appUrl, "_blank")
}
}
const editApp = app => {
if (lockedBy && !lockedByYou) {
notifications.warning(
`App locked by ${lockIdentifer}. Please allow lock to expire or have them unlock this app.`
)
return
}
$goto(`../../../app/${app.devId}`)
}
const copyAppId = async app => {
await Helpers.copyToClipboard(app.prodId)
notifications.success("App ID copied to clipboard.")
}
const exportApp = app => {
const id = isPublished ? app.prodId : app.devId
const appName = encodeURIComponent(app.name)
window.location = `/api/backups/export?appId=${id}&appname=${appName}`
}
const unpublishApp = app => {
selectedApp = app
unpublishModal.show()
}
const confirmUnpublishApp = async () => {
if (!selectedApp) {
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 deleteApp = app => {
selectedApp = app
deletionModal.show()
}
const confirmDeleteApp = async () => {
if (!selectedApp) {
return
}
try {
await API.deleteApp(selectedApp?.devId)
backToAppList()
notifications.success("App deleted successfully")
} catch (err) {
notifications.error("Error deleting app")
}
selectedApp = null
appName = null
}
onDestroy(() => {
store.actions.reset()
})
onMount(async () => {
try {
if (!apps.length) {
await apps.load()
}
await API.syncApp(application)
deployments = await fetchDeployments()
} catch (error) {
notifications.error("Error initialising app overview")
}
})
</script>
<span class="overview-wrap">
<Page wide noPadding>
{#await promise}
<span class="page-header">
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
Back
</ActionButton>
</span>
<div class="loading">
<ProgressCircle size="XL" />
</div>
{:then _}
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
<span class="page-header" class:loaded>
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
Back
</ActionButton>
</span>
<div class="overview-header">
<div class="app-title">
<div class="app-logo">
<div
class="app-icon"
style="color: {selectedApp?.icon?.color || ''}"
>
<EditableIcon
app={selectedApp}
size="XL"
name={selectedApp?.icon?.name || "Apps"}
/>
</div>
</div>
<div class="app-details">
<Heading size="M">{selectedApp?.name}</Heading>
<div class="app-url">{appUrl}</div>
</div>
</div>
<div class="header-right">
<AppLockModal app={selectedApp} />
<ButtonGroup gap="XS">
<Button
size="M"
quiet
secondary
icon="Globe"
disabled={!isPublished}
on:click={viewApp}
dataCy="view-app"
>
View app
</Button>
<Button
size="M"
cta
icon="Edit"
disabled={lockedBy && !lockedByYou}
on:click={() => {
editApp(selectedApp)
}}
>
<span>Edit</span>
</Button>
</ButtonGroup>
<ActionMenu align="right" dataCy="app-overview-menu-popover">
<span slot="control" class="app-overview-actions-icon">
<Icon hoverable name="More" />
</span>
<MenuItem on:click={() => exportApp(selectedApp)} icon="Download">
Export
</MenuItem>
{#if isPublished}
<MenuItem
on:click={() => unpublishApp(selectedApp)}
icon="GlobeRemove"
>
Unpublish
</MenuItem>
<MenuItem on:click={() => copyAppId(selectedApp)} icon="Copy">
Copy App ID
</MenuItem>
{/if}
{#if !isPublished}
<MenuItem on:click={() => deleteApp(selectedApp)} icon="Delete">
Delete
</MenuItem>
{/if}
</ActionMenu>
</div>
</div>
</Layout>
<div class="tab-wrap">
<Tabs
selected={selectedTab}
noPadding
on:select={e => {
selectedTab = e.detail
}}
>
<Tab title="Overview">
<OverviewTab
app={selectedApp}
deployments={latestDeployments}
navigateTab={handleTabChange}
/>
</Tab>
{#if false}
<Tab title="Automation History">
<div class="container">Automation History contents</div>
</Tab>
<Tab title="Backups">
<div class="container">Backups contents</div>
</Tab>
{/if}
<Tab title="Settings">
<SettingsTab app={selectedApp} />
</Tab>
</Tabs>
</div>
<ConfirmDialog
bind:this={deletionModal}
title="Confirm deletion"
okText="Delete app"
onOk={confirmDeleteApp}
onCancel={() => (appName = null)}
disabled={appName !== selectedApp?.name}
>
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
<p>Please enter the app name below to confirm.</p>
<Input
bind:value={appName}
data-cy="delete-app-confirmation"
placeholder={selectedApp?.name}
/>
</ConfirmDialog>
<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>
{:catch error}
<p>Something went wrong: {error.message}</p>
{/await}
</Page>
</span>
<style>
.app-url {
color: var(--spectrum-global-color-gray-600);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
}
.overview-header {
display: flex;
justify-content: space-between;
}
.page-header.loaded {
padding: 0px;
}
.overview-wrap :global(> div > .container),
.tab-wrap :global(.spectrum-Tabs) {
background-color: var(--background);
background-clip: padding-box;
}
@media (max-width: 1000px) {
.overview-header {
flex-direction: column;
gap: var(--spacing-l);
}
}
@media (max-width: 640px) {
.overview-wrap :global(.content > *) {
padding: calc(var(--spacing-xl) * 1.5) !important;
}
}
.app-title {
display: flex;
gap: var(--spacing-m);
}
.header-right {
display: flex;
align-items: center;
gap: var(--spacing-xl);
}
.app-details :global(.spectrum-Heading) {
line-height: 1em;
margin-bottom: var(--spacing-s);
}
.tab-wrap :global(.spectrum-Tabs) {
padding-left: var(--spectrum-alias-grid-gutter-large);
padding-right: var(--spectrum-alias-grid-gutter-large);
}
.page-header {
padding-left: var(--spectrum-alias-grid-gutter-large);
padding-right: var(--spectrum-alias-grid-gutter-large);
padding-top: var(--spectrum-alias-grid-gutter-large);
}
</style>

View File

@ -0,0 +1,11 @@
<script>
//export let app
</script>
<div class="automation-tab" />
<style>
.automation-tab {
color: pink;
}
</style>

View File

@ -0,0 +1,250 @@
<script>
import DashCard from "components/common/DashCard.svelte"
import { AppStatus } from "constants"
import {
Icon,
Heading,
Link,
Avatar,
notifications,
Layout,
} from "@budibase/bbui"
import { store } from "builderStore"
import clientPackage from "@budibase/client/package.json"
import { processStringSync } from "@budibase/string-templates"
import { users, auth } from "stores/portal"
export let app
export let deployments
export let navigateTab
const userInit = async () => {
try {
await users.init()
} catch (error) {
notifications.error("Error getting user list")
}
}
let userPromise = userInit()
$: updateAvailable = clientPackage.version !== $store.version
$: isPublished = app && app?.status === AppStatus.DEPLOYED
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
$: appEditorText = appEditor?.firstName || appEditor?.email
$: filteredUsers = !appEditorId
? []
: $users.filter(user => user._id === appEditorId)
$: appEditor = filteredUsers.length ? filteredUsers[0] : null
const getInitials = user => {
let initials = ""
initials += user.firstName ? user.firstName[0] : ""
initials += user.lastName ? user.lastName[0] : ""
return initials == "" ? user.email[0] : initials
}
</script>
<div class="overview-tab">
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
<div class="top">
<DashCard title={"App Status"} dataCy={"app-status"}>
<div class="status-content">
<div class="status-display">
{#if isPublished}
<Icon name="GlobeCheck" size="XL" disabled={false} />
<span>Published</span>
{:else}
<Icon name="GlobeStrike" size="XL" disabled={true} />
<span class="disabled"> Unpublished </span>
{/if}
</div>
<div class="status-text">
{#if deployments?.length}
{processStringSync(
"Last published {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(deployments[0].updatedAt).getTime(),
}
)}
{/if}
{#if !deployments?.length}
-
{/if}
</div>
</div>
</DashCard>
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
<div class="last-edited-content">
{#await userPromise}
<Avatar size="M" initials={"-"} />
{:then _}
<div class="updated-by">
{#if appEditor}
<Avatar size="M" initials={getInitials(appEditor)} />
<div class="editor-name">
{appEditor._id === $auth.user._id ? "You" : appEditorText}
</div>
{/if}
</div>
{:catch error}
<p>Could not fetch user: {error.message}</p>
{/await}
<div class="last-edit-text">
{#if app}
{processStringSync(
"Last edited {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() - new Date(app?.updatedAt).getTime(),
}
)}
{/if}
</div>
</div>
</DashCard>
<DashCard
title={"App Version"}
showIcon={true}
action={() => {
navigateTab("Settings")
}}
dataCy={"app-version"}
>
<div class="version-content" data-cy={$store.version}>
<Heading size="XS">{$store.version}</Heading>
{#if updateAvailable}
<div class="version-status">
New version <strong>{clientPackage.version}</strong> is available
-
<Link
on:click={() => {
if (typeof navigateTab === "function") {
navigateTab("Settings")
}
}}
>
Update
</Link>
</div>
{:else}
<div class="version-status">You're running the latest!</div>
{/if}
</div>
</DashCard>
</div>
{#if false}
<div class="bottom">
<DashCard
title={"Automation History"}
action={() => {
navigateTab("Automation History")
}}
dataCy={"automation-history"}
>
<div class="automation-content">
<div class="automation-metrics">
<div class="succeeded">
<Heading size="XL">0</Heading>
<div class="metric-info">
<Icon name="CheckmarkCircle" />
Success
</div>
</div>
<div class="failed">
<Heading size="XL">0</Heading>
<div class="metric-info">
<Icon name="Alert" />
Error
</div>
</div>
</div>
</div>
</DashCard>
<DashCard
title={"Backups"}
action={() => {
navigateTab("Backups")
}}
dataCy={"backups"}
>
<div class="backups-content">test</div>
</DashCard>
</div>
{/if}
</Layout>
</div>
<style>
.overview-tab {
display: grid;
}
.overview-tab .top {
display: grid;
grid-gap: var(--spectrum-alias-grid-gutter-medium);
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
}
.overview-tab .bottom,
.automation-metrics {
display: grid;
grid-gap: var(--spectrum-alias-grid-gutter-large);
grid-template-columns: 1fr 1fr;
}
@media (max-width: 1000px) {
.overview-tab .top {
grid-template-columns: 1fr 1fr;
}
.overview-tab .bottom {
grid-template-columns: 1fr;
}
}
@media (max-width: 800px) {
.overview-tab .top,
.overview-tab .bottom {
grid-template-columns: 1fr;
}
}
.status-display {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.status-text,
.last-edit-text {
color: var(--spectrum-global-color-gray-600);
}
.updated-by {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.succeeded :global(.icon) {
color: var(--spectrum-global-color-green-600);
}
.failed :global(.icon) {
color: var(
--spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500)
);
}
.metric-info {
display: flex;
gap: var(--spacing-l);
margin-top: var(--spacing-s);
}
.version-status,
.last-edit-text,
.status-text {
padding-top: var(--spacing-xl);
}
</style>

View File

@ -0,0 +1,131 @@
<script>
import {
Layout,
Divider,
Heading,
Body,
Page,
Button,
Modal,
} from "@budibase/bbui"
import { store } from "builderStore"
import clientPackage from "@budibase/client/package.json"
import VersionModal from "components/deploy/VersionModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
import { AppStatus } from "constants"
export let app
let versionModal
let updatingModal
let selfHostPath =
"https://docs.budibase.com/docs/hosting-methods#self-host-budibase"
$: updateAvailable = clientPackage.version !== $store.version
$: appUrl = `${window.origin}/app${app?.url}`
$: appDeployed = app.status === AppStatus.DEPLOYED
</script>
<div class="settings-tab">
<Page wide={false}>
<Layout gap="XL" paddingY="XXL" paddingX="">
<span class="details-section">
<Layout gap="XS" noPadding>
<Heading size="S">Name and URL</Heading>
<Divider />
<Body>
<div class="app-details">
<div class="app-name">
<div class="name-title detail-title">Name</div>
<div class="name">{app?.name}</div>
</div>
<div class="app-url">
<div class="url-title detail-title">Url Path</div>
<div class="url">{appUrl}</div>
</div>
</div>
<div class="page-action">
<Button
cta
secondary
on:click={() => {
updatingModal.show()
}}
disabled={appDeployed}
>
Edit
</Button>
</div>
</Body>
</Layout>
</span>
<span class="version-section">
<Layout gap="XS" paddingY="XXL" paddingX="">
<Heading size="S">App version</Heading>
<Divider />
<Body>
{#if updateAvailable}
<p class="version-status">
The app is currently using version
<strong>{$store.version}</strong>
but version <strong>{clientPackage.version}</strong> is available.
</p>
{:else}
<p class="version-status">
The app is currently using version
<strong>{$store.version}</strong>. You're running the latest!
</p>
{/if}
Updates can contain new features, performance improvements and bug
fixes.
<div class="page-action">
<Button cta on:click={versionModal.show()}>Update app</Button>
</div>
</Body>
</Layout>
</span>
<span class="selfhost-section">
<Layout gap="XS" paddingY="XXL" paddingX="">
<Heading size="S">Self-host Budibase</Heading>
<Divider />
<Body>
Self-host Budibase for free to get unlimited apps and more - and it
only takes a few minutes!
<div class="page-action">
<Button
secondary
on:click={() => {
window.open(selfHostPath, "_blank")
}}>Self-host Budibase</Button
>
</div>
</Body>
</Layout>
</span>
</Layout>
<VersionModal bind:this={versionModal} hideIcon={true} />
<Modal bind:this={updatingModal} padding={false} width="600px">
<UpdateAppModal {app} />
</Modal>
</Page>
</div>
<style>
.page-action {
padding-top: var(--spacing-xl);
}
.app-details {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.detail-title {
color: var(--spectrum-global-color-gray-600);
font-size: var(
--spectrum-alias-font-size-default,
var(--spectrum-global-dimension-font-size-100)
);
}
</style>

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "^1.0.177",
"@budibase/frontend-core": "^1.0.177",
"@budibase/string-templates": "^1.0.177",
"@budibase/bbui": "^1.0.178-alpha.0",
"@budibase/frontend-core": "^1.0.178-alpha.0",
"@budibase/string-templates": "^1.0.178-alpha.0",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View File

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "^1.0.177",
"@budibase/bbui": "^1.0.178-alpha.0",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -70,10 +70,10 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.177",
"@budibase/client": "^1.0.177",
"@budibase/pro": "^1.0.175",
"@budibase/string-templates": "^1.0.177",
"@budibase/backend-core": "^1.0.178-alpha.0",
"@budibase/client": "^1.0.178-alpha.0",
"@budibase/pro": "1.0.178-alpha.0",
"@budibase/string-templates": "^1.0.178-alpha.0",
"@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0",

View File

@ -6,7 +6,7 @@ const {
setDebounce,
} = require("../utilities/redis")
const { doWithDB } = require("@budibase/backend-core/db")
const { DocumentTypes } = require("../db/utils")
const { DocumentTypes, getGlobalIDFromUserMetadataID } = require("../db/utils")
const { PermissionTypes } = require("@budibase/backend-core/permissions")
const { app: appCache } = require("@budibase/backend-core/cache")
@ -51,6 +51,9 @@ async function updateAppUpdatedAt(ctx) {
await doWithDB(appId, async db => {
const metadata = await db.get(DocumentTypes.APP_METADATA)
metadata.updatedAt = new Date().toISOString()
metadata.updatedBy = getGlobalIDFromUserMetadataID(ctx.user.userId)
const response = await db.put(metadata)
metadata._rev = response.rev
await appCache.invalidateAppMetadata(appId, metadata)
@ -67,7 +70,15 @@ module.exports = async (ctx, permType) => {
}
const isBuilderApi = permType === PermissionTypes.BUILDER
const referer = ctx.headers["referer"]
const editingApp = referer ? referer.includes(appId) : false
const overviewPath = "/builder/portal/overview/"
const overviewContext = !referer ? false : referer.includes(overviewPath)
if (overviewContext) {
return
}
const hasAppId = !referer ? false : referer.includes(appId)
const editingApp = referer ? hasAppId : false
// check this is a builder call and editing
if (!isBuilderApi || !editingApp) {
return

View File

@ -48,7 +48,9 @@ exports.updateLock = async (devAppId, user) => {
...user,
userId: globalId,
_id: globalId,
lockedAt: new Date().getTime(),
}
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
}

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "1.0.177",
"version": "1.0.178-alpha.0",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -32,9 +32,9 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "^1.0.177",
"@budibase/pro": "^1.0.175",
"@budibase/string-templates": "^1.0.177",
"@budibase/backend-core": "^1.0.178-alpha.0",
"@budibase/pro": "1.0.178-alpha.0",
"@budibase/string-templates": "^1.0.178-alpha.0",
"@koa/router": "^8.0.0",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "^0.3.0",