Merge branch 'feature/app-overview-section' of github.com:Budibase/budibase into feature/automation-logs
This commit is contained in:
commit
91e65a9d65
|
@ -7,6 +7,7 @@
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let selected = false
|
export let selected = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let dataCy
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
class:is-selected={selected}
|
class:is-selected={selected}
|
||||||
class:is-disabled={disabled}
|
class:is-disabled={disabled}
|
||||||
on:click
|
on:click
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
{#if heading}
|
{#if heading}
|
||||||
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">
|
<h2 class="spectrum-SideNav-heading" id="nav-heading-{heading}">
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let selected = getContext("tab")
|
let selected = getContext("tab")
|
||||||
let tab
|
let tab_internal
|
||||||
let tabInfo
|
let tabInfo
|
||||||
|
|
||||||
const setTabInfo = () => {
|
const setTabInfo = () => {
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
// We just need to get this off the main thread to fix this, by using
|
// We just need to get this off the main thread to fix this, by using
|
||||||
// a 0ms timeout.
|
// a 0ms timeout.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
tabInfo = tab?.getBoundingClientRect()
|
tabInfo = tab_internal?.getBoundingClientRect()
|
||||||
if (tabInfo && $selected.title === title) {
|
if (tabInfo && $selected.title === title) {
|
||||||
$selected.info = tabInfo
|
$selected.info = tabInfo
|
||||||
}
|
}
|
||||||
|
@ -27,14 +27,30 @@
|
||||||
setTabInfo()
|
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 = () => {
|
const onClick = () => {
|
||||||
$selected = { ...$selected, title, info: tab.getBoundingClientRect() }
|
$selected = {
|
||||||
|
...$selected,
|
||||||
|
title,
|
||||||
|
info: tab_internal.getBoundingClientRect(),
|
||||||
|
}
|
||||||
dispatch("click")
|
dispatch("click")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={tab}
|
bind:this={tab_internal}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
class:is-selected={$selected.title === title}
|
class:is-selected={$selected.title === title}
|
||||||
class="spectrum-Tabs-item"
|
class="spectrum-Tabs-item"
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
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")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
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.deleteAllApps()
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
|
@ -19,7 +19,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Preview")
|
cy.get(".spectrum-Button").contains("View")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("View app")
|
cy.get(".spectrum-Button").contains("View")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ filterTests(['all'], () => {
|
||||||
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("[data-cy='publish-popover-action']").should("exist")
|
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")
|
cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -84,7 +84,7 @@ filterTests(['all'], () => {
|
||||||
|
|
||||||
cy.get(".appTable .app-row-actions").eq(0)
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Button").contains("View app")
|
cy.get(".spectrum-Button").contains("View")
|
||||||
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -112,19 +112,9 @@ filterTests(['all'], () => {
|
||||||
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
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-Menu-item").contains("Edit").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Modal")
|
|
||||||
.within(() => {
|
cy.updateAppName(changedName, noName)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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", () => {
|
Cypress.Commands.add("closeModal", () => {
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".close-icon").click()
|
cy.get(".close-icon").click()
|
||||||
|
@ -140,6 +149,86 @@ 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("createTestApp", () => {
|
Cypress.Commands.add("createTestApp", () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.deleteApp(appName)
|
cy.deleteApp(appName)
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
<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}`
|
||||||
|
|
||||||
|
$: lockExpiry = getExpiryDuration(app)
|
||||||
|
|
||||||
|
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 && lockExpiry > 0}
|
||||||
|
<span class="lock-expiry-body">
|
||||||
|
{processStringSync(
|
||||||
|
"This lock will expire in {{ duration time 'millisecond' }} from now",
|
||||||
|
{
|
||||||
|
time: lockExpiry,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</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>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<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={actionDefined ? "dash-card-header active" : "dash-card-header"}
|
||||||
|
on:click={() => {
|
||||||
|
if (actionDefined) {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="dash-card-title">
|
||||||
|
<Detail size="M">{title}</Detail>
|
||||||
|
</span>
|
||||||
|
<span class="dash-card-action">
|
||||||
|
{#if actionDefined}
|
||||||
|
<Icon name={actionIcon ? 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);
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
.dash-card-header {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.dash-card-body {
|
||||||
|
padding: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.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>
|
|
@ -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>
|
|
@ -11,6 +11,16 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import clientPackage from "@budibase/client/package.json"
|
import clientPackage from "@budibase/client/package.json"
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
updateModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
updateModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
export let hideIcon = false
|
||||||
|
|
||||||
let updateModal
|
let updateModal
|
||||||
|
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
|
@ -57,9 +67,11 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
{#if !hideIcon}
|
||||||
|
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
||||||
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<Modal bind:this={updateModal}>
|
<Modal bind:this={updateModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="App version"
|
title="App version"
|
||||||
|
|
|
@ -1,33 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||||
Heading,
|
import AppLockModal from "../common/AppLockModal.svelte"
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
StatusLight,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let exportApp
|
export let exportApp
|
||||||
export let viewApp
|
|
||||||
export let editApp
|
export let editApp
|
||||||
export let updateApp
|
export let updateApp
|
||||||
export let deleteApp
|
export let deleteApp
|
||||||
export let previewApp
|
|
||||||
export let unpublishApp
|
export let unpublishApp
|
||||||
|
export let appOverview
|
||||||
export let releaseLock
|
export let releaseLock
|
||||||
export let editIcon
|
export let editIcon
|
||||||
export let copyAppId
|
export let copyAppId
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title" data-cy={`${app.devId}`}>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
||||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||||
</div>
|
</div>
|
||||||
<div class="name" on:click={() => editApp(app)}>
|
<div class="name" on:click={() => appOverview(app)}>
|
||||||
<Heading size="XS">
|
<Heading size="XS">
|
||||||
{app.name}
|
{app.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
@ -44,19 +37,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<StatusLight
|
<AppLockModal {app} buttonSize="S" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<div class="app-status">
|
<div class="app-status">
|
||||||
|
@ -71,23 +52,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div data-cy={`row_actions_${app.appId}`}>
|
<div data-cy={`row_actions_${app.appId}`}>
|
||||||
<div class="app-row-actions">
|
<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
|
<Button
|
||||||
size="S"
|
size="S"
|
||||||
cta
|
secondary
|
||||||
|
quiet
|
||||||
disabled={app.lockedOther}
|
disabled={app.lockedOther}
|
||||||
on:click={() => editApp(app)}
|
on:click={() => editApp(app)}
|
||||||
>
|
>Edit
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="S" cta on:click={() => appOverview(app)}>View</Button>
|
||||||
</div>
|
</div>
|
||||||
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
||||||
<span slot="control" class="app-row-actions-icon">
|
<span slot="control" class="app-row-actions-icon">
|
||||||
|
@ -123,6 +96,7 @@
|
||||||
}
|
}
|
||||||
.app-status {
|
.app-status {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
grid-template-columns: 24px 100px;
|
grid-template-columns: 24px 100px;
|
||||||
}
|
}
|
||||||
.app-status span.disabled {
|
.app-status span.disabled {
|
||||||
|
|
|
@ -169,7 +169,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-dropdown">
|
<div class="user-dropdown">
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right" dataCy="user-menu">
|
||||||
<div slot="control" class="avatar">
|
<div slot="control" class="avatar">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="M"
|
size="M"
|
||||||
|
@ -195,7 +195,9 @@
|
||||||
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
|
||||||
Close developer mode
|
Close developer mode
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon="LogOut" on:click={logout}>Log out</MenuItem>
|
<MenuItem dataCy="user-logout" icon="LogOut" on:click={logout}
|
||||||
|
>Log out
|
||||||
|
</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
Layout,
|
Layout,
|
||||||
Detail,
|
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
@ -183,6 +182,10 @@
|
||||||
window.open(`/${app.devId}`)
|
window.open(`/${app.devId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appOverview = app => {
|
||||||
|
$goto(`../overview/${app.devId}`)
|
||||||
|
}
|
||||||
|
|
||||||
const editApp = app => {
|
const editApp = app => {
|
||||||
if (app.lockedOther) {
|
if (app.lockedOther) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
|
@ -304,7 +307,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="XL">
|
<Layout noPadding gap="M">
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
|
@ -314,7 +317,39 @@
|
||||||
{welcomeBody}
|
{welcomeBody}
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
{#if !$apps?.length}
|
||||||
|
<div class="buttons">
|
||||||
|
<Button
|
||||||
|
dataCy="create-app-btn"
|
||||||
|
size="M"
|
||||||
|
icon="Add"
|
||||||
|
cta
|
||||||
|
on:click={initiateAppCreation}
|
||||||
|
>
|
||||||
|
{createAppButtonText}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
dataCy="import-app-btn"
|
||||||
|
icon="Import"
|
||||||
|
size="L"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={initiateAppImport}
|
||||||
|
>
|
||||||
|
Import app
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !$apps?.length && $templates?.length}
|
||||||
|
<TemplateDisplay templates={$templates} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if enrichedApps.length}
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<div class="title">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
dataCy="create-app-btn"
|
dataCy="create-app-btn"
|
||||||
|
@ -349,23 +384,6 @@
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</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}
|
|
||||||
<TemplateDisplay templates={$templates} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if enrichedApps.length}
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div class="title">
|
|
||||||
<Detail size="L">Apps</Detail>
|
|
||||||
{#if enrichedApps.length > 1}
|
{#if enrichedApps.length > 1}
|
||||||
<div class="app-actions">
|
<div class="app-actions">
|
||||||
{#if cloud}
|
{#if cloud}
|
||||||
|
@ -396,7 +414,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<Divider size="S" />
|
||||||
<div class="appTable">
|
<div class="appTable">
|
||||||
{#each filteredApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<AppRow
|
<AppRow
|
||||||
|
@ -411,6 +429,7 @@
|
||||||
{deleteApp}
|
{deleteApp}
|
||||||
{updateApp}
|
{updateApp}
|
||||||
{previewApp}
|
{previewApp}
|
||||||
|
{appOverview}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -478,7 +497,7 @@
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.title .welcome > .buttons {
|
.title .welcome > .buttons {
|
||||||
padding-top: 30px;
|
padding-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Page,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Heading,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
notifications,
|
||||||
|
ProgressCircle,
|
||||||
|
} 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 { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
|
||||||
|
export let application
|
||||||
|
|
||||||
|
let promise = getPackage()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
store.actions.reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
if (!apps.length) {
|
||||||
|
await apps.load()
|
||||||
|
}
|
||||||
|
deployments = await fetchDeployments()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error initialising app overview")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page wide>
|
||||||
|
<Layout noPadding gap="XL">
|
||||||
|
<span>
|
||||||
|
<Button quiet secondary icon={"ChevronLeft"} on:click={backToAppList}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
{#await promise}
|
||||||
|
<div class="loading">
|
||||||
|
<ProgressCircle size="XL" />
|
||||||
|
</div>
|
||||||
|
{:then _}
|
||||||
|
<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"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
{:catch error}
|
||||||
|
<p>Something went wrong: {error.message}</p>
|
||||||
|
{/await}
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-url {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.overview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script>
|
||||||
|
//export let app
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="automation-tab" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.automation-tab {
|
||||||
|
color: pink;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,217 @@
|
||||||
|
<script>
|
||||||
|
import DashCard from "components/common/DashCard.svelte"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import { Icon, Heading, Link, Avatar, notifications } 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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<p 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}
|
||||||
|
</p>
|
||||||
|
</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}
|
||||||
|
<p class="last-edit-text">
|
||||||
|
{#if app}
|
||||||
|
{processStringSync(
|
||||||
|
"Last edited {{ duration time 'millisecond' }} ago",
|
||||||
|
{
|
||||||
|
time: new Date().getTime() - new Date(app?.updatedAt).getTime(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</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}
|
||||||
|
<p class="version-status">
|
||||||
|
New version <strong>{clientPackage.version}</strong> is available -
|
||||||
|
<Link
|
||||||
|
on:click={() => {
|
||||||
|
if (typeof navigateTab === "function") {
|
||||||
|
navigateTab("Settings")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="version-status">You're running the latest!</p>
|
||||||
|
{/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}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Add in size checks */
|
||||||
|
.overview-tab {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.overview-tab .top {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
|
||||||
|
}
|
||||||
|
.overview-tab .bottom,
|
||||||
|
.automation-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-xl);
|
||||||
|
grid-template-columns: 1fr 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);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,133 @@
|
||||||
|
<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" noPadding>
|
||||||
|
<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" noPadding>
|
||||||
|
<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}
|
||||||
|
<p>
|
||||||
|
Updates can contain new features, performance improvements and bug
|
||||||
|
fixes.
|
||||||
|
</p>
|
||||||
|
<div class="page-action">
|
||||||
|
<Button cta on:click={versionModal.show()}>Update App</Button>
|
||||||
|
</div>
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
</span>
|
||||||
|
<span class="selfhost-section">
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Self-host Budibase</Heading>
|
||||||
|
<Divider />
|
||||||
|
<Body>
|
||||||
|
<p>
|
||||||
|
Self-host Budibase for free to get unlimited apps and more - and
|
||||||
|
it only takes a few minutes!
|
||||||
|
</p>
|
||||||
|
<div class="page-action">
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
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>
|
|
@ -6,7 +6,7 @@ const {
|
||||||
setDebounce,
|
setDebounce,
|
||||||
} = require("../utilities/redis")
|
} = require("../utilities/redis")
|
||||||
const { doWithDB } = require("@budibase/backend-core/db")
|
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 { PermissionTypes } = require("@budibase/backend-core/permissions")
|
||||||
const { app: appCache } = require("@budibase/backend-core/cache")
|
const { app: appCache } = require("@budibase/backend-core/cache")
|
||||||
|
|
||||||
|
@ -51,6 +51,9 @@ async function updateAppUpdatedAt(ctx) {
|
||||||
await doWithDB(appId, async db => {
|
await doWithDB(appId, async db => {
|
||||||
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
||||||
metadata.updatedAt = new Date().toISOString()
|
metadata.updatedAt = new Date().toISOString()
|
||||||
|
|
||||||
|
metadata.updatedBy = getGlobalIDFromUserMetadataID(ctx.user.userId)
|
||||||
|
|
||||||
const response = await db.put(metadata)
|
const response = await db.put(metadata)
|
||||||
metadata._rev = response.rev
|
metadata._rev = response.rev
|
||||||
await appCache.invalidateAppMetadata(appId, metadata)
|
await appCache.invalidateAppMetadata(appId, metadata)
|
||||||
|
@ -67,7 +70,15 @@ module.exports = async (ctx, permType) => {
|
||||||
}
|
}
|
||||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||||
const referer = ctx.headers["referer"]
|
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
|
// check this is a builder call and editing
|
||||||
if (!isBuilderApi || !editingApp) {
|
if (!isBuilderApi || !editingApp) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -48,7 +48,9 @@ exports.updateLock = async (devAppId, user) => {
|
||||||
...user,
|
...user,
|
||||||
userId: globalId,
|
userId: globalId,
|
||||||
_id: globalId,
|
_id: globalId,
|
||||||
|
lockedAt: new Date().getTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
|
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue