Browse Source

Merge branch 'develop' into cypressTests

master
MihailHadzhiev2022 4 years ago
parent
commit
25e1696008
  1. 3
      .eslintignore
  2. 53
      .github/workflows/release-develop.yml
  3. 25
      .github/workflows/smoke_test.yaml
  4. 6
      .gitignore
  5. 2
      charts/budibase/Chart.yaml
  6. 10
      charts/budibase/templates/app-service-deployment.yaml
  7. 6
      charts/budibase/templates/worker-service-deployment.yaml
  8. 2
      lerna.json
  9. 5
      package.json
  10. 3
      packages/backend-core/cache.js
  11. 1
      packages/backend-core/logging.js
  12. 8
      packages/backend-core/package.json
  13. 2
      packages/backend-core/src/auth.js
  14. 49
      packages/backend-core/src/cache/generic.js
  15. 12
      packages/backend-core/src/context/index.js
  16. 12
      packages/backend-core/src/db/index.js
  17. 8
      packages/backend-core/src/db/pouch.js
  18. 209
      packages/backend-core/src/db/tests/utils.spec.js
  19. 27
      packages/backend-core/src/db/utils.js
  20. 12
      packages/backend-core/src/environment.js
  21. 2
      packages/backend-core/src/hashing.js
  22. 1
      packages/backend-core/src/index.js
  23. 16
      packages/backend-core/src/logging.js
  24. 16
      packages/backend-core/src/middleware/passport/datasource/google.js
  25. 2
      packages/backend-core/src/middleware/passport/local.js
  26. 14
      packages/backend-core/src/middleware/passport/tests/oidc.spec.js
  27. 10
      packages/backend-core/src/redis/authRedis.js
  28. 2
      packages/backend-core/src/redis/index.js
  29. 1
      packages/backend-core/src/redis/utils.js
  30. 45
      packages/backend-core/src/security/sessions.js
  31. 41
      packages/backend-core/src/utils.js
  32. 349
      packages/backend-core/yarn.lock
  33. 4
      packages/bbui/package.json
  34. 5
      packages/bbui/src/Form/Core/DatePicker.svelte
  35. 8
      packages/bbui/src/Form/Core/Dropzone.svelte
  36. 2
      packages/bbui/src/Form/Dropzone.svelte
  37. 8
      packages/bbui/src/Layout/Layout.svelte
  38. 8
      packages/bbui/src/Layout/Page.svelte
  39. 2
      packages/bbui/src/SideNavigation/Item.svelte
  40. 24
      packages/bbui/src/Tabs/Tab.svelte
  41. 4
      packages/builder/cypress.json
  42. 346
      packages/builder/cypress/integration/appOverview.spec.js
  43. 2
      packages/builder/cypress/integration/appPublishWorkflow.spec.js
  44. 2
      packages/builder/cypress/integration/createApp.spec.js
  45. 5
      packages/builder/cypress/integration/createUserAndRoles.spec.js
  46. 20
      packages/builder/cypress/integration/renameAnApplication.spec.js
  47. 8
      packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js
  48. 29
      packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js
  49. 8
      packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js
  50. 72
      packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js
  51. 8
      packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js
  52. 8
      packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js
  53. 10
      packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js
  54. 8
      packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js
  55. 8
      packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js
  56. 8
      packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js
  57. 8
      packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js
  58. 8
      packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js
  59. 8
      packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js
  60. 44
      packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js
  61. 8
      packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js
  62. 8
      packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js
  63. 18
      packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js
  64. 8
      packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js
  65. 1
      packages/builder/cypress/setup.js
  66. 140
      packages/builder/cypress/support/commands.js
  67. 20
      packages/builder/package.json
  68. 10
      packages/builder/reporterConfig.json
  69. 130
      packages/builder/scripts/cypressResultsWebhook.js
  70. 2
      packages/builder/src/builderStore/store/automation/index.js
  71. 107
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
  72. 75
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
  73. 111
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte
  74. 133
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte
  75. 1
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte
  76. 146
      packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte
  77. 2
      packages/builder/src/components/automation/Shared/WebhookDisplay.svelte
  78. 4
      packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
  79. 2
      packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte
  80. 144
      packages/builder/src/components/common/AppLockModal.svelte
  81. 55
      packages/builder/src/components/common/DashCard.svelte
  82. 47
      packages/builder/src/components/common/EditableIcon.svelte
  83. 3
      packages/builder/src/components/common/inputs/CopyInput.svelte
  84. 18
      packages/builder/src/components/deploy/VersionModal.svelte
  85. 48
      packages/builder/src/components/start/AppRow.svelte
  86. 56
      packages/builder/src/components/usage/Usage.svelte
  87. 35
      packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte
  88. 6
      packages/builder/src/pages/builder/auth/login.svelte
  89. 21
      packages/builder/src/pages/builder/portal/_layout.svelte
  90. 8
      packages/builder/src/pages/builder/portal/apps/create.svelte
  91. 98
      packages/builder/src/pages/builder/portal/apps/index.svelte
  92. 9
      packages/builder/src/pages/builder/portal/apps/templates.svelte
  93. 2
      packages/builder/src/pages/builder/portal/manage/_layout.svelte
  94. 4
      packages/builder/src/pages/builder/portal/manage/auth/index.svelte
  95. 4
      packages/builder/src/pages/builder/portal/manage/email/index.svelte
  96. 5
      packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte
  97. 413
      packages/builder/src/pages/builder/portal/overview/[application]/index.svelte
  98. 11
      packages/builder/src/pages/builder/portal/overview/_components/AutomationTab.svelte
  99. 250
      packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte
  100. 131
      packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte

3
.eslintignore

@ -6,4 +6,5 @@ packages/server/coverage
packages/server/client
packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
packages/builder/cypress/reports

53
.github/workflows/release-develop.yml

@ -72,3 +72,56 @@ jobs:
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
- name: Get the latest budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Proxy service docker image
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:proxy:release
docker tag proxy-service budibase/proxy:$RELEASE_TAG
docker push budibase/proxy:$RELEASE_TAG
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
RELEASE_TAG: k8s-release
- name: Pull values.yaml from budibase-infra
run: |
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.release.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml
wc -l values.release.yaml
- name: Deploy to Release Environment
uses: glopezep/helm@v1.7.1
with:
release: budibase-release
namespace: budibase
chart: charts/budibase
token: ${{ github.token }}
helm: helm3
values: |
globals:
appVersion: develop
ingress:
enabled: true
nginx: true
value-files: >-
[
"values.release.yaml"
]
env:
KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}'
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env."
embed-title: ${{ env.RELEASE_VERSION }}

25
.github/workflows/smoke_test.yaml

@ -33,23 +33,20 @@ jobs:
with:
record: true
install: false
tag: nightly
command: yarn test:e2e:ci:record
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# TODO: upload recordings to s3
# - name: Configure AWS Credentials
# uses: aws-actions/configure-aws-credentials@v1
# with:
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# aws-region: eu-west-1
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
- uses: actions/upload-artifact@v3
with:
webhook-url: ${{ secrets.BUDI_QA_WEBHOOK }}
content: "Smoke test run completed with ${{ steps.cypress.outcome }}. See results at ${{ steps.cypress.outputs.dashboardUrl }}"
embed-title: ${{ steps.cypress.outcome }}
embed-color: ${{ steps.cypress.outcome == 'success' && '3066993' || '15548997' }}
name: Test Reports
path: packages/builder/cypress/reports/testReport.html
- name: Cypress Discord Notify
run: yarn test:e2e:ci:notify
env:
CYPRESS_WEBHOOK_URL: ${{ secrets.BUDI_QA_WEBHOOK }}
CYPRESS_OUTCOME: ${{ steps.cypress.outcome }}
CYPRESS_DASHBOARD_URL: ${{ steps.cypress.outputs.dashboardUrl }}
GITHUB_RUN_URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID

6
.gitignore

@ -97,5 +97,7 @@ hosting/proxy/.generated-nginx.prod.conf
bin/
hosting/.generated*
packages/builder/cypress.env.json
stats.html
packages/builder/cypress.env.json
packages/builder/cypress/reports
stats.html

2
charts/budibase/Chart.yaml

@ -11,7 +11,7 @@ sources:
- https://github.com/Budibase/budibase
- https://budibase.com
type: application
version: 0.2.9
version: 0.2.10
appVersion: 1.0.48
dependencies:
- name: couchdb

10
charts/budibase/templates/app-service-deployment.yaml

@ -78,6 +78,10 @@ spec:
value: {{ .Values.services.objectStore.url }}
- name: PORT
value: {{ .Values.services.apps.port | quote }}
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
- name: API_REQ_LIMIT_PER_SEC
value: {{ .Values.globals.apps.publicApiRateLimitPerSecond | quote }}
{{ end }}
- name: MULTI_TENANCY
value: {{ .Values.globals.multiTenancy | quote }}
- name: LOG_LEVEL
@ -119,6 +123,12 @@ spec:
image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always
livenessProbe:
httpGet:
path: /health
port: {{ .Values.services.apps.port }}
initialDelaySeconds: 5
periodSeconds: 5
name: bbapps
ports:
- containerPort: {{ .Values.services.apps.port }}

6
charts/budibase/templates/worker-service-deployment.yaml

@ -119,6 +119,12 @@ spec:
value: {{ .Values.globals.google.secret | quote }}
image: budibase/worker:{{ .Values.globals.appVersion }}
imagePullPolicy: Always
livenessProbe:
httpGet:
path: /health
port: {{ .Values.services.worker.port }}
initialDelaySeconds: 5
periodSeconds: 5
name: bbworker
ports:
- containerPort: {{ .Values.services.worker.port }}

2
lerna.json

@ -1,5 +1,5 @@
{
"version": "1.0.159-alpha.3",
"version": "1.0.191-alpha.0",
"npmClient": "yarn",
"packages": [
"packages/*"

5
package.json

@ -3,6 +3,8 @@
"private": true,
"devDependencies": {
"@rollup/plugin-json": "^4.0.2",
"@types/mongodb": "3.6.3",
"@typescript-eslint/parser": "4.28.0",
"babel-eslint": "^10.0.3",
"eslint": "^7.28.0",
"eslint-plugin-cypress": "^2.11.3",
@ -16,7 +18,6 @@
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.38.2",
"@typescript-eslint/parser": "4.28.0",
"typescript": "4.5.5"
},
"scripts": {
@ -48,11 +49,13 @@
"test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream",
"test:e2e:ci:record": "lerna run cy:ci:record --stream",
"test:e2e:ci:notify": "lerna run cy:ci:notify",
"build:specs": "lerna run specs",
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
"build:docker:proxy:release": "node scripts/proxy/generateProxyConfig release && npm run build:docker:proxy",
"build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy",
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",

3
packages/backend-core/cache.js

@ -1,4 +1,7 @@
const generic = require("./src/cache/generic")
module.exports = {
user: require("./src/cache/user"),
app: require("./src/cache/appMetadata"),
...generic,
}

1
packages/backend-core/logging.js

@ -0,0 +1 @@
module.exports = require("./src/logging")

8
packages/backend-core/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.159-alpha.3",
"version": "1.0.191-alpha.0",
"description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js",
"author": "Budibase",
@ -12,7 +12,8 @@
"dependencies": {
"@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.901.0",
"bcryptjs": "^2.4.3",
"bcrypt": "^5.0.1",
"dotenv": "^16.0.1",
"emitter-listener": "^1.1.2",
"ioredis": "^4.27.1",
"jsonwebtoken": "^8.5.1",
@ -41,8 +42,7 @@
"devDependencies": {
"ioredis-mock": "^5.5.5",
"jest": "^26.6.3",
"pouchdb-adapter-memory": "^7.2.2",
"pouchdb-all-dbs": "^1.0.2"
"pouchdb-adapter-memory": "^7.2.2"
},
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
}

2
packages/backend-core/src/auth.js

@ -29,7 +29,7 @@ passport.deserializeUser(async (user, done) => {
const user = await db.get(user._id)
return done(null, user)
} catch (err) {
console.error("User not found", err)
console.error(`User not found`, err)
return done(null, false, { message: "User not found" })
}
})

49
packages/backend-core/src/cache/generic.js

@ -0,0 +1,49 @@
const redis = require("../redis/authRedis")
const env = require("../environment")
const { getTenantId } = require("../context")
exports.CacheKeys = {
CHECKLIST: "checklist",
}
exports.TTL = {
ONE_MINUTE: 600,
ONE_HOUR: 3600,
ONE_DAY: 86400,
}
function generateTenantKey(key) {
const tenantId = getTenantId()
return `${key}:${tenantId}`
}
exports.withCache = async (key, ttl, fetchFn) => {
key = generateTenantKey(key)
const client = await redis.getCacheClient()
const cachedValue = await client.get(key)
if (cachedValue) {
return cachedValue
}
try {
const fetchedValue = await fetchFn()
if (!env.isTest()) {
await client.store(key, fetchedValue, ttl)
}
return fetchedValue
} catch (err) {
console.error("Error fetching before cache - ", err)
throw err
}
}
exports.bustCache = async key => {
const client = await redis.getCacheClient()
try {
await client.delete(generateTenantKey(key))
} catch (err) {
console.error("Error busting cache - ", err)
throw err
}
}

12
packages/backend-core/src/context/index.js

@ -73,7 +73,7 @@ exports.isMultiTenant = () => {
}
// used for automations, API endpoints should always be in context already
exports.doInTenant = (tenantId, task) => {
exports.doInTenant = (tenantId, task, { forceNew } = {}) => {
// the internal function is so that we can re-use an existing
// context - don't want to close DB on a parent context
async function internal(opts = { existing: false }) {
@ -98,7 +98,11 @@ exports.doInTenant = (tenantId, task) => {
}
}
const using = cls.getFromContext(ContextKeys.IN_USE)
if (using && cls.getFromContext(ContextKeys.TENANT_ID) === tenantId) {
if (
!forceNew &&
using &&
cls.getFromContext(ContextKeys.TENANT_ID) === tenantId
) {
cls.setOnContext(ContextKeys.IN_USE, using + 1)
return internal({ existing: true })
} else {
@ -135,7 +139,7 @@ const setAppTenantId = appId => {
exports.updateTenantId(appTenantId)
}
exports.doInAppContext = (appId, task) => {
exports.doInAppContext = (appId, task, { forceNew } = {}) => {
if (!appId) {
throw new Error("appId is required")
}
@ -162,7 +166,7 @@ exports.doInAppContext = (appId, task) => {
}
}
const using = cls.getFromContext(ContextKeys.IN_USE)
if (using && cls.getFromContext(ContextKeys.APP_ID) === appId) {
if (!forceNew && using && cls.getFromContext(ContextKeys.APP_ID) === appId) {
cls.setOnContext(ContextKeys.IN_USE, using + 1)
return internal({ existing: true })
} else {

12
packages/backend-core/src/db/index.js

@ -3,13 +3,13 @@ const env = require("../environment")
let PouchDB
let initialised = false
const dbList = new Set()
const put =
dbPut =>
async (doc, options = {}) => {
const response = await dbPut(doc, options)
// TODO: add created / updated
return response
return await dbPut(doc, options)
}
const checkInitialised = () => {
@ -28,6 +28,9 @@ exports.init = opts => {
// in situations that using the function doWithDB does not work
exports.dangerousGetDB = (dbName, opts) => {
checkInitialised()
if (env.isTest()) {
dbList.add(dbName)
}
const db = new PouchDB(dbName, opts)
const dbPut = db.put
db.put = put(dbPut)
@ -63,6 +66,9 @@ exports.doWithDB = async (dbName, cb, opts) => {
}
exports.allDbs = () => {
if (!env.isTest()) {
throw new Error("Cannot be used outside test environment.")
}
checkInitialised()
return PouchDB.allDbs()
return [...dbList]
}

8
packages/backend-core/src/db/pouch.js

@ -92,11 +92,5 @@ exports.getPouch = (opts = {}) => {
PouchDB.plugin(find)
}
const Pouch = PouchDB.defaults(POUCH_DB_DEFAULTS)
if (opts.allDbs) {
const allDbs = require("pouchdb-all-dbs")
allDbs(Pouch)
}
return Pouch
return PouchDB.defaults(POUCH_DB_DEFAULTS)
}

209
packages/backend-core/src/db/tests/utils.spec.js

@ -1,61 +1,194 @@
require("../../tests/utilities/dbConfig");
const {
generateAppID,
getDevelopmentAppID,
getProdAppID,
isDevAppID,
isProdAppID,
getPlatformUrl,
getScopedConfig
} = require("../utils")
const tenancy = require("../../tenancy");
const { Configs, DEFAULT_TENANT_ID } = require("../../constants");
const env = require("../../environment")
function getID() {
const appId = generateAppID()
const split = appId.split("_")
const uuid = split[split.length - 1]
const devAppId = `app_dev_${uuid}`
return { appId, devAppId, split, uuid }
}
describe("utils", () => {
describe("app ID manipulation", () => {
describe("app ID manipulation", () => {
it("should be able to generate a new app ID", () => {
expect(generateAppID().startsWith("app_")).toEqual(true)
})
function getID() {
const appId = generateAppID()
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", () => {
const { appId, uuid } = getID()
expect(getDevelopmentAppID(appId)).toEqual(`app_dev_${uuid}`)
it("should be able to generate a new app ID", () => {
expect(generateAppID().startsWith("app_")).toEqual(true)
})
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 { devAppId, uuid } = getID()
expect(getDevelopmentAppID(devAppId)).toEqual(`app_dev_${uuid}`)
})
const DB_URL = "http://dburl.com"
const DEFAULT_URL = "http://localhost:10000"
const ENV_URL = "http://env.com"
it("should be able to convert a development ID to a production", () => {
const { devAppId, uuid } = getID()
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`)
const setDbPlatformUrl = async () => {
const db = tenancy.getGlobalDB()
db.put({
_id: "config_settings",
type: Configs.SETTINGS,
config: {
platformUrl: DB_URL
}
})
}
it("should be able to convert a production ID to production", () => {
const { appId, uuid } = getID()
expect(getProdAppID(appId)).toEqual(`app_${uuid}`)
const clearSettingsConfig = async () => {
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
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", () => {
const { devAppId } = getID()
expect(isDevAppID(devAppId)).toEqual(true)
})
beforeEach(async () => {
env._set("SELF_HOST", 1)
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", () => {
const { appId } = getID()
expect(isDevAppID(appId)).toEqual(false)
it("gets the platform url from the database", async () => {
await tenancy.doInTenant(null, async () => {
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()
expect(isProdAppID(appId)).toEqual(true)
describe("cloud", () => {
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", () => {
const { devAppId } = getID()
expect(isProdAppID(devAppId)).toEqual(false)
it("returns the platform url without an existing config", async () => {
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
const db = tenancy.getGlobalDB()
const config = await getScopedConfig(db, { type: Configs.SETTINGS })
expect(config.platformUrl).toBe(DEFAULT_URL)
})
})
})
})
})

27
packages/backend-core/src/db/utils.js

@ -9,7 +9,7 @@ const {
APP_PREFIX,
APP_DEV,
} = require("./constants")
const { getTenantId, getGlobalDBName } = require("../tenancy")
const { getTenantId, getGlobalDBName, getGlobalDB } = require("../tenancy")
const fetch = require("node-fetch")
const { doWithDB, allDbs } = require("./index")
const { getCouchInfo } = require("./pouch")
@ -392,9 +392,7 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
// always provide the platform URL
if (type === Configs.SETTINGS) {
if (scopedConfig && scopedConfig.doc) {
scopedConfig.doc.config.platformUrl = await getPlatformUrl(
scopedConfig.doc.config
)
scopedConfig.doc.config.platformUrl = await getPlatformUrl()
} else {
scopedConfig = {
doc: {
@ -409,19 +407,30 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) {
return scopedConfig && scopedConfig.doc
}
const getPlatformUrl = async settings => {
const getPlatformUrl = async (opts = { tenantAware: true }) => {
let platformUrl = env.PLATFORM_URL || "http://localhost:10000"
if (!env.SELF_HOSTED && env.MULTI_TENANCY) {
if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) {
// cloud and multi tenant - add the tenant to the default platform url
const tenantId = getTenantId()
if (!platformUrl.includes("localhost:")) {
platformUrl = platformUrl.replace("://", `://${tenantId}.`)
}
} else {
} else if (env.SELF_HOSTED) {
const db = getGlobalDB()
// get the doc directly instead of with getScopedConfig to prevent loop
let settings
try {
settings = await db.get(generateConfigID({ type: Configs.SETTINGS }))
} catch (e) {
if (e.status !== 404) {
throw e
}
}
// self hosted - check for platform url override
if (settings && settings.platformUrl) {
platformUrl = settings.platformUrl
if (settings && settings.config && settings.config.platformUrl) {
platformUrl = settings.config.platformUrl
}
}

12
packages/backend-core/src/environment.js

@ -10,7 +10,15 @@ function isDev() {
return process.env.NODE_ENV !== "production"
}
let LOADED = false
if (!LOADED && isDev() && !isTest()) {
require("dotenv").config()
LOADED = true
}
module.exports = {
isTest,
isDev,
JWT_SECRET: process.env.JWT_SECRET,
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
@ -41,8 +49,8 @@ module.exports = {
GLOBAL_CLOUD_BUCKET_NAME:
process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads",
USE_COUCH: process.env.USE_COUCH || true,
isTest,
isDev,
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
_set(key, value) {
process.env[key] = value
module.exports[key] = value

2
packages/backend-core/src/hashing.js

@ -1,4 +1,4 @@
const bcrypt = require("bcryptjs")
const bcrypt = require("bcrypt")
const env = require("./environment")
const { v4 } = require("uuid")

1
packages/backend-core/src/index.js

@ -19,5 +19,6 @@ module.exports = {
env: require("./environment"),
accounts: require("./cloud/accounts"),
tenancy: require("./tenancy"),
context: require("../context"),
featureFlags: require("./featureFlags"),
}

16
packages/backend-core/src/logging.js

@ -0,0 +1,16 @@
const NonErrors = ["AccountError"]
function isSuppressed(e) {
return e && e["suppressAlert"]
}
module.exports.logAlert = (message, e = null) => {
if (e && NonErrors.includes(e.name) && isSuppressed(e)) {
return
}
let errorJson = ""
if (e) {
errorJson = ": " + JSON.stringify(e, Object.getOwnPropertyNames(e))
}
console.error(`bb-alert: ${message} ${errorJson}`)
}

16
packages/backend-core/src/middleware/passport/datasource/google.js

@ -21,20 +21,12 @@ async function fetchGoogleCreds() {
)
}
async function platformUrl() {
const db = getGlobalDB()
const publicConfig = await getScopedConfig(db, {
type: Configs.SETTINGS,
})
return getPlatformUrl(publicConfig)
}
async function preAuth(passport, ctx, next) {
// get the relevant config
const googleConfig = await fetchGoogleCreds()
const platUrl = await platformUrl()
const platformUrl = await getPlatformUrl({ tenantAware: false })
let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback`
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
if (!ctx.query.appId || !ctx.query.datasourceId) {
@ -51,9 +43,9 @@ async function preAuth(passport, ctx, next) {
async function postAuth(passport, ctx, next) {
// get the relevant config
const config = await fetchGoogleCreds()
const platUrl = await platformUrl()
const platformUrl = await getPlatformUrl({ tenantAware: false })
let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback`
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
const strategy = await google.strategyFactory(
config,
callbackUrl,

2
packages/backend-core/src/middleware/passport/local.js

@ -30,7 +30,7 @@ exports.authenticate = async function (ctx, email, password, done) {
const dbUser = await getGlobalUserByEmail(email)
if (dbUser == null) {
return authError(done, "User not found")
return authError(done, `User not found: [${email}]`)
}
// check that the user is currently inactive, if this is the case throw invalid

14
packages/backend-core/src/middleware/passport/tests/oidc.spec.js

@ -71,7 +71,7 @@ describe("oidc", () => {
describe("authenticate", () => {
afterEach(() => {
jest.clearAllMocks();
jest.clearAllMocks()
});
// mock third party common authentication
@ -80,10 +80,10 @@ describe("oidc", () => {
// mock the passport callback
const mockDone = jest.fn()
const mockSaveUserFn = jest.fn()
async function doAuthenticate() {
const oidc = require("../oidc")
const mockSaveUserFn = jest.fn()
const authenticate = await oidc.buildVerifyFn(mockSaveUserFn)
await authenticate(
@ -105,11 +105,13 @@ describe("oidc", () => {
expect(authenticateThirdParty).toHaveBeenCalledWith(
user,
false,
mockDone)
mockDone,
mockSaveUserFn,
)
}
it("delegates authentication to third party common", async () => {
doTest()
await doTest()
})
it("uses JWT email to get email", async () => {
@ -118,7 +120,7 @@ describe("oidc", () => {
email : "mock@budibase.com"
}
doTest()
await doTest()
})
it("uses JWT username to get email", async () => {
@ -127,7 +129,7 @@ describe("oidc", () => {
preferred_username : "mock@budibase.com"
}
doTest()
await doTest()
})
it("uses JWT invalid username to get email", async () => {

10
packages/backend-core/src/redis/authRedis.js

@ -1,18 +1,20 @@
const Client = require("./index")
const utils = require("./utils")
let userClient, sessionClient, appClient
let userClient, sessionClient, appClient, cacheClient
async function init() {
userClient = await new Client(utils.Databases.USER_CACHE).init()
sessionClient = await new Client(utils.Databases.SESSIONS).init()
appClient = await new Client(utils.Databases.APP_METADATA).init()
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
}
process.on("exit", async () => {
if (userClient) await userClient.finish()
if (sessionClient) await sessionClient.finish()
if (appClient) await appClient.finish()
if (cacheClient) await cacheClient.finish()
})
module.exports = {
@ -34,4 +36,10 @@ module.exports = {
}
return appClient
},
getCacheClient: async () => {
if (!cacheClient) {
await init()
}
return cacheClient
},
}

2
packages/backend-core/src/redis/index.js

@ -23,7 +23,7 @@ function connectionError(timeout, err) {
if (CLOSED) {
return
}
CLIENT.end()
CLIENT.disconnect()
CLOSED = true
// always clear this on error
clearTimeout(timeout)

1
packages/backend-core/src/redis/utils.js

@ -18,6 +18,7 @@ exports.Databases = {
APP_METADATA: "appMetadata",
QUERY_VARS: "queryVars",
LICENSES: "license",
GENERIC_CACHE: "data_cache",
}
exports.SEPARATOR = SEPARATOR

45
packages/backend-core/src/security/sessions.js

@ -15,29 +15,33 @@ function makeSessionID(userId, sessionId) {
}
async function invalidateSessions(userId, sessionIds = null) {
let sessions = []
try {
let sessions = []
// If no sessionIds, get all the sessions for the user
if (!sessionIds) {
sessions = await getSessionsForUser(userId)
sessions.forEach(
session =>
(session.key = makeSessionID(session.userId, session.sessionId))
)
} else {
// use the passed array of sessionIds
sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
sessions = sessions.map(sessionId => ({
key: makeSessionID(userId, sessionId),
}))
}
// If no sessionIds, get all the sessions for the user
if (!sessionIds) {
sessions = await getSessionsForUser(userId)
sessions.forEach(
session =>
(session.key = makeSessionID(session.userId, session.sessionId))
)
} else {
// use the passed array of sessionIds
sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
sessions = sessions.map(sessionId => ({
key: makeSessionID(userId, sessionId),
}))
}
const client = await redis.getSessionClient()
const promises = []
for (let session of sessions) {
promises.push(client.delete(session.key))
const client = await redis.getSessionClient()
const promises = []
for (let session of sessions) {
promises.push(client.delete(session.key))
}
await Promise.all(promises)
} catch (err) {
console.error(`Error invalidating sessions: ${err}`)
}
await Promise.all(promises)
}
exports.createASession = async (userId, session) => {
@ -76,6 +80,7 @@ exports.getSession = async (userId, sessionId) => {
return client.get(makeSessionID(userId, sessionId))
} catch (err) {
// if can't get session don't error, just don't return anything
console.error(err)
return null
}
}

41
packages/backend-core/src/utils.js

@ -197,11 +197,16 @@ exports.getBuildersCount = async () => {
return builders.length
}
exports.saveUser = async (
const DEFAULT_SAVE_USER = {
hashPassword: true,
requirePassword: true,
bulkCreate: false,
}
exports.internalSaveUser = async (
user,
tenantId,
hashPassword = true,
requirePassword = true
{ hashPassword, requirePassword, bulkCreate } = DEFAULT_SAVE_USER
) => {
if (!tenantId) {
throw "No tenancy specified."
@ -213,7 +218,10 @@ exports.saveUser = async (
let { email, password, _id } = user
// make sure another user isn't using the same email
let dbUser
if (email) {
// user can't exist in bulk creation
if (bulkCreate) {
dbUser = null
} else if (email) {
// check budibase users inside the tenant
dbUser = await exports.getGlobalUserByEmail(email)
if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
@ -267,11 +275,17 @@ exports.saveUser = async (
user.status = UserStatus.ACTIVE
}
try {
const response = await db.put({
const putOpts = {
password: hashedPassword,
...user,
})
await tryAddTenant(tenantId, _id, email)
}
if (bulkCreate) {
return putOpts
}
const response = await db.put(putOpts)
if (env.MULTI_TENANCY) {
await tryAddTenant(tenantId, _id, email)
}
await userCache.invalidateUser(response.id)
return {
_id: response.id,
@ -288,6 +302,19 @@ exports.saveUser = async (
})
}
// maintained for api compat, don't want to change function signature
exports.saveUser = async (
user,
tenantId,
hashPassword = true,
requirePassword = true
) => {
return exports.internalSaveUser(user, tenantId, {
hashPassword,
requirePassword,
})
}
/**
* Logs a user out from budibase. Re-used across account portal and builder.
*/

349
packages/backend-core/yarn.lock

@ -497,6 +497,21 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc"
integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==
dependencies:
detect-libc "^2.0.0"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.7"
nopt "^5.0.0"
npmlog "^5.0.1"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.11"
"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
@ -623,6 +638,11 @@ abab@^2.0.3, abab@^2.0.5:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@ -661,11 +681,6 @@ acorn-walk@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn@^5.2.1:
version "5.7.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
acorn@^7.1.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
@ -701,11 +716,6 @@ ajv@^6.12.3:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
amdefine@>=0.0.4:
version "1.0.1"
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
ansi-escapes@^4.2.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@ -748,6 +758,19 @@ anymatch@^3.0.3:
normalize-path "^3.0.0"
picomatch "^2.0.4"
"aproba@^1.0.3 || ^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
are-we-there-yet@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
dependencies:
delegates "^1.0.0"
readable-stream "^3.6.0"
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -800,11 +823,6 @@ assign-symbols@^1.0.0:
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
ast-types@0.9.6:
version "0.9.6"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
async@~2.1.4:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@ -938,11 +956,6 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base62@^1.1.0:
version "1.2.8"
resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428"
integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA==
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -978,10 +991,13 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
bcryptjs@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
bcrypt@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71"
integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^3.1.0"
bl@^4.0.3:
version "4.1.0"
@ -1164,6 +1180,11 @@ chownr@^1.1.1:
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
@ -1245,6 +1266,11 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-support@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
combined-stream@^1.0.5, combined-stream@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
@ -1259,26 +1285,6 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.5.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commoner@^0.10.1:
version "0.10.8"
resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5"
integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU=
dependencies:
commander "^2.5.0"
detective "^4.3.1"
glob "^5.0.15"
graceful-fs "^4.1.2"
iconv-lite "^0.4.5"
mkdirp "^0.5.0"
private "^0.1.6"
q "^1.1.2"
recast "^0.11.17"
component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
@ -1294,6 +1300,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
console-control-strings@^1.0.0, console-control-strings@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
@ -1445,34 +1456,31 @@ define-property@^2.0.2:
is-descriptor "^1.0.2"
isobject "^3.0.1"
defined@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
denque@^1.1.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
detect-libc@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
detective@^4.3.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e"
integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==
dependencies:
acorn "^5.2.1"
defined "^1.0.0"
diff-sequences@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
@ -1485,6 +1493,11 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
dotenv@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
double-ended-queue@2.1.0-0:
version "2.1.0-0"
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
@ -1572,15 +1585,6 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
es3ify@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/es3ify/-/es3ify-0.2.2.tgz#5dae3e650e5be3684b88066513d528d092629862"
integrity sha1-Xa4+ZQ5b42hLiAZlE9Uo0JJimGI=
dependencies:
esprima "^2.7.1"
jstransform "~11.0.0"
through "~2.3.4"
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@ -1608,26 +1612,11 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
esprima-fb@^15001.1.0-dev-harmony-fb:
version "15001.1.0-dev-harmony-fb"
resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901"
integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE=
esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esprima@~3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
@ -1884,6 +1873,13 @@ fs-constants@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
dependencies:
minipass "^3.0.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -1904,6 +1900,21 @@ functional-red-black-tree@^1.0.1:
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
gauge@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==
dependencies:
aproba "^1.0.3 || ^2.0.0"
color-support "^1.1.2"
console-control-strings "^1.0.0"
has-unicode "^2.0.1"
object-assign "^4.1.1"
signal-exit "^3.0.0"
string-width "^4.2.3"
strip-ansi "^6.0.1"
wide-align "^1.1.2"
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@ -1945,17 +1956,6 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
glob@^5.0.15:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=
dependencies:
inflight "^1.0.4"
inherits "2"
minimatch "2 || 3"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
@ -1999,7 +1999,7 @@ googleapis@^16.0.0:
google-auth-library "~0.10.0"
string-template "~1.0.0"
graceful-fs@^4.1.2, graceful-fs@^4.2.4:
graceful-fs@^4.2.4:
version "4.2.8"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
@ -2055,6 +2055,11 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
has-value@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@ -2165,7 +2170,7 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
iconv-lite@0.4.24, iconv-lite@^0.4.5:
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -2988,17 +2993,6 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.3.6"
jstransform@~11.0.0:
version "11.0.3"
resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223"
integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM=
dependencies:
base62 "^1.1.0"
commoner "^0.10.1"
esprima-fb "^15001.1.0-dev-harmony-fb"
object-assign "^2.0.0"
source-map "^0.4.2"
jwa@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5"
@ -3274,7 +3268,7 @@ ltgt@2.2.1, ltgt@^2.1.2, ltgt@~2.2.0:
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=
make-dir@^3.0.0:
make-dir@^3.0.0, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@ -3387,7 +3381,7 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
"minimatch@2 || 3", minimatch@^3.0.4:
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
@ -3399,6 +3393,21 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
minipass@^3.0.0:
version "3.1.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==
dependencies:
yallist "^4.0.0"
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
dependencies:
minipass "^3.0.0"
yallist "^4.0.0"
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@ -3412,12 +3421,10 @@ mkdirp-classic@^0.5.2:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@^0.5.0:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
dependencies:
minimist "^1.2.5"
mkdirp@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
ms@2.0.0:
version "2.0.0"
@ -3476,12 +3483,17 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-addon-api@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-fetch@2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-fetch@2.6.7, node-fetch@^2.6.1:
node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -3525,6 +3537,13 @@ node-releases@^2.0.1:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5"
integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
dependencies:
abbrev "1"
normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@ -3561,6 +3580,16 @@ npm-run-path@^4.0.0:
dependencies:
path-key "^3.0.0"
npmlog@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
dependencies:
are-we-there-yet "^2.0.0"
console-control-strings "^1.1.0"
gauge "^3.0.0"
set-blocking "^2.0.0"
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
@ -3581,10 +3610,10 @@ oauth@0.9.x, oauth@^0.9.15:
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
object-assign@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa"
integrity sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
object-copy@^0.1.0:
version "0.1.0"
@ -3918,17 +3947,6 @@ pouchdb-adapter-utils@7.2.2:
pouchdb-merge "7.2.2"
pouchdb-utils "7.2.2"
pouchdb-all-dbs@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/pouchdb-all-dbs/-/pouchdb-all-dbs-1.1.1.tgz#85f04a39cafda52497ec49abf1c93bb5e72813f6"
integrity sha512-UUnsdmcnRSQ8MAOYSJjfTwKkQNb/6fvOfd/f7dNNivWZ2YDYVuMfgw1WQdL634yEtcXTxAENZ/EyLRdzPCB41A==
dependencies:
argsarray "0.0.1"
es3ify "^0.2.2"
inherits "~2.0.1"
pouchdb-promise "6.4.3"
tiny-queue "^0.2.0"
pouchdb-binary-utils@7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/pouchdb-binary-utils/-/pouchdb-binary-utils-7.2.2.tgz#0690b348052c543b1e67f032f47092ca82bcb10e"
@ -4005,7 +4023,7 @@ pouchdb-merge@7.2.2:
resolved "https://registry.yarnpkg.com/pouchdb-merge/-/pouchdb-merge-7.2.2.tgz#940d85a2b532d6a93a6cab4b250f5648511bcc16"
integrity sha512-6yzKJfjIchBaS7Tusuk8280WJdESzFfQ0sb4jeMUNnrqs4Cx3b0DIEOYTRRD9EJDM+je7D3AZZ4AT0tFw8gb4A==
pouchdb-promise@6.4.3, pouchdb-promise@^6.0.4:
pouchdb-promise@^6.0.4:
version "6.4.3"
resolved "https://registry.yarnpkg.com/pouchdb-promise/-/pouchdb-promise-6.4.3.tgz#74516f4acf74957b54debd0fb2c0e5b5a68ca7b3"
integrity sha512-ruJaSFXwzsxRHQfwNHjQfsj58LBOY1RzGzde4PM5CWINZwFjCQAhZwfMrch2o/0oZT6d+Xtt0HTWhq35p3b0qw==
@ -4088,11 +4106,6 @@ pretty-format@^26.6.2:
ansi-styles "^4.0.0"
react-is "^17.0.1"
private@^0.1.6, private@~0.1.5:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -4139,11 +4152,6 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
@ -4193,7 +4201,7 @@ readable-stream@1.1.14, readable-stream@^1.0.27-1:
isarray "0.0.1"
string_decoder "~0.10.x"
"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0:
"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@ -4225,16 +4233,6 @@ readline-sync@^1.4.9:
resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
recast@^0.11.17:
version "0.11.23"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3"
integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=
dependencies:
ast-types "0.9.6"
esprima "~3.1.0"
private "~0.1.5"
source-map "~0.5.0"
redis-commands@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
@ -4379,7 +4377,7 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
rimraf@^3.0.0:
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@ -4467,6 +4465,13 @@ semver@^7.3.2:
dependencies:
lru-cache "^6.0.0"
semver@^7.3.5:
version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
dependencies:
lru-cache "^6.0.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -4592,14 +4597,7 @@ source-map-url@^0.4.0:
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
source-map@^0.4.2:
version "0.4.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
dependencies:
amdefine ">=0.0.4"
source-map@^0.5.0, source-map@^0.5.6, source-map@~0.5.0:
source-map@^0.5.0, source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@ -4728,7 +4726,7 @@ string-template@~1.0.0:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
string-width@^4.1.0, string-width@^4.2.0:
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -4841,6 +4839,18 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^6.1.11:
version "6.1.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
dependencies:
chownr "^2.0.0"
fs-minipass "^2.0.0"
minipass "^3.0.0"
minizlib "^2.1.1"
mkdirp "^1.0.3"
yallist "^4.0.0"
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@ -4879,16 +4889,6 @@ through2@^2.0.0, through2@^2.0.2, through2@^2.0.3:
readable-stream "~2.3.6"
xtend "~4.0.1"
through@~2.3.4:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
tiny-queue@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046"
integrity sha1-JaZ/LG4lOyypQZd7XvdELvl6YEY=
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -5229,6 +5229,13 @@ which@^2.0.1, which@^2.0.2:
dependencies:
isexe "^2.0.0"
wide-align@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"

4
packages/bbui/package.json

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

5
packages/bbui/src/Form/Core/DatePicker.svelte

@ -58,6 +58,11 @@
if (timeOnly) {
newValue = `2000-01-01T${newValue.split("T")[1]}`
}
// date only, offset for timezone so always right date
else if (!enableTime) {
const offset = dates[0].getTimezoneOffset() * 60000
newValue = new Date(dates[0].getTime() - offset).toISOString()
}
dispatch("change", newValue)
}

8
packages/bbui/src/Form/Core/Dropzone.svelte

@ -18,6 +18,7 @@
export let fileSizeLimit = BYTES_IN_MB * 20
export let processFiles = null
export let handleFileTooLarge = null
export let handleTooManyFiles = null
export let gallery = true
export let error = null
export let fileTags = []
@ -71,6 +72,13 @@
handleFileTooLarge(fileSizeLimit, value)
return
}
const fileCount = fileList.length + value.length
if (handleTooManyFiles && maximum && fileCount > maximum) {
handleTooManyFiles(maximum)
return
}
if (processFiles) {
const processedFiles = await processFiles(fileList)
const newValue = [...value, ...processedFiles]

2
packages/bbui/src/Form/Dropzone.svelte

@ -11,6 +11,7 @@
export let fileSizeLimit = undefined
export let processFiles = undefined
export let handleFileTooLarge = undefined
export let handleTooManyFiles = undefined
export let gallery = true
export let fileTags = []
export let maximum = undefined
@ -30,6 +31,7 @@
{fileSizeLimit}
{processFiles}
{handleFileTooLarge}
{handleTooManyFiles}
{gallery}
{fileTags}
{maximum}

8
packages/bbui/src/Layout/Layout.svelte

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

8
packages/bbui/src/Layout/Page.svelte

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

2
packages/bbui/src/SideNavigation/Item.svelte

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

24
packages/bbui/src/Tabs/Tab.svelte

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

4
packages/builder/cypress.json

@ -2,6 +2,10 @@
"baseUrl": "http://localhost:4100",
"video": true,
"projectId": "bmbemn",
"reporter": "cypress-multi-reporters",
"reporterOptions": {
"configFile": "reporterConfig.json"
},
"env": {
"PORT": "4100",
"WORKER_PORT": "4200",

346
packages/builder/cypress/integration/appOverview.spec.js

@ -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()
})
})
})

2
packages/builder/cypress/integration/appPublishWorkflow.spec.js

@ -58,7 +58,7 @@ filterTests(['all'], () => {
cy.get(".appTable .app-row-actions").eq(0)
.within(() => {
cy.get(".spectrum-Button").contains("View app")
cy.get(".spectrum-Button").contains("View")
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
})

2
packages/builder/cypress/integration/createApp.spec.js

@ -123,6 +123,7 @@ filterTests(['smoke', 'all'], () => {
cy.applicationInAppTable("Teds app")
cy.deleteApp("Teds app")
cy.wait(2000)
//Accomodate names that end in 'S'
cy.updateUserInformation("Chris", "Userman")
@ -134,6 +135,7 @@ filterTests(['smoke', 'all'], () => {
cy.applicationInAppTable("Chris app")
cy.deleteApp("Chris app")
cy.wait(2000)
cy.updateUserInformation("", "")
})

5
packages/builder/cypress/integration/createUserAndRoles.spec.js

@ -4,6 +4,8 @@ filterTests(["smoke", "all"], () => {
context("Create a User and Assign Roles", () => {
before(() => {
cy.login()
cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests")
})
it("should create a user", () => {
@ -52,7 +54,7 @@ filterTests(["smoke", "all"], () => {
cy.get(".spectrum-Table").contains("bbuser").click()
cy.wait(1000)
for (let i = 0; i < 3; i++) {
cy.get(".spectrum-Table")
cy.get(".spectrum-Table", { timeout: 3000})
.eq(1)
.find(".spectrum-Table-row")
.eq(0)
@ -79,6 +81,7 @@ filterTests(["smoke", "all"], () => {
.contains("Update role")
.click({ force: true })
})
cy.reload()
}
// Confirm roles exist within Configure roles table
cy.wait(2000)

20
packages/builder/cypress/integration/renameAnApplication.spec.js

@ -112,19 +112,9 @@ filterTests(['all'], () => {
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
})
cy.get(".spectrum-Modal")
.within(() => {
if (noName == true) {
cy.get("input").clear()
cy.get(".spectrum-Dialog-grid").click()
.contains("App name must be letters, numbers and spaces only")
return cy
}
cy.get("input").clear()
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
cy.wait(500)
})
}
})
cy.updateAppName(changedName, noName)
}
})
})

8
packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter HR Templates
cy.get(".template-category-filters").within(() => {

29
packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js

@ -1,28 +1,20 @@
import filterTests from "../../../support/filterTests"
filterTests(["all"], () => {
context("Job Application Functionality", () => {
context("Job Application Tracker Template Functionality", () => {
const templateName = "Job Application Tracker"
const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-')
before(() => {
cy.login()
cy.deleteApp(templateName)
cy.visit(`${Cypress.config().baseUrl}/builder`, {
// Template navigation
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, {
onBeforeLoad(win) {
cy.stub(win, 'open')
}
})
cy.wait(2000)
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
})
it("should create and publish app with Job Application Tracker template", () => {
@ -43,19 +35,10 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
})
// Publish App
// Publish App & Verify it opened
cy.wait(2000) // Wait for app to generate
cy.get(".toprightnav").contains("Publish").click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
})
// Verify Published app
cy.wait(2000) // Wait for App to publish and modal to appear
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("View App").click({ force: true })
cy.window().its('open').should('be.calledOnce')
})
cy.publishApp(true)
cy.window().its('open').should('be.calledOnce')
})
it("should add active/inactive vacancies", () => {

8
packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter IT Templates
cy.get(".template-category-filters").within(() => {

72
packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js

@ -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 })
})
}
})
})
})
})

8
packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Admin Panels Templates
cy.get(".template-category-filters").within(() => {

8
packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Approval Apps Templates
cy.get(".template-category-filters").within(() => {

10
packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js

@ -7,14 +7,8 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
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()

8
packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Directories Templates
cy.get(".template-category-filters").within(() => {

8
packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Forms Templates
cy.get(".template-category-filters").within(() => {

8
packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Healthcare Templates
cy.get(".template-category-filters").within(() => {

8
packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Legal Templates
cy.get(".template-category-filters").within(() => {

8
packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Logistics Templates
cy.get(".template-category-filters").within(() => {

8
packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Manufacturing Templates
cy.get(".template-category-filters").within(() => {

44
packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js

@ -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')
})
})
})

8
packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Marketing Templates
cy.get(".template-category-filters").within(() => {

8
packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Operations Templates
cy.get(".template-category-filters").within(() => {

18
packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js

@ -7,21 +7,15 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
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", () => {
// Filter Portal Templates
cy.get(".template-category-filters").within(() => {
cy.get('[data-cy="Portal"]').click()
})
cy.get(".template-grid").find(".template-card").its('length')
.then(len => {
for (let i = 0; i < len; i++) {

8
packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js

@ -7,13 +7,7 @@ filterTests(["all"], () => {
cy.login()
// Template navigation
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
}
})
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`)
// Filter Professional Services Templates
cy.get(".template-category-filters").within(() => {

1
packages/builder/cypress/setup.js

@ -7,7 +7,6 @@ const tmpdir = path.join(require("os").tmpdir(), ".budibase")
const SERVER_PORT = cypressConfig.env.PORT
const WORKER_PORT = cypressConfig.env.WORKER_PORT
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
process.env.NODE_ENV = "cypress"
process.env.ENABLE_ANALYTICS = "false"
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET

140
packages/builder/cypress/support/commands.js

@ -32,10 +32,19 @@ Cypress.Commands.add("login", () => {
})
})
Cypress.Commands.add("logOut", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".user-dropdown .avatar > .icon").click({ force: true })
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
cy.get("li[data-cy='user-logout']").click({ force: true })
})
cy.wait(2000)
})
Cypress.Commands.add("closeModal", () => {
cy.get(".spectrum-Modal").within(() => {
cy.get(".close-icon").click()
cy.wait(500)
cy.wait(1000) // Wait for modal to close
})
})
@ -209,6 +218,109 @@ Cypress.Commands.add("deleteAllApps", () => {
})
})
Cypress.Commands.add("customiseAppIcon", () => {
// Select random icon
cy.get(".grid").within(() => {
cy.get(".icon-item")
.eq(Math.floor(Math.random() * 23) + 1)
.click()
})
// Select random colour
cy.get(".fill").click()
cy.get(".colors").within(() => {
cy.get(".color")
.eq(Math.floor(Math.random() * 33) + 1)
.click()
})
cy.intercept("**/applications/**").as("iconChange")
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.wait("@iconChange")
cy.get("@iconChange").its("response.statusCode").should("eq", 200)
cy.wait(1000)
})
Cypress.Commands.add("alterAppVersion", (appId, version) => {
return cy
.request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, {
version: version || "0.0.1-alpha.0",
})
.then(resp => {
expect(resp.status).to.eq(200)
})
})
Cypress.Commands.add("updateAppName", (changedName, noName) => {
cy.get(".spectrum-Modal").within(() => {
if (noName == true) {
cy.get("input").clear()
cy.get(".spectrum-Dialog-grid")
.click()
.contains("App name must be letters, numbers and spaces only")
return cy
}
cy.get("input").clear()
cy.get("input")
.eq(0)
.type(changedName)
.should("have.value", changedName)
.blur()
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
cy.wait(500)
})
})
Cypress.Commands.add("unlockApp", unlock_config => {
let config = { ...unlock_config }
cy.get(".spectrum-Modal .spectrum-Dialog[data-cy='app-lock-modal']")
.should("be.visible")
.within(() => {
if (config.owned) {
cy.get(".spectrum-Dialog-heading").contains("Locked by you")
cy.get(".lock-expiry-body").contains(
"This lock will expire in 10 minutes from now"
)
cy.intercept("**/lock").as("unlockApp")
cy.get(".spectrum-Button")
.contains("Release Lock")
.click({ force: true })
cy.wait("@unlockApp")
cy.get("@unlockApp").its("response.statusCode").should("eq", 200)
cy.get("@unlockApp").its("response.body").should("deep.equal", {
message: "Lock released successfully.",
})
} else {
//Show the name ?
cy.get(".lock-expiry-body").should("not.be.visible")
cy.get(".spectrum-Button").contains("Done")
}
})
})
Cypress.Commands.add("publishApp", resolvedAppPath => {
//Assumes you have navigated to an application first
cy.get(".toprightnav button.spectrum-Button")
.contains("Publish")
.click({ force: true })
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']")
.should("be.visible")
.within(() => {
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
cy.wait(1000)
})
//Verify that the app url is presented correctly to the user
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
.should("be.visible")
.within(() => {
let appUrl = Cypress.config().baseUrl + "/app/" + resolvedAppPath
cy.get("[data-cy='deployed-app-url'] input").should("have.value", appUrl)
cy.get(".spectrum-Button").contains("Done").click({ force: true })
})
})
Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests"
cy.deleteApp(appName)
@ -222,6 +334,21 @@ Cypress.Commands.add("createTestTableWithData", () => {
cy.addColumn("dog", "age", "Number")
})
Cypress.Commands.add("publishApp", (viewApp = false) => {
cy.get(".toprightnav").contains("Publish").click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
})
cy.wait(2000) // Wait for App to publish and modal to appear
cy.get(".spectrum-Dialog-grid").within(() => {
if (viewApp) {
cy.get(".spectrum-Button").contains("View App").click({ force: true })
} else {
cy.get(".spectrum-Button").contains("Done").click({ force: true })
}
})
})
Cypress.Commands.add("createTable", (tableName, initialTable) => {
if (!initialTable) {
cy.navigateToDataSection()
@ -524,7 +651,12 @@ Cypress.Commands.add("createAppFromScratch", appName => {
.contains("Start from scratch")
.click({ force: true })
cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
cy.get("input")
.eq(0)
.clear()
.type(appName)
.should("have.value", appName)
.blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
cy.wait(10000)
})
@ -638,12 +770,14 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
.click({ force: true })
})
} else {
cy.intercept("**/tables").as("datasourceTables")
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button")
.contains("Save and fetch tables")
.click({ force: true })
cy.wait(3000)
})
// Wait for tables to be fetched
cy.wait("@datasourceTables", { timeout: 60000 })
}
})

20
packages/builder/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.0.159-alpha.3",
"version": "1.0.191-alpha.0",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -13,11 +13,13 @@
"cy:setup:ci": "node ./cypress/setup.js",
"cy:open": "cypress open",
"cy:run": "cypress run",
"cy:run:ci": "xvfb-run cypress run --headed --browser chrome",
"cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js",
"cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record",
"cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run",
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci",
"cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record",
"cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record && npm run cy:ci:report",
"cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline",
"cy:ci:notify": "node scripts/cypressResultsWebhook",
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",
"cy:debug:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:open"
},
@ -67,10 +69,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.159-alpha.3",
"@budibase/client": "^1.0.159-alpha.3",
"@budibase/frontend-core": "^1.0.159-alpha.3",
"@budibase/string-templates": "^1.0.159-alpha.3",
"@budibase/bbui": "^1.0.191-alpha.0",
"@budibase/client": "^1.0.191-alpha.0",
"@budibase/frontend-core": "^1.0.191-alpha.0",
"@budibase/string-templates": "^1.0.191-alpha.0",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
@ -98,9 +100,13 @@
"@testing-library/svelte": "^3.0.0",
"babel-jest": "^26.6.3",
"cypress": "^9.3.1",
"cypress-multi-reporters": "^1.6.0",
"cypress-terminal-report": "^1.4.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"mochawesome": "^7.1.3",
"mochawesome-merge": "^4.2.1",
"mochawesome-report-generator": "^6.2.0",
"ncp": "^2.0.0",
"rimraf": "^3.0.2",
"rollup": "^2.44.0",

10
packages/builder/reporterConfig.json

@ -0,0 +1,10 @@
{
"reporterEnabled": "mochawesome",
"mochawesomeReporterOptions": {
"reportDir": "cypress/reports",
"quiet": true,
"overwrite": false,
"html": false,
"json": true
}
}

130
packages/builder/scripts/cypressResultsWebhook.js

@ -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()

2
packages/builder/src/builderStore/store/automation/index.js

@ -105,9 +105,7 @@ const automationActions = store => ({
},
select: automation => {
store.update(state => {
let testResults = state.selectedAutomation?.testResults
state.selectedAutomation = new Automation(cloneDeep(automation))
state.selectedAutomation.testResults = testResults
state.selectedBlock = null
return state
})

107
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte

@ -14,7 +14,6 @@
} from "@budibase/bbui"
export let automation
let testDataModal
let blocks
let confirmDeleteDialog
@ -41,66 +40,70 @@
</script>
<div class="canvas">
<div class="content">
<div class="title">
<div class="subtitle">
<Heading size="S">{automation.name}</Heading>
<div style="display:flex; align-items: center;">
<div class="iconPadding">
<div class="icon">
<Icon
on:click={confirmDeleteDialog.show}
hoverable
size="M"
name="DeleteOutline"
/>
</div>
</div>
<div style="float: left; padding-left: var(--spacing-xl);">
<Heading size="S">{automation.name}</Heading>
</div>
<div style="float: right; padding-right: var(--spacing-xl);" class="title">
<div class="subtitle">
<div style="display:flex; align-items: center;">
<div class="icon">
<Icon
on:click={confirmDeleteDialog.show}
hoverable
size="M"
name="DeleteOutline"
/>
</div>
<ActionButton
on:click={() => {
testDataModal.show()
}}
icon="MultipleCheck"
size="M">Run test</ActionButton
>
<div style="padding-left: var(--spacing-m);">
<ActionButton
disabled={!$automationStore.selectedAutomation?.testResults}
on:click={() => {
testDataModal.show()
$automationStore.selectedAutomation.automation.showTestPanel = true
}}
icon="MultipleCheck"
size="M">Run test</ActionButton
size="M">Test Details</ActionButton
>
</div>
</div>
</div>
{#each blocks as block, idx (block.id)}
<div
class="block"
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }}
>
{#if block.stepId !== "LOOP"}
<FlowItem {testDataModal} {block} />
{/if}
</div>
{/each}
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Automation"
onOk={deleteAutomation}
title="Confirm Deletion"
>
Are you sure you wish to delete the automation
<i>{automation.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<Modal bind:this={testDataModal} width="30%">
<TestDataModal />
</Modal>
</div>
<div class="content">
{#each blocks as block, idx (block.id)}
<div
class="block"
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 500 }}
out:fly|local={{ x: 500, duration: 500 }}
>
{#if block.stepId !== "LOOP"}
<FlowItem {testDataModal} {block} />
{/if}
</div>
{/each}
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Automation"
onOk={deleteAutomation}
title="Confirm Deletion"
>
Are you sure you wish to delete the automation
<i>{automation.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<Modal bind:this={testDataModal} width="30%">
<TestDataModal />
</Modal>
<style>
.canvas {
margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
overflow-y: auto;
text-align: center;
height: 100%;
}
/* Fix for firefox not respecting bottom padding in scrolling containers */
.canvas > *:last-child {
padding-bottom: 40px;
@ -128,10 +131,6 @@
justify-content: space-between;
align-items: center;
}
.iconPadding {
padding-top: var(--spacing-s);
}
.icon {
cursor: pointer;
padding-right: var(--spacing-m);

75
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte

@ -1,40 +1,33 @@
<script>
import FlowItemHeader from "./FlowItemHeader.svelte"
import { automationStore } from "builderStore"
import {
Icon,
Divider,
Layout,
Body,
Detail,
Modal,
Button,
StatusLight,
Select,
ActionButton,
notifications,
} from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import ResultsModal from "./ResultsModal.svelte"
import ActionModal from "./ActionModal.svelte"
import { externalActions } from "./ExternalActions"
export let block
export let testDataModal
let selected
let webhookModal
let actionModal
let resultsModal
let blockComplete
let showLooping = false
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
$: showBindingPicker =
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW"
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
step => (block.id ? step.id === block.id : step.stepId === block.stepId)
)
$: isTrigger = block.type === "TRIGGER"
$: selected = $automationStore.selectedBlock?.id === block.id
@ -182,63 +175,7 @@
{/if}
{/if}
<div class="blockSection">
<div
on:click={() => {
blockComplete = !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 testResult && testResult[0]}
<div style="float: right;" on:click={() => resultsModal.show()}>
<StatusLight
positive={isTrigger || testResult[0].outputs?.success}
negative={!testResult[0].outputs?.success}
><Body size="XS">View response</Body></StatusLight
>
</div>
{/if}
<div
style="margin-left: 10px;"
on:click={() => {
onSelect(block)
}}
>
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
</div>
</div>
</div>
</div>
<FlowItemHeader bind:blockComplete {block} {testDataModal} />
{#if !blockComplete}
<Divider noMargin />
<div class="blockSection">
@ -256,7 +193,7 @@
on:change={toggleFieldControl}
defaultValue="Use values"
autoWidth
value={rowControl ? "Use bindings" : "Use values"}
value={block.rowControl ? "Use bindings" : "Use values"}
options={["Use values", "Use bindings"]}
placeholder={null}
/>
@ -283,10 +220,6 @@
</div>
{/if}
<Modal bind:this={resultsModal} width="30%">
<ResultsModal {isTrigger} {testResult} />
</Modal>
<Modal bind:this={actionModal} width="30%">
<ActionModal {blockIdx} bind:blockComplete />
</Modal>

111
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte

@ -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>

133
packages/builder/src/components/automation/AutomationBuilder/FlowChart/ResultsModal.svelte

@ -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>

1
packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte

@ -51,6 +51,7 @@
$automationStore.selectedAutomation?.automation,
testData
)
$automationStore.selectedAutomation.automation.showTestPanel = true
} catch (error) {
notifications.error("Error testing notification")
}

146
packages/builder/src/components/automation/AutomationBuilder/TestPanel.svelte

@ -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>

2
packages/builder/src/components/automation/Shared/WebhookDisplay.svelte

@ -12,4 +12,4 @@
}
</script>
<CopyInput {value} copyValue={fullWebhookURL(value)} />
<CopyInput value={fullWebhookURL(value)} />

4
packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte

@ -304,7 +304,9 @@
)
}
const newError = {}
if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
if (!external && fieldInfo.name?.startsWith("_")) {
newError.name = `Column name cannot start with an underscore.`
} else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
", "
)} are not allowed as column names`

2
packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte

@ -45,6 +45,8 @@
name,
schema: addAutoColumns(name, dataImport.schema || {}),
dataImport,
type: "internal",
sourceId: "bb_internal",
}
// Only set primary display if defined

144
packages/builder/src/components/common/AppLockModal.svelte

@ -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>

55
packages/builder/src/components/common/DashCard.svelte

@ -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>

47
packages/builder/src/components/common/EditableIcon.svelte

@ -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>

3
packages/builder/src/components/common/inputs/CopyInput.svelte

@ -3,7 +3,6 @@
export let label = null
export let value
export let copyValue
export let dataCy = null
const copyToClipboard = val => {
@ -19,7 +18,7 @@
<div data-cy={dataCy}>
<Input readonly {value} {label} />
<div class="icon" on:click={() => copyToClipboard(value || copyValue)}>
<div class="icon" on:click={() => copyToClipboard(value)}>
<Icon size="S" name="Copy" />
</div>
</div>

18
packages/builder/src/components/deploy/VersionModal.svelte

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

48
packages/builder/src/components/start/AppRow.svelte

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

56
packages/builder/src/components/usage/Usage.svelte

@ -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>

35
packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte

@ -4,7 +4,12 @@
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
$: automation = $automationStore.automations[0]
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
$: automation =
$automationStore.selectedAutomation?.automation ||
$automationStore.automations[0]
let modal
let webhookModal
</script>
@ -39,6 +44,12 @@
</div>
{/if}
</div>
{#if automation?.showTestPanel}
<div class="setup">
<TestPanel {automation} />
</div>
{/if}
<Modal bind:this={modal}>
<CreateAutomationModal {webhookModal} />
</Modal>
@ -52,7 +63,9 @@
flex: 1 1 auto;
height: 0;
display: grid;
grid-template-columns: 260px minmax(510px, 1fr);
grid-auto-flow: column dense;
grid-template-columns: 260px minmax(510px, 1fr) fit-content(500px);
overflow: hidden;
}
.nav {
@ -64,17 +77,18 @@
border-right: var(--border-light);
background-color: var(--background);
padding-bottom: 60px;
overflow: hidden;
}
.content {
position: relative;
padding: var(--spacing-l) 40px;
padding-top: var(--spacing-l);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
overflow: hidden;
overflow: auto;
}
.centered {
top: 0;
@ -92,4 +106,17 @@
.main {
width: 300px;
}
.setup {
padding-top: var(--spectrum-global-dimension-size-200);
border-left: var(--border-light);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
background-color: var(--background);
grid-column: 3;
overflow: auto;
}
</style>

6
packages/builder/src/pages/builder/auth/login.svelte

@ -28,7 +28,7 @@
async function login() {
try {
await auth.login({
username,
username: username.trim(),
password,
})
if ($auth?.user?.forceResetPassword) {
@ -80,7 +80,9 @@
/>
</Layout>
<Layout gap="XS" noPadding>
<Button cta on:click={login}>Sign in to {company}</Button>
<Button cta disabled={!username && !password} on:click={login}
>Sign in to {company}</Button
>
<ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password?
</ActionButton>

21
packages/builder/src/pages/builder/portal/_layout.svelte

@ -31,7 +31,20 @@
$: menu = buildMenu($auth.isAdmin)
const buildMenu = admin => {
let menu = [{ title: "Apps", href: "/builder/portal/apps" }]
let menu = [
{
title: "Apps",
href: "/builder/portal/apps",
},
]
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
menu = menu.concat([
{
title: "Usage",
href: "/builder/portal/settings/usage",
},
])
}
if (admin) {
menu = menu.concat([
{
@ -190,7 +203,9 @@
<MenuItem icon="UserDeveloper" on:click={() => $goto("../apps")}>
Close developer mode
</MenuItem>
<MenuItem icon="LogOut" on:click={logout}>Log out</MenuItem>
<MenuItem dataCy="user-logout" icon="LogOut" on:click={logout}
>Log out
</MenuItem>
</ActionMenu>
</div>
</div>
@ -319,7 +334,7 @@
.mobile-toggle,
.user-dropdown {
flex: 1 1 0;
flex: 0 1 0;
}
/* Reduce BBUI page padding */

8
packages/builder/src/pages/builder/portal/apps/create.svelte

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

98
packages/builder/src/pages/builder/portal/apps/index.svelte

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

9
packages/builder/src/pages/builder/portal/apps/templates.svelte

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

2
packages/builder/src/pages/builder/portal/manage/_layout.svelte

@ -12,7 +12,7 @@
</script>
{#if $auth.isAdmin}
<Page wide={$page.path.includes("email/:template")}>
<Page maxWidth="90ch" wide={$page.path.includes("email/:template")}>
<slot />
</Page>
{/if}

4
packages/builder/src/pages/builder/portal/manage/auth/index.svelte

@ -297,7 +297,7 @@
</Body>
</Layout>
{#if providers.google}
<Divider />
<Divider size="S" />
<Layout gap="XS" noPadding>
<Heading size="S">
<div class="provider-title">
@ -336,7 +336,7 @@
</Layout>
{/if}
{#if providers.oidc}
<Divider />
<Divider size="S" />
<Layout gap="XS" noPadding>
<Heading size="S">
<div class="provider-title">

4
packages/builder/src/pages/builder/portal/manage/email/index.svelte

@ -132,7 +132,7 @@
values below and click activate.
</Body>
</Layout>
<Divider />
<Divider size="S" />
{#if smtpConfig}
<Layout gap="XS" noPadding>
<Heading size="S">SMTP</Heading>
@ -186,7 +186,7 @@
Reset
</Button>
</div>
<Divider />
<Divider size="S" />
<Layout gap="XS" noPadding>
<Heading size="S">Templates</Heading>
<Body size="S">

5
packages/builder/src/pages/builder/portal/manage/users/_components/UpdateRolesModal.svelte

@ -14,7 +14,10 @@
let options = roles
.filter(role => role._id !== "PUBLIC")
.map(role => ({ value: role._id, label: role.name }))
options.push({ value: NO_ACCESS, label: "No Access" })
if (!user?.builder?.global) {
options.push({ value: NO_ACCESS, label: "No Access" })
}
let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() {

413
packages/builder/src/pages/builder/portal/overview/[application]/index.svelte

@ -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>

11
packages/builder/src/pages/builder/portal/overview/_components/AutomationTab.svelte

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

250
packages/builder/src/pages/builder/portal/overview/_components/OverviewTab.svelte

@ -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>

131
packages/builder/src/pages/builder/portal/overview/_components/SettingsTab.svelte

@ -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>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save