forked from tsai/budibase
111 changed files with 3461 additions and 1024 deletions
@ -1,4 +1,7 @@ |
|||
const generic = require("./src/cache/generic") |
|||
|
|||
module.exports = { |
|||
user: require("./src/cache/user"), |
|||
app: require("./src/cache/appMetadata"), |
|||
...generic, |
|||
} |
|||
|
|||
@ -0,0 +1,49 @@ |
|||
const redis = require("../redis/authRedis") |
|||
const env = require("../environment") |
|||
const { getTenantId } = require("../context") |
|||
|
|||
exports.CacheKeys = { |
|||
CHECKLIST: "checklist", |
|||
} |
|||
|
|||
exports.TTL = { |
|||
ONE_MINUTE: 600, |
|||
ONE_HOUR: 3600, |
|||
ONE_DAY: 86400, |
|||
} |
|||
|
|||
function generateTenantKey(key) { |
|||
const tenantId = getTenantId() |
|||
return `${key}:${tenantId}` |
|||
} |
|||
|
|||
exports.withCache = async (key, ttl, fetchFn) => { |
|||
key = generateTenantKey(key) |
|||
const client = await redis.getCacheClient() |
|||
const cachedValue = await client.get(key) |
|||
if (cachedValue) { |
|||
return cachedValue |
|||
} |
|||
|
|||
try { |
|||
const fetchedValue = await fetchFn() |
|||
|
|||
if (!env.isTest()) { |
|||
await client.store(key, fetchedValue, ttl) |
|||
} |
|||
return fetchedValue |
|||
} catch (err) { |
|||
console.error("Error fetching before cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
|
|||
exports.bustCache = async key => { |
|||
const client = await redis.getCacheClient() |
|||
try { |
|||
await client.delete(generateTenantKey(key)) |
|||
} catch (err) { |
|||
console.error("Error busting cache - ", err) |
|||
throw err |
|||
} |
|||
} |
|||
@ -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() |
|||
}) |
|||
|
|||
}) |
|||
}) |
|||
@ -0,0 +1,44 @@ |
|||
import filterTests from "../../../support/filterTests" |
|||
|
|||
filterTests(["all"], () => { |
|||
context("Lead Generation Form Template Functionality", () => { |
|||
const templateName = "Lead Generation Form" |
|||
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') |
|||
|
|||
before(() => { |
|||
cy.login() |
|||
cy.deleteApp(templateName) |
|||
// Template navigation
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { |
|||
onBeforeLoad(win) { |
|||
cy.stub(win, 'open') |
|||
} |
|||
}) |
|||
cy.wait(2000) |
|||
}) |
|||
|
|||
it("should create and publish app with Lead Generation Form template", () => { |
|||
// Select Lead Generation Form template
|
|||
cy.get(".template-thumbnail-text") |
|||
.contains(templateName).parentsUntil(".template-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Use template").click({ force: true }) |
|||
}) |
|||
|
|||
// Confirm URL matches template name
|
|||
const appUrl = cy.get(".app-server") |
|||
appUrl.invoke('text').then(appUrlText => { |
|||
expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
|||
}) |
|||
|
|||
// Create App
|
|||
cy.get(".spectrum-Dialog-grid").within(() => { |
|||
cy.get(".spectrum-Button").contains("Create app").click({ force: true }) |
|||
}) |
|||
|
|||
// Publish App & Verify it opened
|
|||
cy.wait(2000) // Wait for app to generate
|
|||
cy.publishApp(true) |
|||
cy.window().its('open').should('be.calledOnce') |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,111 @@ |
|||
<script> |
|||
import { automationStore } from "builderStore" |
|||
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui" |
|||
import { externalActions } from "./ExternalActions" |
|||
|
|||
export let block |
|||
export let blockComplete |
|||
export let showTestStatus = false |
|||
export let showParameters = {} |
|||
|
|||
$: testResult = |
|||
$automationStore.selectedAutomation?.testResults?.steps.filter(step => |
|||
block.id ? step.id === block.id : step.stepId === block.stepId |
|||
) |
|||
$: isTrigger = block.type === "TRIGGER" |
|||
|
|||
async function onSelect(block) { |
|||
await automationStore.update(state => { |
|||
state.selectedBlock = block |
|||
return state |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<div class="blockSection"> |
|||
<div |
|||
on:click={() => { |
|||
blockComplete = !blockComplete |
|||
showParameters[block.id] = blockComplete |
|||
}} |
|||
class="splitHeader" |
|||
> |
|||
<div class="center-items"> |
|||
{#if externalActions[block.stepId]} |
|||
<img |
|||
alt={externalActions[block.stepId].name} |
|||
width="28px" |
|||
height="28px" |
|||
src={externalActions[block.stepId].icon} |
|||
/> |
|||
{:else} |
|||
<svg |
|||
width="28px" |
|||
height="28px" |
|||
class="spectrum-Icon" |
|||
style="color:grey;" |
|||
focusable="false" |
|||
> |
|||
<use xlink:href="#spectrum-icon-18-{block.icon}" /> |
|||
</svg> |
|||
{/if} |
|||
<div class="iconAlign"> |
|||
{#if isTrigger} |
|||
<Body size="XS">When this happens:</Body> |
|||
{:else} |
|||
<Body size="XS">Do this:</Body> |
|||
{/if} |
|||
|
|||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> |
|||
</div> |
|||
</div> |
|||
<div class="blockTitle"> |
|||
{#if showTestStatus && testResult && testResult[0]} |
|||
<div style="float: right;"> |
|||
<StatusLight |
|||
positive={isTrigger || testResult[0].outputs?.success} |
|||
negative={!testResult[0].outputs?.success} |
|||
><Body size="XS" |
|||
>{testResult[0].outputs?.success || isTrigger |
|||
? "Success" |
|||
: "Error"}</Body |
|||
></StatusLight |
|||
> |
|||
</div> |
|||
{/if} |
|||
<div |
|||
style="margin-left: 10px; margin-bottom: var(--spacing-xs);" |
|||
on:click={() => { |
|||
onSelect(block) |
|||
}} |
|||
> |
|||
<Icon name={blockComplete ? "ChevronUp" : "ChevronDown"} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.center-items { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.splitHeader { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.iconAlign { |
|||
padding: 0 0 0 var(--spacing-m); |
|||
display: inline-block; |
|||
} |
|||
|
|||
.blockSection { |
|||
padding: var(--spacing-xl); |
|||
} |
|||
|
|||
.blockTitle { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
@ -1,133 +0,0 @@ |
|||
<script> |
|||
import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui" |
|||
|
|||
export let testResult |
|||
export let isTrigger |
|||
let inputToggled |
|||
let outputToggled |
|||
</script> |
|||
|
|||
<ModalContent |
|||
showCloseIcon={false} |
|||
showConfirmButton={false} |
|||
cancelText="Close" |
|||
> |
|||
<div slot="header" class="result-modal-header"> |
|||
<span>Test Results</span> |
|||
<div> |
|||
{#if isTrigger || testResult[0].outputs.success} |
|||
<div class="iconSuccess"> |
|||
<Icon size="S" name="CheckmarkCircle" /> |
|||
</div> |
|||
{:else} |
|||
<div class="iconFailure"> |
|||
<Icon size="S" name="CloseCircle" /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
<span> |
|||
{#if testResult[0].outputs.iterations} |
|||
<div style="display: flex;"> |
|||
<Icon name="Reuse" /> |
|||
<div style="margin-left: 10px;"> |
|||
<Label> |
|||
This loop ran {testResult[0].outputs.iterations} times.</Label |
|||
> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
</span> |
|||
<div |
|||
on:click={() => { |
|||
inputToggled = !inputToggled |
|||
}} |
|||
class="toggle splitHeader" |
|||
> |
|||
<div> |
|||
<div style="display: flex; align-items: center;"> |
|||
<span style="padding-left: var(--spacing-s);"> |
|||
<Detail size="S">Input</Detail> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
{#if inputToggled} |
|||
<Icon size="M" name="ChevronDown" /> |
|||
{:else} |
|||
<Icon size="M" name="ChevronRight" /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{#if inputToggled} |
|||
<div class="text-area-container"> |
|||
<TextArea |
|||
disabled |
|||
value={JSON.stringify(testResult[0].inputs, null, 2)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
|
|||
<div |
|||
on:click={() => { |
|||
outputToggled = !outputToggled |
|||
}} |
|||
class="toggle splitHeader" |
|||
> |
|||
<div> |
|||
<div style="display: flex; align-items: center;"> |
|||
<span style="padding-left: var(--spacing-s);"> |
|||
<Detail size="S">Output</Detail> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
{#if outputToggled} |
|||
<Icon size="M" name="ChevronDown" /> |
|||
{:else} |
|||
<Icon size="M" name="ChevronRight" /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{#if outputToggled} |
|||
<div class="text-area-container"> |
|||
<TextArea |
|||
disabled |
|||
value={JSON.stringify(testResult[0].outputs, null, 2)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.result-modal-header { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
width: 100%; |
|||
} |
|||
|
|||
.iconSuccess { |
|||
color: var(--spectrum-global-color-green-600); |
|||
} |
|||
|
|||
.iconFailure { |
|||
color: var(--spectrum-global-color-red-600); |
|||
} |
|||
|
|||
.splitHeader { |
|||
cursor: pointer; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.toggle { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.text-area-container :global(textarea) { |
|||
height: 150px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,146 @@ |
|||
<script> |
|||
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui" |
|||
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte" |
|||
import { automationStore } from "builderStore" |
|||
|
|||
export let automation |
|||
|
|||
let showParameters |
|||
let blocks |
|||
|
|||
$: { |
|||
blocks = [] |
|||
if (automation) { |
|||
if (automation.definition.trigger) { |
|||
blocks.push(automation.definition.trigger) |
|||
} |
|||
blocks = blocks |
|||
.concat(automation.definition.steps || []) |
|||
.filter(x => x.stepId !== "LOOP") |
|||
} |
|||
} |
|||
|
|||
$: testResults = |
|||
$automationStore.selectedAutomation?.testResults?.steps.filter( |
|||
x => x.stepId !== "LOOP" || [] |
|||
) |
|||
</script> |
|||
|
|||
<div class="title"> |
|||
<div class="title-text"> |
|||
<Icon name="MultipleCheck" /> |
|||
<div style="padding-left: var(--spacing-l)">Test Details</div> |
|||
</div> |
|||
<div style="padding-right: var(--spacing-xl)"> |
|||
<Icon |
|||
on:click={async () => { |
|||
$automationStore.selectedAutomation.automation.showTestPanel = false |
|||
}} |
|||
hoverable |
|||
name="Close" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<Divider /> |
|||
|
|||
<div class="container"> |
|||
{#each blocks as block, idx} |
|||
<div class="block"> |
|||
{#if block.stepId !== "LOOP"} |
|||
<FlowItemHeader showTestStatus={true} bind:showParameters {block} /> |
|||
{#if showParameters && showParameters[block.id]} |
|||
<Divider noMargin /> |
|||
{#if testResults?.[idx]?.outputs.iterations} |
|||
<div style="display: flex; padding: 10px 10px 0px 12px;"> |
|||
<Icon name="Reuse" /> |
|||
<div style="margin-left: 10px;"> |
|||
<Label> |
|||
This loop ran {testResults?.[idx]?.outputs.iterations} times.</Label |
|||
> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
|
|||
<div class="tabs"> |
|||
<Tabs quiet noPadding selected="Input"> |
|||
<Tab title="Input"> |
|||
<div style="padding: 10px 10px 10px 10px;"> |
|||
<TextArea |
|||
minHeight="80px" |
|||
disabled |
|||
value={JSON.stringify(testResults?.[idx]?.inputs, null, 2)} |
|||
/> |
|||
</div></Tab |
|||
> |
|||
<Tab title="Output"> |
|||
<div style="padding: 10px 10px 10px 10px;"> |
|||
<TextArea |
|||
minHeight="100px" |
|||
disabled |
|||
value={JSON.stringify(testResults?.[idx]?.outputs, null, 2)} |
|||
/> |
|||
</div> |
|||
</Tab> |
|||
</Tabs> |
|||
</div> |
|||
{/if} |
|||
{/if} |
|||
</div> |
|||
{#if blocks.length - 1 !== idx} |
|||
<div class="separator" /> |
|||
{/if} |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
padding: 0px 30px 0px 30px; |
|||
} |
|||
.title { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
gap: var(--spacing-xs); |
|||
padding-left: var(--spacing-xl); |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.tabs { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
position: relative; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.title-text { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
} |
|||
|
|||
.title :global(h1) { |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.block { |
|||
display: inline-block; |
|||
width: 400px; |
|||
font-size: 16px; |
|||
background-color: var(--background); |
|||
border: 1px solid var(--spectrum-global-color-gray-300); |
|||
border-radius: 4px 4px 4px 4px; |
|||
} |
|||
|
|||
.separator { |
|||
width: 1px; |
|||
height: 40px; |
|||
border-left: 1px dashed var(--grey-4); |
|||
color: var(--grey-4); |
|||
/* center horizontally */ |
|||
text-align: center; |
|||
margin-left: 50%; |
|||
} |
|||
</style> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -0,0 +1,11 @@ |
|||
<script> |
|||
//export let app |
|||
</script> |
|||
|
|||
<div class="automation-tab" /> |
|||
|
|||
<style> |
|||
.automation-tab { |
|||
color: pink; |
|||
} |
|||
</style> |
|||
@ -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> |
|||
@ -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> |
|||
@ -0,0 +1,7 @@ |
|||
#!/usr/bin/env bash |
|||
|
|||
if [ -z $CLUSTER_MODE ]; then |
|||
yarn run:docker |
|||
else |
|||
yarn run:docker:cluster |
|||
fi |
|||
@ -0,0 +1,9 @@ |
|||
module.exports = { |
|||
apps: [ |
|||
{ |
|||
script: "dist/index.js", |
|||
instances: "max", |
|||
exec_mode: "cluster", |
|||
}, |
|||
], |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
// TODO: REMOVE
|
|||
|
|||
const bcrypt = require("bcryptjs") |
|||
const env = require("../environment") |
|||
|
|||
const SALT_ROUNDS = env.SALT_ROUNDS || 10 |
|||
|
|||
exports.hash = async data => { |
|||
const salt = await bcrypt.genSalt(SALT_ROUNDS) |
|||
const result = await bcrypt.hash(data, salt) |
|||
return result |
|||
} |
|||
|
|||
exports.compare = async (data, encrypted) => |
|||
await bcrypt.compare(data, encrypted) |
|||
@ -0,0 +1,7 @@ |
|||
#!/bin/sh |
|||
|
|||
if [[ -z $CLUSTER_MODE ]]; then |
|||
yarn run:docker |
|||
else |
|||
yarn run:docker:cluster |
|||
fi |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue