mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
134 changed files with 6096 additions and 912 deletions
@ -1,61 +1,194 @@ |
|||||
|
require("../../tests/utilities/dbConfig"); |
||||
const { |
const { |
||||
generateAppID, |
generateAppID, |
||||
getDevelopmentAppID, |
getDevelopmentAppID, |
||||
getProdAppID, |
getProdAppID, |
||||
isDevAppID, |
isDevAppID, |
||||
isProdAppID, |
isProdAppID, |
||||
|
getPlatformUrl, |
||||
|
getScopedConfig |
||||
} = require("../utils") |
} = require("../utils") |
||||
|
const tenancy = require("../../tenancy"); |
||||
|
const { Configs, DEFAULT_TENANT_ID } = require("../../constants"); |
||||
|
const env = require("../../environment") |
||||
|
|
||||
function getID() { |
describe("utils", () => { |
||||
const appId = generateAppID() |
describe("app ID manipulation", () => { |
||||
const split = appId.split("_") |
|
||||
const uuid = split[split.length - 1] |
|
||||
const devAppId = `app_dev_${uuid}` |
|
||||
return { appId, devAppId, split, uuid } |
|
||||
} |
|
||||
|
|
||||
describe("app ID manipulation", () => { |
function getID() { |
||||
it("should be able to generate a new app ID", () => { |
const appId = generateAppID() |
||||
expect(generateAppID().startsWith("app_")).toEqual(true) |
const split = appId.split("_") |
||||
}) |
const uuid = split[split.length - 1] |
||||
|
const devAppId = `app_dev_${uuid}` |
||||
|
return { appId, devAppId, split, uuid } |
||||
|
} |
||||
|
|
||||
it("should be able to convert a production app ID to development", () => { |
it("should be able to generate a new app ID", () => { |
||||
const { appId, uuid } = getID() |
expect(generateAppID().startsWith("app_")).toEqual(true) |
||||
expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) |
}) |
||||
|
|
||||
|
it("should be able to convert a production app ID to development", () => { |
||||
|
const { appId, uuid } = getID() |
||||
|
expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to convert a development app ID to development", () => { |
||||
|
const { devAppId, uuid } = getID() |
||||
|
expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to convert a development ID to a production", () => { |
||||
|
const { devAppId, uuid } = getID() |
||||
|
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to convert a production ID to production", () => { |
||||
|
const { appId, uuid } = getID() |
||||
|
expect(getProdAppID(appId)).toEqual(`app_${uuid}`) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to confirm dev app ID is development", () => { |
||||
|
const { devAppId } = getID() |
||||
|
expect(isDevAppID(devAppId)).toEqual(true) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to confirm prod app ID is not development", () => { |
||||
|
const { appId } = getID() |
||||
|
expect(isDevAppID(appId)).toEqual(false) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to confirm prod app ID is prod", () => { |
||||
|
const { appId } = getID() |
||||
|
expect(isProdAppID(appId)).toEqual(true) |
||||
|
}) |
||||
|
|
||||
|
it("should be able to confirm dev app ID is not prod", () => { |
||||
|
const { devAppId } = getID() |
||||
|
expect(isProdAppID(devAppId)).toEqual(false) |
||||
|
}) |
||||
}) |
}) |
||||
|
}) |
||||
|
|
||||
it("should be able to convert a development app ID to development", () => { |
const DB_URL = "http://dburl.com" |
||||
const { devAppId, uuid } = getID() |
const DEFAULT_URL = "http://localhost:10000" |
||||
expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`) |
const ENV_URL = "http://env.com" |
||||
}) |
|
||||
|
|
||||
it("should be able to convert a development ID to a production", () => { |
const setDbPlatformUrl = async () => { |
||||
const { devAppId, uuid } = getID() |
const db = tenancy.getGlobalDB() |
||||
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`) |
db.put({ |
||||
|
_id: "config_settings", |
||||
|
type: Configs.SETTINGS, |
||||
|
config: { |
||||
|
platformUrl: DB_URL |
||||
|
} |
||||
}) |
}) |
||||
|
} |
||||
|
|
||||
it("should be able to convert a production ID to production", () => { |
const clearSettingsConfig = async () => { |
||||
const { appId, uuid } = getID() |
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
||||
expect(getProdAppID(appId)).toEqual(`app_${uuid}`) |
const db = tenancy.getGlobalDB() |
||||
|
try { |
||||
|
const config = await db.get("config_settings") |
||||
|
await db.remove("config_settings", config._rev) |
||||
|
} catch (e) { |
||||
|
if (e.status !== 404) { |
||||
|
throw e |
||||
|
} |
||||
|
} |
||||
}) |
}) |
||||
|
} |
||||
|
|
||||
|
describe("getPlatformUrl", () => { |
||||
|
describe("self host", () => { |
||||
|
|
||||
it("should be able to confirm dev app ID is development", () => { |
beforeEach(async () => { |
||||
const { devAppId } = getID() |
env._set("SELF_HOST", 1) |
||||
expect(isDevAppID(devAppId)).toEqual(true) |
await clearSettingsConfig() |
||||
}) |
}) |
||||
|
|
||||
|
it("gets the default url", async () => { |
||||
|
await tenancy.doInTenant(null, async () => { |
||||
|
const url = await getPlatformUrl() |
||||
|
expect(url).toBe(DEFAULT_URL) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("gets the platform url from the environment", async () => { |
||||
|
await tenancy.doInTenant(null, async () => { |
||||
|
env._set("PLATFORM_URL", ENV_URL) |
||||
|
const url = await getPlatformUrl() |
||||
|
expect(url).toBe(ENV_URL) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
it("should be able to confirm prod app ID is not development", () => { |
it("gets the platform url from the database", async () => { |
||||
const { appId } = getID() |
await tenancy.doInTenant(null, async () => { |
||||
expect(isDevAppID(appId)).toEqual(false) |
await setDbPlatformUrl() |
||||
|
const url = await getPlatformUrl() |
||||
|
expect(url).toBe(DB_URL) |
||||
|
}) |
||||
|
}) |
||||
}) |
}) |
||||
|
|
||||
it("should be able to confirm prod app ID is prod", () => { |
|
||||
const { appId } = getID() |
describe("cloud", () => { |
||||
expect(isProdAppID(appId)).toEqual(true) |
const TENANT_AWARE_URL = "http://default.env.com" |
||||
|
|
||||
|
beforeEach(async () => { |
||||
|
env._set("SELF_HOSTED", 0) |
||||
|
env._set("MULTI_TENANCY", 1) |
||||
|
env._set("PLATFORM_URL", ENV_URL) |
||||
|
await clearSettingsConfig() |
||||
|
}) |
||||
|
|
||||
|
it("gets the platform url from the environment without tenancy", async () => { |
||||
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
||||
|
const url = await getPlatformUrl({ tenantAware: false }) |
||||
|
expect(url).toBe(ENV_URL) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("gets the platform url from the environment with tenancy", async () => { |
||||
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
||||
|
const url = await getPlatformUrl() |
||||
|
expect(url).toBe(TENANT_AWARE_URL) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("never gets the platform url from the database", async () => { |
||||
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
||||
|
await setDbPlatformUrl() |
||||
|
const url = await getPlatformUrl() |
||||
|
expect(url).toBe(TENANT_AWARE_URL) |
||||
|
}) |
||||
|
}) |
||||
}) |
}) |
||||
|
}) |
||||
|
|
||||
|
describe("getScopedConfig", () => { |
||||
|
describe("settings config", () => { |
||||
|
|
||||
|
beforeEach(async () => { |
||||
|
env._set("SELF_HOSTED", 1) |
||||
|
env._set("PLATFORM_URL", "") |
||||
|
await clearSettingsConfig() |
||||
|
}) |
||||
|
|
||||
|
it("returns the platform url with an existing config", async () => { |
||||
|
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
||||
|
await setDbPlatformUrl() |
||||
|
const db = tenancy.getGlobalDB() |
||||
|
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) |
||||
|
expect(config.platformUrl).toBe(DB_URL) |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
it("should be able to confirm dev app ID is not prod", () => { |
it("returns the platform url without an existing config", async () => { |
||||
const { devAppId } = getID() |
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { |
||||
expect(isProdAppID(devAppId)).toEqual(false) |
const db = tenancy.getGlobalDB() |
||||
|
const config = await getScopedConfig(db, { type: Configs.SETTINGS }) |
||||
|
expect(config.platformUrl).toBe(DEFAULT_URL) |
||||
|
}) |
||||
|
}) |
||||
}) |
}) |
||||
}) |
}) |
||||
|
|||||
@ -0,0 +1,68 @@ |
|||||
|
<script> |
||||
|
import "@spectrum-css/fieldgroup/dist/index-vars.css" |
||||
|
import "@spectrum-css/radio/dist/index-vars.css" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
|
export let direction = "vertical" |
||||
|
export let value = [] |
||||
|
export let options = [] |
||||
|
export let error = null |
||||
|
export let disabled = false |
||||
|
export let getOptionLabel = option => option |
||||
|
export let getOptionValue = option => option |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
const onChange = e => { |
||||
|
let tempValue = value |
||||
|
let isChecked = e.target.checked |
||||
|
if (!tempValue.includes(e.target.value) && isChecked) { |
||||
|
tempValue.push(e.target.value) |
||||
|
} |
||||
|
value = tempValue |
||||
|
dispatch( |
||||
|
"change", |
||||
|
tempValue.filter(val => val !== e.target.value || isChecked) |
||||
|
) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}> |
||||
|
{#if options && Array.isArray(options)} |
||||
|
{#each options as option} |
||||
|
<div |
||||
|
title={getOptionLabel(option)} |
||||
|
class="spectrum-Checkbox spectrum-FieldGroup-item" |
||||
|
class:is-invalid={!!error} |
||||
|
> |
||||
|
<label |
||||
|
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item" |
||||
|
> |
||||
|
<input |
||||
|
on:change={onChange} |
||||
|
value={getOptionValue(option)} |
||||
|
type="checkbox" |
||||
|
class="spectrum-Checkbox-input" |
||||
|
{disabled} |
||||
|
checked={value.includes(getOptionValue(option))} |
||||
|
/> |
||||
|
<span class="spectrum-Checkbox-box"> |
||||
|
<svg |
||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark" |
||||
|
focusable="false" |
||||
|
aria-hidden="true" |
||||
|
> |
||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
||||
|
</svg> |
||||
|
</span> |
||||
|
<span class="spectrum-Checkbox-label">{getOptionLabel(option)}</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
{/each} |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.spectrum-Checkbox-input { |
||||
|
opacity: 0; |
||||
|
} |
||||
|
</style> |
||||
@ -1,11 +1,19 @@ |
|||||
{ |
{ |
||||
"baseUrl": "http://localhost:4100", |
"baseUrl": "http://localhost:4100", |
||||
"video": false, |
"video": true, |
||||
"projectId": "bmbemn", |
"projectId": "bmbemn", |
||||
|
"reporter": "cypress-multi-reporters", |
||||
|
"reporterOptions": { |
||||
|
"configFile": "reporterConfig.json" |
||||
|
}, |
||||
"env": { |
"env": { |
||||
"PORT": "4100", |
"PORT": "4100", |
||||
"WORKER_PORT": "4200", |
"WORKER_PORT": "4200", |
||||
"JWT_SECRET": "test", |
"JWT_SECRET": "test", |
||||
"HOST_IP": "" |
"HOST_IP": "" |
||||
|
}, |
||||
|
"retries": { |
||||
|
"runMode": 2, |
||||
|
"openMode": 0 |
||||
} |
} |
||||
} |
} |
||||
|
|||||
File diff suppressed because one or more lines are too long
@ -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,56 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify HR Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter HR Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="HR"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for HR templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
|
||||
|
if (templateNameText == "Job Application Tracker") { |
||||
|
// Template name should include 'applicant-tracking-system'
|
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', 'applicant-tracking-system') |
||||
|
} |
||||
|
else if (templateNameText == "Job Portal App") { |
||||
|
// Template name should include 'job-portal'
|
||||
|
const templateNameSplit = templateNameParsed.split('-app')[0] |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameSplit) |
||||
|
} |
||||
|
else { |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
} |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,222 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Job Application Tracker Template Functionality", () => { |
||||
|
const templateName = "Job Application Tracker" |
||||
|
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 Job Application Tracker template", () => { |
||||
|
// Select Job Application Tracker 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') |
||||
|
}) |
||||
|
|
||||
|
it("should add active/inactive vacancies", () => { |
||||
|
// Visit published app
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
||||
|
|
||||
|
// loop for active/inactive vacancies
|
||||
|
for (let i = 0; i < 2; i++) { |
||||
|
// Vacancies section
|
||||
|
cy.get(".links").contains("Vacancies").click({ force: true }) |
||||
|
cy.get(".spectrum-Button").contains("Create New").click() |
||||
|
|
||||
|
// Add inactive vacancy
|
||||
|
// Title
|
||||
|
cy.get('[data-name="Title"]').within(() => { |
||||
|
cy.get(".spectrum-Textfield").type("Tester") |
||||
|
}) |
||||
|
|
||||
|
// Closing Date
|
||||
|
cy.get('[data-name="Closing date"]').within(() => { |
||||
|
cy.get('[aria-label=Calendar]').click({ force: true }) |
||||
|
}) |
||||
|
cy.get("[aria-current=date]").click() |
||||
|
|
||||
|
// Department
|
||||
|
cy.get('[data-name="Department"]').within(() => { |
||||
|
cy.get(".spectrum-Picker-label").click() |
||||
|
}) |
||||
|
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
||||
|
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
||||
|
}) |
||||
|
|
||||
|
// Employment Type
|
||||
|
cy.get('[data-name="Employment type"]').within(() => { |
||||
|
cy.get(".spectrum-Picker-label").click() |
||||
|
}) |
||||
|
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
||||
|
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
||||
|
}) |
||||
|
|
||||
|
// Salary
|
||||
|
cy.get('[data-name="Salary ($)"]').within(() => { |
||||
|
cy.get(".spectrum-Textfield").type(40000) |
||||
|
}) |
||||
|
|
||||
|
// Description
|
||||
|
cy.get('[data-name="Description"]').within(() => { |
||||
|
cy.get(".spectrum-Textfield").type("description") |
||||
|
}) |
||||
|
|
||||
|
// Responsibilities
|
||||
|
cy.get('[data-name="Responsibilities"]').within(() => { |
||||
|
cy.get(".spectrum-Textfield").type("Responsibilities") |
||||
|
}) |
||||
|
|
||||
|
// Requirements
|
||||
|
cy.get('[data-name="Requirements"]').within(() => { |
||||
|
cy.get(".spectrum-Textfield").type("Requirements") |
||||
|
}) |
||||
|
|
||||
|
// Hiring manager
|
||||
|
cy.get('[data-name="Hiring manager"]').within(() => { |
||||
|
cy.get(".spectrum-Picker-label").click() |
||||
|
}) |
||||
|
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
||||
|
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
||||
|
}) |
||||
|
|
||||
|
// Active
|
||||
|
if (i == 0) { |
||||
|
cy.get('[data-name="Active"]').within(() => { |
||||
|
cy.get(".spectrum-Checkbox-box").click({ force: true }) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Location
|
||||
|
cy.get('[data-name="Location"]').within(() => { |
||||
|
cy.get(".spectrum-Picker-label").click() |
||||
|
}) |
||||
|
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
||||
|
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() |
||||
|
}) |
||||
|
|
||||
|
// Save vacancy
|
||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true }) |
||||
|
cy.wait(1000) |
||||
|
|
||||
|
// Check table was updated
|
||||
|
cy.get('[data-name="Vacancies Table"]').eq(i).should('contain', 'Tester') |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
xit("should filter applications by stage", () => { |
||||
|
// Visit published app
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
||||
|
cy.wait(1000) |
||||
|
|
||||
|
// Applications section
|
||||
|
cy.get(".links").contains("Applications").click({ force: true }) |
||||
|
cy.wait(1000) |
||||
|
|
||||
|
// Filter by stage - Confirm table updates
|
||||
|
cy.get(".spectrum-Picker").contains("Filter by stage").click({ force: true }) |
||||
|
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
||||
|
for (let i = 1; i < len; i++) { |
||||
|
cy.get(".spectrum-Menu-item").eq(i).click() |
||||
|
const stage = cy.get(".spectrum-Picker-label") |
||||
|
stage.invoke('text').then(stageText => { |
||||
|
if (stageText == "1st interview") { |
||||
|
cy.get(".placeholder").should('contain', 'No rows found') |
||||
|
} |
||||
|
else { |
||||
|
cy.get(".spectrum-Table-row").should('contain', stageText) |
||||
|
} |
||||
|
cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
xit("should edit an application", () => { |
||||
|
// Switch application from not hired to hired
|
||||
|
// Visit published app
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
||||
|
cy.wait(1000) |
||||
|
|
||||
|
// Not Hired section
|
||||
|
cy.get(".links").contains("Not hired").click({ force: true }) |
||||
|
cy.wait(500) |
||||
|
|
||||
|
// View application
|
||||
|
cy.get(".spectrum-Table").within(() => { |
||||
|
cy.get(".spectrum-Button").contains("View").click({ force: true }) |
||||
|
cy.wait(500) |
||||
|
}) |
||||
|
|
||||
|
// Update value for 'Staged'
|
||||
|
cy.get('[data-name="Stage"]').within(() => { |
||||
|
cy.get(".spectrum-Picker-label").click() |
||||
|
}) |
||||
|
cy.get(".spectrum-Menu").within(() => { |
||||
|
cy.get(".spectrum-Menu-item").contains("Hired").click() |
||||
|
}) |
||||
|
|
||||
|
// Save application
|
||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true }) |
||||
|
cy.wait(500) |
||||
|
|
||||
|
// Hired section
|
||||
|
cy.get(".links").contains("Hired").click({ force: true }) |
||||
|
cy.wait(500) |
||||
|
|
||||
|
// Verify Table size - Total rows = 2
|
||||
|
cy.get(".spectrum-Table").find(".spectrum-Table-row").its('length').then((len => { |
||||
|
expect(len).to.eq(2) |
||||
|
})) |
||||
|
}) |
||||
|
|
||||
|
xit("should delete an application", () => { |
||||
|
// Visit published app
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
||||
|
cy.wait(1000) |
||||
|
|
||||
|
// Hired section
|
||||
|
cy.get(".links").contains("Hired").click({ force: true }) |
||||
|
cy.wait(500) |
||||
|
|
||||
|
// View first application
|
||||
|
cy.get(".spectrum-Table-row").eq(0).within(() => { |
||||
|
cy.get(".spectrum-Button").contains("View").click({ force: true }) |
||||
|
cy.wait(500) |
||||
|
}) |
||||
|
|
||||
|
// Delete application
|
||||
|
cy.get(".spectrum-Button").contains("Delete").click({ force: true }) |
||||
|
cy.get(".spectrum-Dialog-grid").within(() => { |
||||
|
cy.get(".spectrum-Button").contains("Confirm").click() |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,60 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify IT Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter IT Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="IT"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for IT templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
|
||||
|
if (templateNameText == "Hashicorp Scorecard Template") { |
||||
|
const templateNameSplit = templateNameParsed.split('-template')[0] |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameSplit) |
||||
|
} |
||||
|
else if (templateNameText == "IT Ticketing System") { |
||||
|
const templateNameSplit = templateNameParsed.split('it-')[1] |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameSplit) |
||||
|
} |
||||
|
else if (templateNameText == "IT Incident Report Form") { |
||||
|
const templateNameSplit = templateNameParsed.split('-form')[0] |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameSplit) |
||||
|
} |
||||
|
else { |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
} |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,72 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("IT Ticketing System Template Functionality", () => { |
||||
|
const templateName = "IT Ticketing System" |
||||
|
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 IT Ticketing System template", () => { |
||||
|
// Select IT Ticketing System 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') |
||||
|
}) |
||||
|
|
||||
|
xit("should filter tickets by status", () => { |
||||
|
// Visit published app
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) |
||||
|
cy.wait(1000) |
||||
|
|
||||
|
// Tickets section
|
||||
|
cy.get(".links").contains("Tickets").click({ force: true }) |
||||
|
cy.wait(1000) |
||||
|
|
||||
|
// Filter by stage - Confirm table updates
|
||||
|
cy.get(".spectrum-Picker").contains("Filter by status").click({ force: true }) |
||||
|
cy.get(".spectrum-Menu").find('li').its('length').then(len => { |
||||
|
for (let i = 1; i < len; i++) { |
||||
|
cy.get(".spectrum-Menu-item").eq(i).click() |
||||
|
const stage = cy.get(".spectrum-Picker-label") |
||||
|
stage.invoke('text').then(stageText => { |
||||
|
if (stageText == "In progress" || stageText == "On hold" || stageText == "Triaged") { |
||||
|
cy.get(".placeholder").should('contain', 'No rows found') |
||||
|
} |
||||
|
else { |
||||
|
cy.get(".spectrum-Table-row").should('contain', stageText) |
||||
|
} |
||||
|
cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,42 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Admin Panel Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Admin Panels Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Admin Panels"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Admin Panels templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,51 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Aproval Apps Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Approval Apps Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Approval Apps"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Approval Apps templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
|
||||
|
if (templateNameText == "Content Approval System") { |
||||
|
// Template name should include 'content-approval'
|
||||
|
const templateNameSplit = templateNameParsed.split('-system')[0] |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameSplit) |
||||
|
} |
||||
|
else { |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
} |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,51 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Business Apps Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Business Apps Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Business Apps"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Business Apps templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
|
||||
|
if (templateNameText == "Employee Check-in/Check-Out Template") { |
||||
|
// Remove / from template name
|
||||
|
const templateNameReplace = templateNameParsed.replace(/\//g, "-") |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameReplace) |
||||
|
} |
||||
|
else { |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
} |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,44 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Directories Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Directories Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Directories"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Directories templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
const templateNameSplit = templateNameParsed.split('-template')[0] |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameSplit) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,42 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Forms Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Forms Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Forms"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Forms templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,43 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Healthcare Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Healthcare Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Healthcare"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Healthcare templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
|
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,42 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Legal Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Legal Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Legal"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Legal templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,42 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Logistics Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Logistics Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Logistics"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Logistics templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,42 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Manufacturing Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Manufacturing Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Manufacturing"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Manufacturing templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -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,51 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Marketing Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Marketing Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Marketing"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Marketing templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
|
||||
|
if (templateNameText == "Lead Generation Form") { |
||||
|
// Multi-step lead form
|
||||
|
// Template name includes 'multi-step-lead-form'
|
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', 'multi-step-lead-form') |
||||
|
} |
||||
|
else { |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
} |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,42 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Operations Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Operations Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Operations"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Operations templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,71 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Portals Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Portal Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Portal"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Portal templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Portals templates", () => { |
||||
|
// Filter Portals Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Portals"]').click() |
||||
|
}) |
||||
|
|
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a') |
||||
|
.should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,42 @@ |
|||||
|
import filterTests from "../../../support/filterTests" |
||||
|
|
||||
|
filterTests(["all"], () => { |
||||
|
context("Verify Professional Services Template Details", () => { |
||||
|
|
||||
|
before(() => { |
||||
|
cy.login() |
||||
|
|
||||
|
// Template navigation
|
||||
|
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) |
||||
|
|
||||
|
// Filter Professional Services Templates
|
||||
|
cy.get(".template-category-filters").within(() => { |
||||
|
cy.get('[data-cy="Professional Services"]').click() |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should verify the details option for Professional Services templates", () => { |
||||
|
cy.get(".template-grid").find(".template-card").its('length') |
||||
|
.then(len => { |
||||
|
// Verify template name is within details link
|
||||
|
for (let i = 0; i < len; i++) { |
||||
|
cy.get(".template-card").eq(i).within(() => { |
||||
|
const templateName = cy.get(".template-thumbnail-text") |
||||
|
templateName.invoke('text') |
||||
|
.then(templateNameText => { |
||||
|
const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') |
||||
|
cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) |
||||
|
}) |
||||
|
// Verify correct status from Details link - 200
|
||||
|
cy.get('a') |
||||
|
.then(link => { |
||||
|
cy.request(link.prop('href')) |
||||
|
.its('status') |
||||
|
.should('eq', 200) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -0,0 +1,60 @@ |
|||||
|
// createApp test
|
||||
|
export const CREATE_APP_BUTTON = '[data-cy="create-app-btn"]' |
||||
|
export const TEMPLATE_CATEGORY_FILTER = ".template-category-filters" |
||||
|
export const TEMPLATE_CATEGORY = ".template-categories" |
||||
|
export const APP_TABLE = ".appTable" |
||||
|
export const SPECTRUM_BUTTON_TEMPLATE = ".spectrum-Button" |
||||
|
export const TEMPLATE_CATEGORY_ACTIONGROUP = ".template-category" |
||||
|
export const TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON = |
||||
|
".template-category-filters .spectrum-ActionButton" |
||||
|
export const SPECTRUM_MODAL = ".spectrum-Modal" |
||||
|
export const APP_NAME_INPUT = "input" // we need to update this with atribute cy-data;
|
||||
|
export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup" |
||||
|
export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input" |
||||
|
|
||||
|
//AddMultiOptionDatatype test
|
||||
|
export const CATEGORY_DATA = '[data-cy="category-Data"]' |
||||
|
export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]' |
||||
|
export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]' |
||||
|
export const DROPDOWN = ".dropdown" |
||||
|
export const SPECTRUM_PICKER_LABEL = ".spectrum-Picker-label" |
||||
|
export const DATASOURCE_FIELD_CONTROL = '[data-cy="field-prop-control"]' |
||||
|
export const OPTION_TYPE_PROP_CONTROL = '[data-cy="optionsType-prop-control' |
||||
|
|
||||
|
//AddRadioButtons
|
||||
|
export const SPECTRUM_POPOVER = ".spectrum-Popover" |
||||
|
export const OPTION_SOURCE_PROP_CONROL = '[data-cy="optionsSource-prop-control' |
||||
|
export const APP_TABLE_STATUS = ".appTable .app-status" |
||||
|
export const APP_TABLE_ROW_ACTION = ".appTable .app-row-actions" |
||||
|
export const DEPLOYMENT_TOP_NAV_GLOBESTRIKE = |
||||
|
".deployment-top-nav svg[aria-label=GlobeStrike]" |
||||
|
export const DEPLOYMENT_TOP_GLOBE = ".deployment-top-nav svg[aria-label=Globe]" |
||||
|
export const PUBLISH_POPOVER_MENU = '[data-cy="publish-popover-menu"]' |
||||
|
export const PUBLISH_POPOVER_ACTION = '[data-cy="publish-popover-action"]' |
||||
|
export const PUBLISH_POPOVER_MESSAGE = ".publish-popover-message" |
||||
|
export const SPECTRUM_BUTTON = ".spectrum-Button" |
||||
|
export const TOPRIGHTNAV_BUTTON_SPECTRUM = ".toprightnav button.spectrum-Button" |
||||
|
|
||||
|
//createComponents
|
||||
|
export const SETTINGS = "[data-cy=Settings]" |
||||
|
export const SETTINGS_INPUT = "[data-cy=setting-text] input" |
||||
|
export const DESIGN = "[data-cy=Design]" |
||||
|
export const FONT_SIZE_PROP_CONTRO = "[data-cy=font-size-prop-control]" |
||||
|
export const DATA_CY_DATASOURCE = "[data-cy=setting-dataSource]" |
||||
|
export const DROPDOWN_CONTAINER = ".dropdown-container" |
||||
|
export const SPECTRUM_PICKER = ".spectrum-Picker" |
||||
|
|
||||
|
//autoScreens
|
||||
|
export const LABEL_ADD_CIRCLE = "[aria-label=AddCircle]" |
||||
|
export const ITEM_DISABLED = ".item.disabled" |
||||
|
export const CONFIRM_WRAP_SPE_BUTTON = ".confirm-wrap .spectrum-Button" |
||||
|
export const DATA_SOURCE_ENTRY = ".data-source-entry" |
||||
|
export const NAV_ITEMS_CONTAINER = ".nav-items-container" |
||||
|
|
||||
|
//publishWorkFlow
|
||||
|
export const DEPLOY_APP_MODAL = ".spectrum-Modal [data-cy=deploy-app-modal]" |
||||
|
export const DEPLOY_APP_URL_INPUT = "[data-cy=deployed-app-url] input" |
||||
|
export const GLOBESTRIKE = "svg[aria-label=GlobeStrike]" |
||||
|
export const GLOBE = "svg[aria-label=Globe]" |
||||
|
export const UNPUBLISH_MODAL = "[data-cy=unpublish-modal]" |
||||
|
export const CONFIRM_WRAP_BUTTON = ".confirm-wrap button" |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"reporterEnabled": "mochawesome", |
||||
|
"mochawesomeReporterOptions": { |
||||
|
"reportDir": "cypress/reports", |
||||
|
"quiet": true, |
||||
|
"overwrite": false, |
||||
|
"html": false, |
||||
|
"json": true |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
const fetch = require("node-fetch") |
||||
|
const path = require("path") |
||||
|
const fs = require("fs") |
||||
|
|
||||
|
const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL |
||||
|
const OUTCOME = process.env.CYPRESS_OUTCOME |
||||
|
const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL |
||||
|
const GIT_SHA = process.env.GITHUB_SHA |
||||
|
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL |
||||
|
|
||||
|
async function generateReport() { |
||||
|
// read the report file
|
||||
|
const REPORT_PATH = path.resolve( |
||||
|
__dirname, |
||||
|
"..", |
||||
|
"cypress", |
||||
|
"reports", |
||||
|
"testReport.json" |
||||
|
) |
||||
|
const report = fs.readFileSync(REPORT_PATH, "utf-8") |
||||
|
return JSON.parse(report) |
||||
|
} |
||||
|
|
||||
|
async function discordCypressResultsNotification(report) { |
||||
|
const { |
||||
|
suites, |
||||
|
tests, |
||||
|
passes, |
||||
|
pending, |
||||
|
failures, |
||||
|
duration, |
||||
|
passPercent, |
||||
|
skipped, |
||||
|
} = report.stats |
||||
|
|
||||
|
const options = { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
Accept: "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
content: `**Nightly Tests Status**: ${OUTCOME}`, |
||||
|
embeds: [ |
||||
|
{ |
||||
|
title: "Budi QA Bot", |
||||
|
description: `Nightly Tests`, |
||||
|
url: GITHUB_ACTIONS_RUN_URL, |
||||
|
color: OUTCOME === "success" ? 3066993 : 15548997, |
||||
|
timestamp: new Date(), |
||||
|
footer: { |
||||
|
icon_url: "http://bbui.budibase.com/budibase-logo.png", |
||||
|
text: "Budibase QA Bot", |
||||
|
}, |
||||
|
thumbnail: { |
||||
|
url: "http://bbui.budibase.com/budibase-logo.png", |
||||
|
}, |
||||
|
author: { |
||||
|
name: "Budibase QA Bot", |
||||
|
url: "https://discordapp.com", |
||||
|
icon_url: "http://bbui.budibase.com/budibase-logo.png", |
||||
|
}, |
||||
|
fields: [ |
||||
|
{ |
||||
|
name: "Commit", |
||||
|
value: `https://github.com/Budibase/budibase/commit/${GIT_SHA}`, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Cypress Dashboard URL", |
||||
|
value: DASHBOARD_URL || "None Supplied", |
||||
|
}, |
||||
|
{ |
||||
|
name: "Github Actions Run URL", |
||||
|
value: GITHUB_ACTIONS_RUN_URL || "None Supplied", |
||||
|
}, |
||||
|
{ |
||||
|
name: "Test Suites", |
||||
|
value: suites, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Tests", |
||||
|
value: tests, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Passed", |
||||
|
value: passes, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Pending", |
||||
|
value: pending, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Skipped", |
||||
|
value: skipped, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Failures", |
||||
|
value: failures, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Duration", |
||||
|
value: `${duration / 1000} Seconds`, |
||||
|
}, |
||||
|
{ |
||||
|
name: "Pass Percentage", |
||||
|
value: Math.floor(passPercent), |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
], |
||||
|
}), |
||||
|
} |
||||
|
const response = await fetch(WEBHOOK_URL, options) |
||||
|
|
||||
|
if (response.status >= 400) { |
||||
|
const text = await response.text() |
||||
|
console.error( |
||||
|
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}` |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function run() { |
||||
|
const report = await generateReport() |
||||
|
await discordCypressResultsNotification(report) |
||||
|
} |
||||
|
|
||||
|
run() |
||||
@ -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,56 @@ |
|||||
|
<script> |
||||
|
import { Body, ProgressBar, Label } from "@budibase/bbui" |
||||
|
import { onMount } from "svelte" |
||||
|
export let usage |
||||
|
|
||||
|
let percentage |
||||
|
let unlimited = false |
||||
|
|
||||
|
const isUnlimited = () => { |
||||
|
if (usage.total === -1) { |
||||
|
return true |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
const getPercentage = () => { |
||||
|
return Math.min(Math.ceil((usage.used / usage.total) * 100), 100) |
||||
|
} |
||||
|
|
||||
|
onMount(() => { |
||||
|
unlimited = isUnlimited() |
||||
|
percentage = getPercentage() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<div class="usage"> |
||||
|
<div class="info"> |
||||
|
<Label size="XL">{usage.name}</Label> |
||||
|
{#if unlimited} |
||||
|
<Body size="S">{usage.used}</Body> |
||||
|
{:else} |
||||
|
<Body size="S">{usage.used} / {usage.total}</Body> |
||||
|
{/if} |
||||
|
</div> |
||||
|
<div> |
||||
|
{#if unlimited} |
||||
|
<Body size="S">Unlimited</Body> |
||||
|
{:else} |
||||
|
<ProgressBar width={"100%"} duration={1} value={percentage} /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.usage { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
.info { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
gap: var(--spacing-m); |
||||
|
} |
||||
|
</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,139 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Body, |
||||
|
Divider, |
||||
|
Heading, |
||||
|
Layout, |
||||
|
notifications, |
||||
|
Link, |
||||
|
} from "@budibase/bbui" |
||||
|
import { onMount } from "svelte" |
||||
|
import { admin, auth, licensing } from "stores/portal" |
||||
|
import Usage from "components/usage/Usage.svelte" |
||||
|
|
||||
|
let staticUsage = [] |
||||
|
let monthlyUsage = [] |
||||
|
let loaded = false |
||||
|
|
||||
|
$: quotaUsage = $licensing.quotaUsage |
||||
|
$: license = $auth.user?.license |
||||
|
|
||||
|
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade` |
||||
|
|
||||
|
const setMonthlyUsage = () => { |
||||
|
monthlyUsage = [] |
||||
|
if (quotaUsage.monthly) { |
||||
|
for (let [key, value] of Object.entries(license.quotas.usage.monthly)) { |
||||
|
const used = quotaUsage.monthly.current[key] |
||||
|
if (used !== undefined) { |
||||
|
monthlyUsage.push({ |
||||
|
name: value.name, |
||||
|
used: used, |
||||
|
total: value.value, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const setStaticUsage = () => { |
||||
|
staticUsage = [] |
||||
|
for (let [key, value] of Object.entries(license.quotas.usage.static)) { |
||||
|
const used = quotaUsage.usageQuota[key] |
||||
|
if (used !== undefined) { |
||||
|
staticUsage.push({ |
||||
|
name: value.name, |
||||
|
used: used, |
||||
|
total: value.value, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const capitalise = string => { |
||||
|
if (string) { |
||||
|
return string.charAt(0).toUpperCase() + string.slice(1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const init = async () => { |
||||
|
try { |
||||
|
await licensing.getQuotaUsage() |
||||
|
} catch (e) { |
||||
|
console.error(e) |
||||
|
notifications.error(e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
await init() |
||||
|
loaded = true |
||||
|
}) |
||||
|
|
||||
|
$: { |
||||
|
if (license && quotaUsage) { |
||||
|
setMonthlyUsage() |
||||
|
setStaticUsage() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
{#if loaded} |
||||
|
<Layout> |
||||
|
<Heading>Usage</Heading> |
||||
|
<Body |
||||
|
>Get information about your current usage within Budibase. |
||||
|
{#if $admin.cloud} |
||||
|
{#if $auth.user?.accountPortalAccess} |
||||
|
To upgrade your plan and usage limits visit your <Link |
||||
|
size="L" |
||||
|
href={upgradeUrl}>Account</Link |
||||
|
>. |
||||
|
{:else} |
||||
|
Contact your account holder to upgrade your usage limits. |
||||
|
{/if} |
||||
|
{/if} |
||||
|
</Body> |
||||
|
</Layout> |
||||
|
<Layout gap="S"> |
||||
|
<Divider size="S" /> |
||||
|
</Layout> |
||||
|
<Layout gap="S" noPadding> |
||||
|
<Layout gap="XS"> |
||||
|
<Body size="S">YOUR PLAN</Body> |
||||
|
<Heading size="S">{capitalise(license?.plan.type)}</Heading> |
||||
|
</Layout> |
||||
|
<Layout gap="S"> |
||||
|
<Body size="S">USAGE</Body> |
||||
|
<div class="usages"> |
||||
|
{#each staticUsage as usage} |
||||
|
<div class="usage"> |
||||
|
<Usage {usage} /> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</Layout> |
||||
|
{#if monthlyUsage.length} |
||||
|
<Layout gap="S"> |
||||
|
<Body size="S">MONTHLY</Body> |
||||
|
<div class="usages"> |
||||
|
{#each monthlyUsage as usage} |
||||
|
<div class="usage"> |
||||
|
<Usage {usage} /> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</Layout> |
||||
|
<div /> |
||||
|
{/if} |
||||
|
</Layout> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
.usages { |
||||
|
display: grid; |
||||
|
column-gap: 60px; |
||||
|
row-gap: 50px; |
||||
|
grid-template-columns: 1fr 1fr 1fr; |
||||
|
} |
||||
|
</style> |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue