1
0
Fork 0
mirror of synced 2024-08-17 19:11:52 +12:00

Merge branch 'develop' of github.com:Budibase/budibase into feature/automation-logs

This commit is contained in:
mike12345567 2022-05-27 12:31:00 +01:00
commit 33a4c0de69
148 changed files with 3262 additions and 1283 deletions

View file

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

View file

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

View file

@ -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 vendored
View file

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

View file

@ -1,5 +1,5 @@
{
"version": "1.0.159-alpha.1",
"version": "1.0.185-alpha.7",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.159-alpha.1",
"version": "1.0.185-alpha.7",
"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"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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", () => {
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 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)
})
})
})
const DB_URL = "http://dburl.com"
const DEFAULT_URL = "http://localhost:10000"
const ENV_URL = "http://env.com"
const setDbPlatformUrl = async () => {
const db = tenancy.getGlobalDB()
db.put({
_id: "config_settings",
type: Configs.SETTINGS,
config: {
platformUrl: DB_URL
}
})
}
describe("app ID manipulation", () => {
it("should be able to generate a new app ID", () => {
expect(generateAppID().startsWith("app_")).toEqual(true)
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", () => {
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("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 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}`)
})
describe("cloud", () => {
const TENANT_AWARE_URL = "http://default.env.com"
it("should be able to convert a development ID to a production", () => {
const { devAppId, uuid } = getID()
expect(getProdAppID(devAppId)).toEqual(`app_${uuid}`)
})
beforeEach(async () => {
env._set("SELF_HOSTED", 0)
env._set("MULTI_TENANCY", 1)
env._set("PLATFORM_URL", ENV_URL)
await clearSettingsConfig()
})
it("should be able to convert a production ID to production", () => {
const { appId, uuid } = getID()
expect(getProdAppID(appId)).toEqual(`app_${uuid}`)
})
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("should be able to confirm dev app ID is development", () => {
const { devAppId } = getID()
expect(isDevAppID(devAppId)).toEqual(true)
})
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("should be able to confirm prod app ID is not development", () => {
const { appId } = getID()
expect(isDevAppID(appId)).toEqual(false)
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)
})
})
})
})
it("should be able to confirm prod app ID is prod", () => {
const { appId } = getID()
expect(isProdAppID(appId)).toEqual(true)
})
describe("getScopedConfig", () => {
describe("settings config", () => {
it("should be able to confirm dev app ID is not prod", () => {
const { devAppId } = getID()
expect(isProdAppID(devAppId)).toEqual(false)
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("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)
})
})
})
})
})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
*/

View file

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

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.159-alpha.1",
"version": "1.0.185-alpha.7",
"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.1",
"@budibase/string-templates": "^1.0.185-alpha.7",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<script>
import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import { createEventDispatcher, onMount } from "svelte"
export let value = null
export let placeholder = null
@ -13,8 +13,11 @@
export let quiet = false
export let dataCy
export let align
export let autofocus = false
const dispatch = createEventDispatcher()
let field
let focus = false
const updateValue = newValue => {
@ -58,6 +61,11 @@
updateValue(event.target.value)
}
}
onMount(() => {
focus = autofocus
if (focus) field.focus()
})
</script>
<div
@ -77,6 +85,7 @@
</svg>
{/if}
<input
bind:this={field}
{disabled}
{readonly}
{id}

View file

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

View file

@ -14,6 +14,7 @@
export let updateOnChange = true
export let quiet = false
export let dataCy
export let autofocus
const dispatch = createEventDispatcher()
const onChange = e => {
@ -33,6 +34,7 @@
{placeholder}
{type}
{quiet}
{autofocus}
on:change={onChange}
on:click
on:input

View file

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

View file

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

View file

@ -2,10 +2,18 @@
"baseUrl": "http://localhost:4100",
"video": true,
"projectId": "bmbemn",
"reporter": "cypress-multi-reporters",
"reporterOptions": {
"configFile": "reporterConfig.json"
},
"env": {
"PORT": "4100",
"WORKER_PORT": "4200",
"JWT_SECRET": "test",
"HOST_IP": ""
},
"retries": {
"runMode": 2,
"openMode": 0
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,5 @@
import filterTests from "../support/filterTests"
const interact = require('../support/interact')
filterTests(['all'], () => {
context("Add Multi-Option Datatype", () => {
@ -17,19 +18,19 @@ filterTests(['all'], () => {
cy.navigateToFrontend()
cy.wait(500)
// Add data provider
cy.get(`[data-cy="category-Data"]`).click()
cy.get(`[data-cy="component-Data Provider"]`).click()
cy.get('[data-cy="dataSource-prop-control"]').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.get(interact.CATEGORY_DATA).click()
cy.get(interact.COMPONENT_DATA_PROVIDER).click()
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
cy.get(interact.DROPDOWN).contains("Multi Data").click()
cy.wait(500)
// Add Form with schema to match table
cy.addComponent("Form", "Form")
cy.get('[data-cy="dataSource-prop-control"').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
cy.get(interact.DROPDOWN).contains("Multi Data").click()
cy.wait(500)
// Add multi-select picker to form
cy.addComponent("Form", "Multi-select Picker").then(componentId => {
cy.get('[data-cy="field-prop-control"]').type("Test Data").type("{enter}")
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}")
cy.wait(1000)
cy.getComponent(componentId).contains("Choose some options").click()
// Check picker has 5 items
@ -40,7 +41,7 @@ filterTests(['all'], () => {
}
// Check items have been selected
cy.getComponent(componentId)
.find(".spectrum-Picker-label")
.find(interact.SPECTRUM_Picker_LABEL)
.contains("(5)")
})
})

View file

@ -1,4 +1,5 @@
import filterTests from "../support/filterTests"
const interact = require('../support/interact')
filterTests(['all'], () => {
context("Add Radio Buttons", () => {
@ -12,10 +13,10 @@ filterTests(['all'], () => {
cy.addComponent("Form", "Form")
cy.addComponent("Form", "Options Picker").then((componentId) => {
// Provide field setting
cy.get(`[data-cy="field-prop-control"]`).type("1")
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("1")
// Open dropdown and select Radio buttons
cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => {
cy.get('.spectrum-Popover').contains('Radio buttons')
cy.get(interact.OPTION_TYPE_PROP_CONTROL).click().then(() => {
cy.get(interact.SPECTRUM_POPOVER).contains('Radio buttons')
.wait(500)
.click()
})
@ -28,8 +29,8 @@ filterTests(['all'], () => {
})
const addRadioButtonData = (totalRadioButtons) => {
cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => {
cy.get('.spectrum-Popover').contains('Custom')
cy.get(interact.OPTION_SOURCE_PROP_CONROL).click().then(() => {
cy.get(interact.SPECTRUM_POPOVER).contains('Custom')
.wait(500)
.click()
})

View file

@ -268,23 +268,77 @@ filterTests(['all'], () => {
cy.get(".details-section .page-action .spectrum-Button").should("be.disabled")
})
after(() => {
it("Should allow copying of the published application Id", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true})
cy.get(".appTable .app-row-actions").eq(0)
.within(() => {
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
})
cy.get(".deployment-top-nav svg[aria-label='Globe']")
.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='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.get(".overview-tab [data-cy='app-status']").within(() => {
cy.get(".status-display").contains("Unpublished")
cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist")
})
})
it("Should allow deleting of the application", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .name").eq(0).click()
cy.get(".app-overview-actions-icon > .icon").click({ force : true })
cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => {
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true })
cy.wait(500)
})
//The test application was renamed earlier in the spec
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get("input").type("sample name")
cy.get(".spectrum-Button--warning").click()
})
cy.location().should((loc) => {
expect(loc.pathname).to.eq('/builder/portal/apps')
})
cy.get(".appTable").should("not.exist")
cy.get(".welcome .container h1").contains("Let's create your first app!")
})
after(() => {
cy.deleteAllApps()
})

View file

@ -29,22 +29,8 @@ filterTests(['all'], () => {
it("Should publish an application and correctly reflect that", () => {
//Assuming the previous test was run and the unpublished app is open in edit mode.
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
.within(() => {
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
cy.wait(1000)
});
//Verify that the app url is presented correctly to the user
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
.should("be.visible")
.within(() => {
let appUrl = Cypress.config().baseUrl + '/app/cypress-tests'
cy.get("[data-cy='deployed-app-url'] input").should('have.value', appUrl)
cy.get(".spectrum-Button").contains("Done").click({ force: true })
})
cy.publishApp("cypress-tests")
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)

View file

@ -1,4 +1,5 @@
import filterTests from '../support/filterTests'
const interact = require('../support/interact')
filterTests(['smoke', 'all'], () => {
context("Create an Application", () => {
@ -10,14 +11,14 @@ filterTests(['smoke', 'all'], () => {
if (!(Cypress.env("TEST_ENV"))) {
it("should show the new user UI/UX", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').should("exist")
cy.get(`[data-cy="import-app-btn"]`).should("exist")
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`) //added /portal/apps/create
cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist")
cy.get(interact.CREATE_APP_BUTTON).should("exist")
cy.get(".template-category-filters").should("exist")
cy.get(".template-categories").should("exist")
cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
cy.get(interact.TEMPLATE_CATEGORY).should("exist")
cy.get(".appTable").should("not.exist")
cy.get(interact.APP_TABLE).should("not.exist")
})
}
@ -29,21 +30,21 @@ filterTests(['smoke', 'all'], () => {
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(".spectrum-Button").contains("Templates").click({force: true})
cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Templates").click({force: true})
}
})
cy.get(".template-category-filters").should("exist")
cy.get(".template-categories").should("exist")
cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
cy.get(interact.TEMPLATE_CATEGORY).should("exist")
cy.get(".template-category").its('length').should('be.gt', 1)
cy.get(".template-category-filters .spectrum-ActionButton").its('length').should('be.gt', 2)
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1)
cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).its('length').should('be.gt', 2)
cy.get(".template-category-filters .spectrum-ActionButton").eq(1).click()
cy.get(".template-category").should('have.length', 1)
cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(1).click()
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).should('have.length', 1)
cy.get(".template-category-filters .spectrum-ActionButton").eq(0).click()
cy.get(".template-category").its('length').should('be.gt', 1)
cy.get(interact.TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON).eq(0).click()
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).its('length').should('be.gt', 1)
})
it("should enforce a valid url before submission", () => {
@ -51,37 +52,40 @@ filterTests(['smoke', 'all'], () => {
cy.wait(500)
// Start create app process. If apps already exist, click second button
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
}
})
const appName = "Cypress Tests"
cy.get(".spectrum-Modal").within(() => {
cy.get(interact.SPECTRUM_MODAL).within(() => {
cy.get(interact.APP_NAME_INPUT).eq(0).should('have.focus')
//Auto fill
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
cy.get("input").eq(1).should("have.value", "/cypress-tests")
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
cy.get(interact.APP_NAME_INPUT).eq(0).clear()
cy.get(interact.APP_NAME_INPUT).eq(0).type(appName).should("have.value", appName).blur()
cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/cypress-tests")
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled')
//Empty the app url - disabled create
cy.get("input").eq(1).clear().blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
cy.get(interact.APP_NAME_INPUT).eq(1).clear().blur()
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled')
//Invalid url
cy.get("input").eq(1).type("/new app-url").blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
cy.get(interact.APP_NAME_INPUT).eq(1).type("/new app-url").blur()
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('be.disabled')
//Specifically alter the url
cy.get("input").eq(1).clear()
cy.get("input").eq(1).type("another-app-name").blur()
cy.get("input").eq(1).should("have.value", "/another-app-name")
cy.get("input").eq(0).should("have.value", appName)
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
cy.get(interact.APP_NAME_INPUT).eq(1).clear()
cy.get(interact.APP_NAME_INPUT).eq(1).type("another-app-name").blur()
cy.get(interact.APP_NAME_INPUT).eq(1).should("have.value", "/another-app-name")
cy.get(interact.APP_NAME_INPUT).eq(0).should("have.value", appName)
cy.get(interact.SPECTRUM_BUTTON_GROUP).contains("Create app").should('not.be.disabled')
})
})
@ -97,6 +101,77 @@ filterTests(['smoke', 'all'], () => {
cy.deleteApp(appName)
})
it("should create the first application from scratch with a default name", () => {
cy.createApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable("My app")
cy.deleteApp("My app")
})
it("should create the first application from scratch, using the users first name as the default app name", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.updateUserInformation("Ted", "Userman")
cy.createApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable("Teds app")
cy.deleteApp("Teds app")
cy.wait(2000)
//Accomodate names that end in 'S'
cy.updateUserInformation("Chris", "Userman")
cy.createApp()
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000)
cy.applicationInAppTable("Chris app")
cy.deleteApp("Chris app")
cy.wait(2000)
cy.updateUserInformation("", "")
})
it("should create an application from an export", () => {
const exportedApp = 'cypress/fixtures/exported-app.txt'
cy.importApp(exportedApp, "")
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.applicationInAppTable("My app")
cy.get(".appTable .name").eq(0).click()
cy.deleteApp("My app")
})
it("should create an application from an export, using the users first name as the default app name", () => {
const exportedApp = 'cypress/fixtures/exported-app.txt'
cy.updateUserInformation("Ted", "Userman")
cy.importApp(exportedApp, "")
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.applicationInAppTable("Teds app")
cy.get(".appTable .name").eq(0).click()
cy.deleteApp("Teds app")
cy.updateUserInformation("", "")
})
it("should generate the first application from a template", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500)
@ -106,15 +181,15 @@ filterTests(['smoke', 'all'], () => {
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
}
})
cy.get(".template-category-filters").should("exist")
cy.get(".template-categories").should("exist")
cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
cy.get(interact.TEMPLATE_CATEGORY).should("exist")
// Select template
cy.get('.template-category').eq(0).within(() => {
cy.get(interact.TEMPLATE_CATEGORY_ACTIONGROUP).eq(0).within(() => {
const card = cy.get('.template-card').eq(0).should("exist");
const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist")
cardOverlay.invoke("show")
@ -128,8 +203,8 @@ filterTests(['smoke', 'all'], () => {
templateName.invoke('text')
.then(templateNameText => {
const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-")
cy.get(".spectrum-Modal input").eq(0).should("have.value", templateNameText)
cy.get(".spectrum-Modal input").eq(1).should("have.value", templateNameParsed)
cy.get(interact.SPECTRUM_MODAL_INPUT).eq(0).should("have.value", templateNameText)
cy.get(interact.SPECTRUM_MODAL_INPUT).eq(1).should("have.value", templateNameParsed)
cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click()
cy.wait(5000)

View file

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

View file

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

View file

@ -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", () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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++) {

View file

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

View file

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

View file

@ -44,7 +44,72 @@ Cypress.Commands.add("logOut", () => {
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
})
})
Cypress.Commands.add("importApp", (exportFilePath, name) => {
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
cy.wait(500)
}
cy.get(`[data-cy="import-app-btn"]`).click({ force: true })
})
cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(1).should("have.focus")
cy.get(".spectrum-Dropzone").selectFile(exportFilePath, {
action: "drag-drop",
})
cy.get(".gallery .filename").contains("exported-app.txt")
if (name && name != "") {
cy.get("input").eq(0).type(name).should("have.value", name).blur()
}
cy.get(".confirm-wrap button")
.should("not.be.disabled")
.click({ force: true })
cy.wait(5000)
})
})
Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
cy.get(".user-dropdown .avatar > .icon").click({ force: true })
cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => {
cy.get("li[data-cy='user-info']").click({ force: true })
})
cy.get(".spectrum-Modal.is-open").within(() => {
cy.get("[data-cy='user-first-name']").clear()
if (!firstName || firstName == "") {
cy.get("[data-cy='user-first-name']").invoke("val").should("be.empty")
} else {
cy.get("[data-cy='user-first-name']")
.type(firstName)
.should("have.value", firstName)
.blur()
}
cy.get("[data-cy='user-last-name']").clear()
if (!lastName || lastName == "") {
cy.get("[data-cy='user-last-name']").invoke("val").should("be.empty")
} else {
cy.get("[data-cy='user-last-name']")
.type(lastName)
.should("have.value", lastName)
.blur()
}
cy.get("button").contains("Update information").click({ force: true })
})
})
@ -66,7 +131,11 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
})
cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur()
cy.get("input").eq(0).should("have.focus")
if (name && name != "") {
cy.get("input").eq(0).clear()
cy.get("input").eq(0).type(name).should("have.value", name).blur()
}
cy.get(".spectrum-ButtonGroup")
.contains("Create app")
.click({ force: true })
@ -229,6 +298,29 @@ Cypress.Commands.add("unlockApp", unlock_config => {
})
})
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)
@ -242,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()
@ -544,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)
})
@ -658,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(1000)
})
// Wait for tables to be fetched
cy.wait("@datasourceTables", { timeout: 60000 })
}
})

View file

@ -0,0 +1,26 @@
// createApp test
export const CREATE_APP_BUTTON = '[data-cy="create-app-btn"]'
export const TEMPLATE_CATEGORY_FILTER = ".template-category-filters"
export const TEMPLATE_CATEGORY = ".template-categories"
export const APP_TABLE = ".appTable"
export const SPECTRUM_BUTTON_TEMPLATE = ".spectrum-Button"
export const TEMPLATE_CATEGORY_ACTIONGROUP = ".template-category"
export const TEMPLATE_CATEGORY_FILTER_ACTIONBUTTON =
".template-category-filters .spectrum-ActionButton"
export const SPECTRUM_MODAL = ".spectrum-Modal"
export const APP_NAME_INPUT = "input" // we need to update this with atribute cy-data;
export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup"
export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input"
//AddMultiOptionDatatype test
export const CATEGORY_DATA = '[data-cy="category-Data"]'
export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]'
export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]'
export const DROPDOWN = ".dropdown"
export const SPECTRUM_Picker_LABEL = ".spectrum-Picker-label"
export const DATASOURCE_FIELD_CONTROL = '[data-cy="field-prop-control"]'
export const OPTION_TYPE_PROP_CONTROL = '[data-cy="optionsType-prop-control'
//AddRadioButtons
export const SPECTRUM_POPOVER = ".spectrum-Popover"
export const OPTION_SOURCE_PROP_CONROL = '[data-cy="optionsSource-prop-control'

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.0.159-alpha.1",
"version": "1.0.185-alpha.7",
"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.1",
"@budibase/client": "^1.0.159-alpha.1",
"@budibase/frontend-core": "^1.0.159-alpha.1",
"@budibase/string-templates": "^1.0.159-alpha.1",
"@budibase/bbui": "^1.0.185-alpha.7",
"@budibase/client": "^1.0.185-alpha.7",
"@budibase/frontend-core": "^1.0.185-alpha.7",
"@budibase/string-templates": "^1.0.185-alpha.7",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
@ -99,9 +101,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",

View file

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

View file

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

View file

@ -116,9 +116,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
})

View file

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

View file

@ -24,13 +24,9 @@
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
@ -178,12 +174,7 @@
{/if}
{/if}
<FlowItemHeader
{onSelect}
{block}
bind:results={testResult}
bind:open={blockComplete}
/>
<FlowItemHeader bind:blockComplete {block} {testDataModal} />
{#if !blockComplete}
<Divider noMargin />
<div class="blockSection">
@ -201,7 +192,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}
/>

View file

@ -1,23 +1,32 @@
<script>
import { Detail, Icon, Body, StatusLight, Modal } from "@budibase/bbui"
import { automationStore } from "builderStore"
import { Icon, Body, Detail, StatusLight } from "@budibase/bbui"
import { externalActions } from "./ExternalActions"
import ResultsModal from "./ResultsModal.svelte"
export let onSelect
export let block
export let results
export let useResultsModal = true
export let open
export let blockComplete
export let showTestStatus = false
export let showParameters = {}
$: isTrigger = block?.type === "TRIGGER"
$: testResult =
$automationStore.selectedAutomation?.testResults?.steps.filter(step =>
block.id ? step.id === block.id : step.stepId === block.stepId
)
$: isTrigger = block.type === "TRIGGER"
let resultsModal
async function onSelect(block) {
await automationStore.update(state => {
state.selectedBlock = block
return state
})
}
</script>
<div class="blockSection">
<div
on:click={() => {
open = !open
blockComplete = !blockComplete
showParameters[block.id] = blockComplete
}}
class="splitHeader"
>
@ -51,49 +60,41 @@
</div>
</div>
<div class="blockTitle">
{#if useResultsModal}
{#if results && results[0]}
<div style="float: right;" on:click={() => resultsModal.show()}>
<StatusLight
positive={isTrigger || results[0].outputs?.success}
negative={!results[0].outputs?.success}
><Body size="XS">View response</Body></StatusLight
>
</div>
{/if}
{:else}
<slot />
{#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;"
style="margin-left: 10px; margin-bottom: var(--spacing-xs);"
on:click={() => {
onSelect(block)
}}
>
<Icon name={open ? "ChevronDown" : "ChevronUp"} />
<Icon name={blockComplete ? "ChevronUp" : "ChevronDown"} />
</div>
</div>
</div>
</div>
{#if useResultsModal}
<Modal bind:this={resultsModal} width="30%">
<ResultsModal {isTrigger} bind:testResult={results} />
</Modal>
{/if}
<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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,8 +28,6 @@
$: lockedByHeading =
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
$: lockExpiry = getExpiryDuration(app)
const getExpiryDuration = app => {
if (!app?.lockedBy?.lockedAt) {
return -1
@ -86,12 +84,12 @@
between your team.
</p>
{#if lockedByYou && lockExpiry > 0}
{#if lockedByYou && getExpiryDuration(app) > 0}
<span class="lock-expiry-body">
{processStringSync(
"This lock will expire in {{ duration time 'millisecond' }} from now",
{
time: lockExpiry,
time: getExpiryDuration(app),
}
)}
</span>

View file

@ -10,20 +10,13 @@
</script>
<div class="dash-card" data-cy={dataCy}>
<div
class={actionDefined ? "dash-card-header active" : "dash-card-header"}
on:click={() => {
if (actionDefined) {
action()
}
}}
>
<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 ? actionIcon : "ChevronRight"} />
<Icon name={actionIcon || "ChevronRight"} />
{/if}
</span>
</div>
@ -36,18 +29,17 @@
.dash-card {
background: var(--spectrum-alias-background-color-primary);
border-radius: var(--border-radius-s);
border: 1px solid var(--spectrum-global-color-gray-300);
overflow: hidden;
min-height: 150px;
}
.dash-card-header {
padding: var(--spacing-l);
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-l);
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2);
}
.dash-card-title :global(.spectrum-Detail) {
color: var(

View file

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

View file

@ -27,6 +27,14 @@
Personalise the platform by adding your first name and last name.
</Body>
<Input disabled bind:value={$auth.user.email} label="Email" />
<Input bind:value={$values.firstName} label="First name" />
<Input bind:value={$values.lastName} label="Last name" />
<Input
bind:value={$values.firstName}
label="First name"
dataCy="user-first-name"
/>
<Input
bind:value={$values.lastName}
label="Last name"
dataCy="user-last-name"
/>
</ModalContent>

View file

@ -16,13 +16,26 @@
export let template
let creating = false
let defaultAppName
const values = writable({ name: "", url: null })
const validation = createValidationStore()
$: validation.check($values)
onMount(async () => {
$values.name = resolveAppName(template, $values.name)
const lastChar = $auth.user?.firstName
? $auth.user?.firstName[$auth.user?.firstName.length - 1]
: null
defaultAppName =
lastChar && lastChar.toLowerCase() == "s"
? `${$auth.user?.firstName} app`
: `${$auth.user.firstName}s app`
$values.name = resolveAppName(
template,
!$auth.user?.firstName ? "My app" : defaultAppName
)
nameToUrl($values.name)
await setupValidation()
})
@ -44,7 +57,7 @@
}
const resolveAppName = (template, name) => {
if (template && !name) {
if (template && !template.fromFile) {
return template.name
}
return name ? name.trim() : null
@ -83,7 +96,7 @@
}
data.append("useTemplate", template != null)
if (template) {
data.append("templateName", template.name) //or here?
data.append("templateName", template.name)
data.append("templateKey", template.key)
data.append("templateFile", $values.file)
}
@ -159,15 +172,14 @@
/>
{/if}
<Input
autofocus={true}
bind:value={$values.name}
disabled={creating}
error={$validation.touched.name && $validation.errors.name}
on:blur={() => ($validation.touched.name = true)}
on:change={nameToUrl($values.name)}
label="Name"
placeholder={$auth.user?.firstName
? `${$auth.user.firstName}s app`
: "My app"}
placeholder={defaultAppName}
/>
<span>
<Input

View file

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

View file

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

View file

@ -69,7 +69,7 @@
<Layout noPadding>
<div class="header">
<img alt="logo" src={$organisation.logoUrl || Logo} />
<ActionMenu align="right">
<ActionMenu align="right" dataCy="user-menu">
<div slot="control" class="avatar">
<Avatar
size="M"

View file

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

View file

@ -37,6 +37,14 @@
href: "/builder/portal/apps",
},
]
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
menu = menu.concat([
{
title: "Usage",
href: "/builder/portal/settings/usage",
},
])
}
if (admin) {
menu = menu.concat([
{
@ -174,7 +182,11 @@
/>
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
<MenuItem
icon="UserEdit"
on:click={() => userInfoModal.show()}
dataCy={"user-info"}
>
Update user information
</MenuItem>
{#if $auth.isBuilder}
@ -322,7 +334,7 @@
.mobile-toggle,
.user-dropdown {
flex: 1 1 0;
flex: 0 1 0;
}
/* Reduce BBUI page padding */

View file

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

View file

@ -10,7 +10,6 @@
notifications,
Body,
Search,
Divider,
Helpers,
} from "@budibase/bbui"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
@ -66,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,
@ -178,10 +180,6 @@
}
}
const previewApp = app => {
window.open(`/${app.devId}`)
}
const appOverview = app => {
$goto(`../overview/${app.devId}`)
}
@ -348,7 +346,7 @@
{/if}
{#if enrichedApps.length}
<Layout noPadding gap="S">
<Layout noPadding gap="L">
<div class="title">
<div class="buttons">
<Button
@ -414,8 +412,8 @@
</div>
{/if}
</div>
<Divider size="S" />
<div class="appTable">
<div class="appTable" class:unlocked>
{#each filteredApps as app (app.appId)}
<AppRow
{copyAppId}
@ -428,7 +426,6 @@
{exportApp}
{deleteApp}
{updateApp}
{previewApp}
{appOverview}
/>
{/each}
@ -490,6 +487,9 @@
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
<style>
.appTable {
border-top: var(--border-light);
}
.app-actions {
display: flex;
}
@ -533,6 +533,11 @@
grid-template-columns: 1fr 1fr 1fr 1fr auto;
align-items: center;
}
.appTable.unlocked {
grid-template-columns: 1fr 1fr auto 1fr auto;
}
.appTable :global(> div) {
height: 70px;
display: grid;

View file

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

View file

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

View file

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

View file

@ -68,6 +68,26 @@
}
}
async function deleteSmtp() {
// Delete the SMTP config
try {
await API.deleteConfig({
id: smtpConfig._id,
rev: smtpConfig._rev,
})
smtpConfig = {
config: {},
}
await admin.getChecklist()
notifications.success(`Settings cleared`)
analytics.captureEvent(Events.SMTP.SAVED)
} catch (error) {
notifications.error(
`Failed to clear email settings, reason: ${error?.message || "Unknown"}`
)
}
}
async function fetchSmtp() {
loading = true
try {
@ -112,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>
@ -156,10 +176,17 @@
</div>
{/if}
</Layout>
<div>
<div class="spectrum-ButtonGroup spectrum-Settings-buttonGroup">
<Button cta on:click={saveSmtp}>Save</Button>
<Button
secondary
on:click={deleteSmtp}
disabled={!$admin.checklist.smtp.checked}
>
Reset
</Button>
</div>
<Divider />
<Divider size="S" />
<Layout gap="XS" noPadding>
<Heading size="S">Templates</Heading>
<Body size="S">
@ -186,4 +213,8 @@
grid-gap: var(--spacing-l);
align-items: center;
}
.spectrum-Settings-buttonGroup {
gap: var(--spectrum-global-dimension-static-size-200);
align-items: flex-end;
}
</style>

View file

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

View file

@ -4,12 +4,18 @@
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"
@ -21,12 +27,17 @@
import AppLockModal from "components/common/AppLockModal.svelte"
import EditableIcon from "components/common/EditableIcon.svelte"
import HistoryTab from "components/portal/overview/HistoryTab.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)
@ -75,6 +86,7 @@
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}`)
@ -122,6 +134,58 @@
$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()
})
@ -131,6 +195,7 @@
if (!apps.length) {
await apps.load()
}
await API.syncApp(application)
deployments = await fetchDeployments()
} catch (error) {
notifications.error("Error initialising app overview")
@ -138,35 +203,93 @@
})
</script>
<Page wide>
<Layout noPadding gap="XL">
<span>
<Button quiet secondary icon={"ChevronLeft"} on:click={backToAppList}>
Back
</Button>
</span>
<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 _}
<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"}
/>
<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="app-details">
<Heading size="M">{selectedApp?.name}</Heading>
<div class="app-url">{appUrl}</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>
<div class="header-right">
@ -195,38 +318,73 @@
</Button>
</ButtonGroup>
</div>
</div>
<Tabs
selected={selectedTab}
noPadding
on:select={e => {
selectedTab = e.detail
}}
>
<Tab title="Overview">
<OverviewTab
app={selectedApp}
deployments={latestDeployments}
navigateTab={handleTabChange}
/>
</Tab>
<Tab title="Automation History">
<HistoryTab appId={selectedApp?._id} />
</Tab>
{#if false}
<Tab title="Backups">
<div class="container">Backups contents</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}
<Tab title="Settings">
<SettingsTab app={selectedApp} />
</Tab>
</Tabs>
<Tab title="Automation History">
<HistoryTab appId={selectedApp?._id} />
</Tab>
{#if false}
<Tab title="Backups">
<div class="container">Backups contents</div>
</Tab>
{/if}
<Tab title="Overview">
<OverviewTab
app={selectedApp}
deployments={latestDeployments}
navigateTab={handleTabChange}
/>
</Tab>
<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}
</Layout>
</Page>
</Page>
</span>
<style>
.app-url {
@ -235,11 +393,35 @@
.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);
@ -253,4 +435,13 @@
line-height: 1em;
margin-bottom: var(--spacing-s);
}
.tab-wrap :global(.spectrum-Tabs) {
padding-left: var(--spectrum-alias-grid-gutter-large);
padding-right: var(--spectrum-alias-grid-gutter-large);
}
.page-header {
padding-left: var(--spectrum-alias-grid-gutter-large);
padding-right: var(--spectrum-alias-grid-gutter-large);
padding-top: var(--spectrum-alias-grid-gutter-large);
}
</style>

View file

@ -1,7 +1,14 @@
<script>
import DashCard from "components/common/DashCard.svelte"
import { AppStatus } from "constants"
import { Icon, Heading, Link, Avatar, notifications } from "@budibase/bbui"
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"
@ -40,178 +47,211 @@
}
</script>
<div class="overview-tab">
<div class="top">
<DashCard title={"App Status"} dataCy={"app-status"}>
<div class="status-content">
<div class="status-display">
{#if isPublished}
<Icon name="GlobeCheck" size="XL" disabled={false} />
<span>Published</span>
{:else}
<Icon name="GlobeStrike" size="XL" disabled={true} />
<span class="disabled"> Unpublished </span>
{/if}
</div>
<p class="status-text">
{#if deployments?.length}
{processStringSync(
"Last published {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(deployments[0].updatedAt).getTime(),
}
)}
{/if}
{#if !deployments?.length}
-
{/if}
</p>
</div>
</DashCard>
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
<div class="last-edited-content">
{#await userPromise}
<Avatar size="M" initials={"-"} />
{:then _}
<div class="updated-by">
{#if appEditor}
<Avatar size="M" initials={getInitials(appEditor)} />
<div class="editor-name">
{appEditor._id === $auth.user._id ? "You" : appEditorText}
</div>
<div className="overview-tab">
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
<div className="top">
<DashCard title={"App Status"} dataCy={"app-status"}>
<div className="status-content">
<div className="status-display">
{#if isPublished}
<Icon name="GlobeCheck" size="XL" disabled={false} />
<span>Published</span>
{:else}
<Icon name="GlobeStrike" size="XL" disabled={true} />
<span className="disabled"> Unpublished </span>
{/if}
</div>
{:catch error}
<p>Could not fetch user: {error.message}</p>
{/await}
<p class="last-edit-text">
{#if app}
{processStringSync(
"Last edited {{ duration time 'millisecond' }} ago",
{
time: new Date().getTime() - new Date(app?.updatedAt).getTime(),
}
)}
{/if}
</p>
</div>
</DashCard>
<DashCard
title={"App Version"}
showIcon={true}
action={() => {
navigateTab("Settings")
}}
dataCy={"app-version"}
>
<div class="version-content" data-cy={$store.version}>
<Heading size="XS">{$store.version}</Heading>
{#if updateAvailable}
<p class="version-status">
New version <strong>{clientPackage.version}</strong> is available -
<Link
on:click={() => {
if (typeof navigateTab === "function") {
navigateTab("Settings")
<div class="status-text">
{#if deployments?.length}
{processStringSync(
"Last published {{ duration time 'millisecond' }} ago",
{
time:
new Date().getTime() -
new Date(deployments[0].updatedAt).getTime(),
}
}}
>
Update
</Link>
</p>
{:else}
<p class="version-status">You're running the latest!</p>
{/if}
</div>
</DashCard>
</div>
{#if false}
<div class="bottom">
<DashCard
title={"Automation History"}
action={() => {
navigateTab("Automation History")
}}
dataCy={"automation-overview"}
>
<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>
)}
{/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={"Backups"}
title={"App Version"}
showIcon={true}
action={() => {
navigateTab("Backups")
navigateTab("Settings")
}}
dataCy={"backups"}
dataCy={"app-version"}
>
<div class="backups-content">test</div>
<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}
{#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>
/* Add in size checks */
.overview-tab {
display: grid;
grid-gap: var(--spacing-xl);
}
.overview-tab .top {
display: grid;
grid-gap: var(--spacing-xl);
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(--spacing-xl);
grid-gap: var(--spectrum-alias-grid-gutter-large);
grid-template-columns: 1fr 1fr;
}
@media (max-width: 1000px) {
.overview-tab .top {
grid-template-columns: 1fr 1fr;
}
.overview-tab .bottom {
grid-template-columns: 1fr;
}
}
@media (max-width: 800px) {
.overview-tab .top,
.overview-tab .bottom {
grid-template-columns: 1fr;
}
}
.status-display {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.status-text,
.last-edit-text {
color: var(--spectrum-global-color-gray-600);
}
.updated-by {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.succeeded :global(.icon) {
color: var(--spectrum-global-color-green-600);
}
.failed :global(.icon) {
color: var(
--spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500)
);
}
.metric-info {
display: flex;
gap: var(--spacing-l);
margin-top: var(--spacing-s);
}
.version-status,
.last-edit-text,
.status-text {
padding-top: var(--spacing-xl);
}
</style>

View file

@ -28,10 +28,10 @@
<div class="settings-tab">
<Page wide={false}>
<Layout gap="XL" noPadding>
<Layout gap="XL" paddingY="XXL" paddingX="">
<span class="details-section">
<Layout gap="XS" noPadding>
<Heading size="S">Name and Url</Heading>
<Heading size="S">Name and URL</Heading>
<Divider />
<Body>
<div class="app-details">
@ -60,8 +60,8 @@
</Layout>
</span>
<span class="version-section">
<Layout gap="XS" noPadding>
<Heading size="S">App Version</Heading>
<Layout gap="XS" paddingY="XXL" paddingX="">
<Heading size="S">App version</Heading>
<Divider />
<Body>
{#if updateAvailable}
@ -76,28 +76,26 @@
<strong>{$store.version}</strong>. You're running the latest!
</p>
{/if}
<p>
Updates can contain new features, performance improvements and bug
fixes.
</p>
Updates can contain new features, performance improvements and bug
fixes.
<div class="page-action">
<Button cta on:click={versionModal.show()}>Update App</Button>
<Button cta on:click={versionModal.show()}>Update app</Button>
</div>
</Body>
</Layout>
</span>
<span class="selfhost-section">
<Layout gap="XS" noPadding>
<Layout gap="XS" paddingY="XXL" paddingX="">
<Heading size="S">Self-host Budibase</Heading>
<Divider />
<Body>
<p>
Self-host Budibase for free to get unlimited apps and more - and
it only takes a few minutes!
</p>
Self-host Budibase for free to get unlimited apps and more - and it
only takes a few minutes!
<div class="page-action">
<Button
cta
secondary
on:click={() => {
window.open(selfHostPath, "_blank")
}}>Self-host Budibase</Button

View file

@ -2,6 +2,6 @@
import { Page } from "@budibase/bbui"
</script>
<Page>
<Page maxWidth="90ch">
<slot />
</Page>

View file

@ -0,0 +1,139 @@
<script>
import {
Body,
Divider,
Heading,
Layout,
notifications,
Link,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { admin, auth, licensing } from "stores/portal"
import Usage from "components/usage/Usage.svelte"
let staticUsage = []
let monthlyUsage = []
let loaded = false
$: quotaUsage = $licensing.quotaUsage
$: license = $auth.user?.license
const upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
const setMonthlyUsage = () => {
monthlyUsage = []
if (quotaUsage.monthly) {
for (let [key, value] of Object.entries(license.quotas.usage.monthly)) {
const used = quotaUsage.monthly.current[key]
if (used !== undefined) {
monthlyUsage.push({
name: value.name,
used: used,
total: value.value,
})
}
}
}
}
const setStaticUsage = () => {
staticUsage = []
for (let [key, value] of Object.entries(license.quotas.usage.static)) {
const used = quotaUsage.usageQuota[key]
if (used !== undefined) {
staticUsage.push({
name: value.name,
used: used,
total: value.value,
})
}
}
}
const capitalise = string => {
if (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
}
const init = async () => {
try {
await licensing.getQuotaUsage()
} catch (e) {
console.error(e)
notifications.error(e)
}
}
onMount(async () => {
await init()
loaded = true
})
$: {
if (license && quotaUsage) {
setMonthlyUsage()
setStaticUsage()
}
}
</script>
{#if loaded}
<Layout>
<Heading>Usage</Heading>
<Body
>Get information about your current usage within Budibase.
{#if $admin.cloud}
{#if $auth.user?.accountPortalAccess}
To upgrade your plan and usage limits visit your <Link
size="L"
href={upgradeUrl}>Account</Link
>.
{:else}
Contact your account holder to upgrade your usage limits.
{/if}
{/if}
</Body>
</Layout>
<Layout gap="S">
<Divider size="S" />
</Layout>
<Layout gap="S" noPadding>
<Layout gap="XS">
<Body size="S">YOUR PLAN</Body>
<Heading size="S">{capitalise(license?.plan.type)}</Heading>
</Layout>
<Layout gap="S">
<Body size="S">USAGE</Body>
<div class="usages">
{#each staticUsage as usage}
<div class="usage">
<Usage {usage} />
</div>
{/each}
</div>
</Layout>
{#if monthlyUsage.length}
<Layout gap="S">
<Body size="S">MONTHLY</Body>
<div class="usages">
{#each monthlyUsage as usage}
<div class="usage">
<Usage {usage} />
</div>
{/each}
</div>
</Layout>
<div />
{/if}
</Layout>
{/if}
<style>
.usages {
display: grid;
column-gap: 60px;
row-gap: 50px;
grid-template-columns: 1fr 1fr 1fr;
}
</style>

View file

@ -6,3 +6,4 @@ export { email } from "./email"
export { auth } from "./auth"
export { oidc } from "./oidc"
export { templates } from "./templates"
export { licensing } from "./licensing"

View file

@ -0,0 +1,29 @@
import { writable } from "svelte/store"
import { API } from "api"
export const createLicensingStore = () => {
const DEFAULT = {
plans: {},
}
const store = writable(DEFAULT)
const actions = {
getQuotaUsage: async () => {
const quotaUsage = await API.getQuotaUsage()
store.update(state => {
return {
...state,
quotaUsage,
}
})
},
}
return {
subscribe: store.subscribe,
...actions,
}
}
export const licensing = createLicensingStore()

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