Merge branch 'develop' into feature/posthog-v2
This commit is contained in:
commit
70fff5c7b9
|
@ -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
|
|
@ -1,4 +1,5 @@
|
|||
name: Budibase Release Staging
|
||||
concurrency: release-develop
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
|
@ -87,3 +87,10 @@ jobs:
|
|||
packages/cli/build/cli-macos
|
||||
packages/server/specs/openapi.yaml
|
||||
packages/server/specs/openapi.json
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v4.0.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
content: "Self Host Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Self Host."
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
|
@ -1,4 +1,5 @@
|
|||
name: Budibase Release
|
||||
concurrency: release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
|
@ -31,24 +31,22 @@ jobs:
|
|||
continue-on-error: true
|
||||
uses: cypress-io/github-action@v2
|
||||
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.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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -114,8 +114,8 @@ spec:
|
|||
value: {{ .Values.globals.google.secret | quote }}
|
||||
- name: AUTOMATION_MAX_ITERATIONS
|
||||
value: {{ .Values.globals.automationMaxIterations | quote }}
|
||||
- name: EXCLUDE_QUOTAS_TENANTS
|
||||
value: {{ .Values.globals.excludeQuotasTenants | quote }}
|
||||
- name: TENANT_FEATURE_FLAGS
|
||||
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||
|
||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||
imagePullPolicy: Always
|
||||
|
|
|
@ -30,7 +30,7 @@ http {
|
|||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default "upgrade";
|
||||
}
|
||||
|
@ -42,13 +42,13 @@ http {
|
|||
client_max_body_size 1000m;
|
||||
ignore_invalid_headers off;
|
||||
proxy_buffering off;
|
||||
|
||||
|
||||
set $csp_default "default-src 'self'";
|
||||
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io";
|
||||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||
set $csp_object "object-src 'none'";
|
||||
set $csp_base_uri "base-uri 'self'";
|
||||
set $csp_connect "connect-src 'self' https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.*.amazonaws.com";
|
||||
set $csp_connect "connect-src 'self' https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com";
|
||||
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
||||
set $csp_frame "frame-src 'self' https:";
|
||||
set $csp_img "img-src http: https: data: blob:";
|
||||
|
@ -58,7 +58,7 @@ http {
|
|||
|
||||
error_page 502 503 504 /error.html;
|
||||
location = /error.html {
|
||||
root /usr/share/nginx/html;
|
||||
root /usr/share/nginx/html;
|
||||
internal;
|
||||
}
|
||||
|
||||
|
@ -154,4 +154,4 @@ http {
|
|||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -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,6 +49,7 @@
|
|||
"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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -15,7 +15,8 @@
|
|||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
"aws-sdk": "^2.901.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cls-hooked": "^4.2.2",
|
||||
"dotenv": "^16.0.1",
|
||||
"emitter-listener": "^1.1.2",
|
||||
"ioredis": "^4.27.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"koa-passport": "^4.1.4",
|
||||
|
|
|
@ -0,0 +1,650 @@
|
|||
const util = require("util")
|
||||
const assert = require("assert")
|
||||
const wrapEmitter = require("emitter-listener")
|
||||
const async_hooks = require("async_hooks")
|
||||
|
||||
const CONTEXTS_SYMBOL = "cls@contexts"
|
||||
const ERROR_SYMBOL = "error@context"
|
||||
|
||||
const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED
|
||||
|
||||
let currentUid = -1
|
||||
|
||||
module.exports = {
|
||||
getNamespace: getNamespace,
|
||||
createNamespace: createNamespace,
|
||||
destroyNamespace: destroyNamespace,
|
||||
reset: reset,
|
||||
ERROR_SYMBOL: ERROR_SYMBOL,
|
||||
}
|
||||
|
||||
function Namespace(name) {
|
||||
this.name = name
|
||||
// changed in 2.7: no default context
|
||||
this.active = null
|
||||
this._set = []
|
||||
this.id = null
|
||||
this._contexts = new Map()
|
||||
this._indent = 0
|
||||
this._hook = null
|
||||
}
|
||||
|
||||
Namespace.prototype.set = function set(key, value) {
|
||||
if (!this.active) {
|
||||
throw new Error(
|
||||
"No context available. ns.run() or ns.bind() must be called first."
|
||||
)
|
||||
}
|
||||
|
||||
this.active[key] = value
|
||||
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
indentStr +
|
||||
"CONTEXT-SET KEY:" +
|
||||
key +
|
||||
"=" +
|
||||
value +
|
||||
" in ns:" +
|
||||
this.name +
|
||||
" currentUid:" +
|
||||
currentUid +
|
||||
" active:" +
|
||||
util.inspect(this.active, { showHidden: true, depth: 2, colors: true })
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
Namespace.prototype.get = function get(key) {
|
||||
if (!this.active) {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const asyncHooksCurrentId = async_hooks.currentId()
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
`${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}`
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const asyncHooksCurrentId = async_hooks.executionAsyncId()
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
indentStr +
|
||||
"CONTEXT-GETTING KEY:" +
|
||||
key +
|
||||
"=" +
|
||||
this.active[key] +
|
||||
" (" +
|
||||
this.name +
|
||||
") currentUid:" +
|
||||
currentUid +
|
||||
" active:" +
|
||||
util.inspect(this.active, { showHidden: true, depth: 2, colors: true })
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${
|
||||
this.active[key]
|
||||
} currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${
|
||||
this._set.length
|
||||
} active:${util.inspect(this.active)}`
|
||||
)
|
||||
}
|
||||
return this.active[key]
|
||||
}
|
||||
|
||||
Namespace.prototype.createContext = function createContext() {
|
||||
// Prototype inherit existing context if created a new child context within existing context.
|
||||
let context = Object.create(this.active ? this.active : Object.prototype)
|
||||
context._ns_name = this.name
|
||||
context.id = currentUid
|
||||
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const asyncHooksCurrentId = async_hooks.executionAsyncId()
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
`${indentStr}CONTEXT-CREATED Context: (${
|
||||
this.name
|
||||
}) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${
|
||||
this._set.length
|
||||
} context:${util.inspect(context, {
|
||||
showHidden: true,
|
||||
depth: 2,
|
||||
colors: true,
|
||||
})}`
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
Namespace.prototype.run = function run(fn) {
|
||||
let context = this.createContext()
|
||||
this.enter(context)
|
||||
|
||||
try {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const asyncHooksCurrentId = async_hooks.executionAsyncId()
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
`${indentStr}CONTEXT-RUN BEGIN: (${
|
||||
this.name
|
||||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
|
||||
this._set.length
|
||||
} context:${util.inspect(context)}`
|
||||
)
|
||||
}
|
||||
fn(context)
|
||||
return context
|
||||
} catch (exception) {
|
||||
if (exception) {
|
||||
exception[ERROR_SYMBOL] = context
|
||||
}
|
||||
throw exception
|
||||
} finally {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const asyncHooksCurrentId = async_hooks.executionAsyncId()
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
`${indentStr}CONTEXT-RUN END: (${
|
||||
this.name
|
||||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
|
||||
this._set.length
|
||||
} ${util.inspect(context)}`
|
||||
)
|
||||
}
|
||||
this.exit(context)
|
||||
}
|
||||
}
|
||||
|
||||
Namespace.prototype.runAndReturn = function runAndReturn(fn) {
|
||||
let value
|
||||
this.run(function (context) {
|
||||
value = fn(context)
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses global Promise and assumes Promise is cls friendly or wrapped already.
|
||||
* @param {function} fn
|
||||
* @returns {*}
|
||||
*/
|
||||
Namespace.prototype.runPromise = function runPromise(fn) {
|
||||
let context = this.createContext()
|
||||
this.enter(context)
|
||||
|
||||
let promise = fn(context)
|
||||
if (!promise || !promise.then || !promise.catch) {
|
||||
throw new Error("fn must return a promise.")
|
||||
}
|
||||
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
debug2(
|
||||
"CONTEXT-runPromise BEFORE: (" +
|
||||
this.name +
|
||||
") currentUid:" +
|
||||
currentUid +
|
||||
" len:" +
|
||||
this._set.length +
|
||||
" " +
|
||||
util.inspect(context)
|
||||
)
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(result => {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
debug2(
|
||||
"CONTEXT-runPromise AFTER then: (" +
|
||||
this.name +
|
||||
") currentUid:" +
|
||||
currentUid +
|
||||
" len:" +
|
||||
this._set.length +
|
||||
" " +
|
||||
util.inspect(context)
|
||||
)
|
||||
}
|
||||
this.exit(context)
|
||||
return result
|
||||
})
|
||||
.catch(err => {
|
||||
err[ERROR_SYMBOL] = context
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
debug2(
|
||||
"CONTEXT-runPromise AFTER catch: (" +
|
||||
this.name +
|
||||
") currentUid:" +
|
||||
currentUid +
|
||||
" len:" +
|
||||
this._set.length +
|
||||
" " +
|
||||
util.inspect(context)
|
||||
)
|
||||
}
|
||||
this.exit(context)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
Namespace.prototype.bind = function bindFactory(fn, context) {
|
||||
if (!context) {
|
||||
if (!this.active) {
|
||||
context = this.createContext()
|
||||
} else {
|
||||
context = this.active
|
||||
}
|
||||
}
|
||||
|
||||
let self = this
|
||||
return function clsBind() {
|
||||
self.enter(context)
|
||||
try {
|
||||
return fn.apply(this, arguments)
|
||||
} catch (exception) {
|
||||
if (exception) {
|
||||
exception[ERROR_SYMBOL] = context
|
||||
}
|
||||
throw exception
|
||||
} finally {
|
||||
self.exit(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Namespace.prototype.enter = function enter(context) {
|
||||
assert.ok(context, "context must be provided for entering")
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const asyncHooksCurrentId = async_hooks.executionAsyncId()
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
`${indentStr}CONTEXT-ENTER: (${
|
||||
this.name
|
||||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
|
||||
this._set.length
|
||||
} ${util.inspect(context)}`
|
||||
)
|
||||
}
|
||||
|
||||
this._set.push(this.active)
|
||||
this.active = context
|
||||
}
|
||||
|
||||
Namespace.prototype.exit = function exit(context) {
|
||||
assert.ok(context, "context must be provided for exiting")
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const asyncHooksCurrentId = async_hooks.executionAsyncId()
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent)
|
||||
debug2(
|
||||
`${indentStr}CONTEXT-EXIT: (${
|
||||
this.name
|
||||
}) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${
|
||||
this._set.length
|
||||
} ${util.inspect(context)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Fast path for most exits that are at the top of the stack
|
||||
if (this.active === context) {
|
||||
assert.ok(this._set.length, "can't remove top context")
|
||||
this.active = this._set.pop()
|
||||
return
|
||||
}
|
||||
|
||||
// Fast search in the stack using lastIndexOf
|
||||
let index = this._set.lastIndexOf(context)
|
||||
|
||||
if (index < 0) {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
debug2(
|
||||
"??ERROR?? context exiting but not entered - ignoring: " +
|
||||
util.inspect(context)
|
||||
)
|
||||
}
|
||||
assert.ok(
|
||||
index >= 0,
|
||||
"context not currently entered; can't exit. \n" +
|
||||
util.inspect(this) +
|
||||
"\n" +
|
||||
util.inspect(context)
|
||||
)
|
||||
} else {
|
||||
assert.ok(index, "can't remove top context")
|
||||
this._set.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Namespace.prototype.bindEmitter = function bindEmitter(emitter) {
|
||||
assert.ok(
|
||||
emitter.on && emitter.addListener && emitter.emit,
|
||||
"can only bind real EEs"
|
||||
)
|
||||
|
||||
let namespace = this
|
||||
let thisSymbol = "context@" + this.name
|
||||
|
||||
// Capture the context active at the time the emitter is bound.
|
||||
function attach(listener) {
|
||||
if (!listener) {
|
||||
return
|
||||
}
|
||||
if (!listener[CONTEXTS_SYMBOL]) {
|
||||
listener[CONTEXTS_SYMBOL] = Object.create(null)
|
||||
}
|
||||
|
||||
listener[CONTEXTS_SYMBOL][thisSymbol] = {
|
||||
namespace: namespace,
|
||||
context: namespace.active,
|
||||
}
|
||||
}
|
||||
|
||||
// At emit time, bind the listener within the correct context.
|
||||
function bind(unwrapped) {
|
||||
if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) {
|
||||
return unwrapped
|
||||
}
|
||||
|
||||
let wrapped = unwrapped
|
||||
let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL]
|
||||
Object.keys(unwrappedContexts).forEach(function (name) {
|
||||
let thunk = unwrappedContexts[name]
|
||||
wrapped = thunk.namespace.bind(wrapped, thunk.context)
|
||||
})
|
||||
return wrapped
|
||||
}
|
||||
|
||||
wrapEmitter(emitter, attach, bind)
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error comes out of a namespace, it will have a context attached to it.
|
||||
* This function knows how to find it.
|
||||
*
|
||||
* @param {Error} exception Possibly annotated error.
|
||||
*/
|
||||
Namespace.prototype.fromException = function fromException(exception) {
|
||||
return exception[ERROR_SYMBOL]
|
||||
}
|
||||
|
||||
function getNamespace(name) {
|
||||
return process.namespaces[name]
|
||||
}
|
||||
|
||||
function createNamespace(name) {
|
||||
assert.ok(name, "namespace must be given a name.")
|
||||
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
debug2(`NS-CREATING NAMESPACE (${name})`)
|
||||
}
|
||||
let namespace = new Namespace(name)
|
||||
namespace.id = currentUid
|
||||
|
||||
const hook = async_hooks.createHook({
|
||||
init(asyncId, type, triggerId, resource) {
|
||||
currentUid = async_hooks.executionAsyncId()
|
||||
|
||||
//CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec
|
||||
// let initContext = namespace.active;
|
||||
// if(!initContext && triggerId) {
|
||||
// let parentContext = namespace._contexts.get(triggerId);
|
||||
// if (parentContext) {
|
||||
// namespace.active = parentContext;
|
||||
// namespace._contexts.set(currentUid, parentContext);
|
||||
// if (DEBUG_CLS_HOOKED) {
|
||||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
||||
// debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
||||
// }
|
||||
// } else if (DEBUG_CLS_HOOKED) {
|
||||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
||||
// debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
||||
// }
|
||||
// }else {
|
||||
// namespace._contexts.set(currentUid, namespace.active);
|
||||
// if (DEBUG_CLS_HOOKED) {
|
||||
// const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent);
|
||||
// debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`);
|
||||
// }
|
||||
// }
|
||||
if (namespace.active) {
|
||||
namespace._contexts.set(asyncId, namespace.active)
|
||||
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} resource:${resource}`
|
||||
)
|
||||
}
|
||||
} else if (currentUid === 0) {
|
||||
// CurrentId will be 0 when triggered from C++. Promise events
|
||||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const triggerIdContext = namespace._contexts.get(triggerId)
|
||||
if (triggerIdContext) {
|
||||
namespace._contexts.set(asyncId, triggerIdContext)
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} resource:${resource}`
|
||||
)
|
||||
}
|
||||
} else if (DEBUG_CLS_HOOKED) {
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} resource:${resource}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG_CLS_HOOKED && type === "PROMISE") {
|
||||
debug2(util.inspect(resource, { showHidden: true }))
|
||||
const parentId = resource.parentId
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} resource:${resource}`
|
||||
)
|
||||
}
|
||||
},
|
||||
before(asyncId) {
|
||||
currentUid = async_hooks.executionAsyncId()
|
||||
let context
|
||||
|
||||
/*
|
||||
if(currentUid === 0){
|
||||
// CurrentId will be 0 when triggered from C++. Promise events
|
||||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
||||
//const triggerId = async_hooks.triggerAsyncId();
|
||||
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
||||
}else{
|
||||
context = namespace._contexts.get(currentUid);
|
||||
}
|
||||
*/
|
||||
|
||||
//HACK to work with promises until they are fixed in node > 8.1.1
|
||||
context =
|
||||
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid)
|
||||
|
||||
if (context) {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} context:${util.inspect(context)}`
|
||||
)
|
||||
namespace._indent += 2
|
||||
}
|
||||
|
||||
namespace.enter(context)
|
||||
} else if (DEBUG_CLS_HOOKED) {
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} namespace._contexts:${util.inspect(namespace._contexts, {
|
||||
showHidden: true,
|
||||
depth: 2,
|
||||
colors: true,
|
||||
})}`
|
||||
)
|
||||
namespace._indent += 2
|
||||
}
|
||||
},
|
||||
after(asyncId) {
|
||||
currentUid = async_hooks.executionAsyncId()
|
||||
let context // = namespace._contexts.get(currentUid);
|
||||
/*
|
||||
if(currentUid === 0){
|
||||
// CurrentId will be 0 when triggered from C++. Promise events
|
||||
// https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid
|
||||
//const triggerId = async_hooks.triggerAsyncId();
|
||||
context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId);
|
||||
}else{
|
||||
context = namespace._contexts.get(currentUid);
|
||||
}
|
||||
*/
|
||||
//HACK to work with promises until they are fixed in node > 8.1.1
|
||||
context =
|
||||
namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid)
|
||||
|
||||
if (context) {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
namespace._indent -= 2
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} context:${util.inspect(context)}`
|
||||
)
|
||||
}
|
||||
|
||||
namespace.exit(context)
|
||||
} else if (DEBUG_CLS_HOOKED) {
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
namespace._indent -= 2
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} context:${util.inspect(context)}`
|
||||
)
|
||||
}
|
||||
},
|
||||
destroy(asyncId) {
|
||||
currentUid = async_hooks.executionAsyncId()
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
const triggerId = async_hooks.triggerAsyncId()
|
||||
const indentStr = " ".repeat(
|
||||
namespace._indent < 0 ? 0 : namespace._indent
|
||||
)
|
||||
debug2(
|
||||
`${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect(
|
||||
namespace.active,
|
||||
{ showHidden: true, depth: 2, colors: true }
|
||||
)} context:${util.inspect(namespace._contexts.get(currentUid))}`
|
||||
)
|
||||
}
|
||||
|
||||
namespace._contexts.delete(asyncId)
|
||||
},
|
||||
})
|
||||
|
||||
hook.enable()
|
||||
namespace._hook = hook
|
||||
|
||||
process.namespaces[name] = namespace
|
||||
return namespace
|
||||
}
|
||||
|
||||
function destroyNamespace(name) {
|
||||
let namespace = getNamespace(name)
|
||||
|
||||
assert.ok(namespace, "can't delete nonexistent namespace! \"" + name + '"')
|
||||
assert.ok(
|
||||
namespace.id,
|
||||
"don't assign to process.namespaces directly! " + util.inspect(namespace)
|
||||
)
|
||||
|
||||
namespace._hook.disable()
|
||||
namespace._contexts = null
|
||||
process.namespaces[name] = null
|
||||
}
|
||||
|
||||
function reset() {
|
||||
// must unregister async listeners
|
||||
if (process.namespaces) {
|
||||
Object.keys(process.namespaces).forEach(function (name) {
|
||||
destroyNamespace(name)
|
||||
})
|
||||
}
|
||||
process.namespaces = Object.create(null)
|
||||
}
|
||||
|
||||
process.namespaces = process.namespaces || {}
|
||||
|
||||
//const fs = require('fs');
|
||||
function debug2(...args) {
|
||||
if (DEBUG_CLS_HOOKED) {
|
||||
//fs.writeSync(1, `${util.format(...args)}\n`);
|
||||
process._rawDebug(`${util.format(...args)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/*function getFunctionName(fn) {
|
||||
if (!fn) {
|
||||
return fn;
|
||||
}
|
||||
if (typeof fn === 'function') {
|
||||
if (fn.name) {
|
||||
return fn.name;
|
||||
}
|
||||
return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1];
|
||||
} else if (fn.constructor && fn.constructor.name) {
|
||||
return fn.constructor.name;
|
||||
}
|
||||
}*/
|
|
@ -1,84 +1,47 @@
|
|||
const cls = require("cls-hooked")
|
||||
const cls = require("../clshooked")
|
||||
const { newid } = require("../hashing")
|
||||
|
||||
const REQUEST_ID_KEY = "requestId"
|
||||
const MAIN_CTX = cls.createNamespace("main")
|
||||
|
||||
function getContextStorage(namespace) {
|
||||
if (namespace && namespace.active) {
|
||||
let contextData = namespace.active
|
||||
delete contextData.id
|
||||
delete contextData._ns_name
|
||||
return contextData
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
class FunctionContext {
|
||||
static getMiddleware(
|
||||
updateCtxFn = null,
|
||||
destroyFn = null,
|
||||
contextName = "session"
|
||||
) {
|
||||
const namespace = this.createNamespace(contextName)
|
||||
|
||||
return async function (ctx, next) {
|
||||
await new Promise(
|
||||
namespace.bind(function (resolve, reject) {
|
||||
// store a contextual request ID that can be used anywhere (audit logs)
|
||||
namespace.set(REQUEST_ID_KEY, newid())
|
||||
namespace.bindEmitter(ctx.req)
|
||||
namespace.bindEmitter(ctx.res)
|
||||
|
||||
if (updateCtxFn) {
|
||||
updateCtxFn(ctx)
|
||||
}
|
||||
next()
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => {
|
||||
if (destroyFn) {
|
||||
return destroyFn(ctx)
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
static run(callback) {
|
||||
return MAIN_CTX.runAndReturn(async () => {
|
||||
const namespaceId = newid()
|
||||
MAIN_CTX.set(REQUEST_ID_KEY, namespaceId)
|
||||
const namespace = cls.createNamespace(namespaceId)
|
||||
let response = await namespace.runAndReturn(callback)
|
||||
cls.destroyNamespace(namespaceId)
|
||||
return response
|
||||
})
|
||||
}
|
||||
|
||||
static run(callback, contextName = "session") {
|
||||
const namespace = this.createNamespace(contextName)
|
||||
|
||||
return namespace.runAndReturn(callback)
|
||||
}
|
||||
|
||||
static setOnContext(key, value, contextName = "session") {
|
||||
const namespace = this.createNamespace(contextName)
|
||||
static setOnContext(key, value) {
|
||||
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY)
|
||||
const namespace = cls.getNamespace(namespaceId)
|
||||
namespace.set(key, value)
|
||||
}
|
||||
|
||||
static getContextStorage() {
|
||||
if (this._namespace && this._namespace.active) {
|
||||
let contextData = this._namespace.active
|
||||
delete contextData.id
|
||||
delete contextData._ns_name
|
||||
return contextData
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
static getFromContext(key) {
|
||||
const context = this.getContextStorage()
|
||||
const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY)
|
||||
const namespace = cls.getNamespace(namespaceId)
|
||||
const context = getContextStorage(namespace)
|
||||
if (context) {
|
||||
return context[key]
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static destroyNamespace(name = "session") {
|
||||
if (this._namespace) {
|
||||
cls.destroyNamespace(name)
|
||||
this._namespace = null
|
||||
}
|
||||
}
|
||||
|
||||
static createNamespace(name = "session") {
|
||||
if (!this._namespace) {
|
||||
this._namespace = cls.createNamespace(name)
|
||||
}
|
||||
return this._namespace
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FunctionContext
|
||||
|
|
|
@ -55,6 +55,15 @@ async function closeAppDBs() {
|
|||
}
|
||||
}
|
||||
|
||||
exports.closeTenancy = async () => {
|
||||
if (env.USE_COUCH) {
|
||||
await closeDB(exports.getGlobalDB())
|
||||
}
|
||||
// clear from context now that database is closed/task is finished
|
||||
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
||||
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
||||
}
|
||||
|
||||
exports.isDefaultTenant = () => {
|
||||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
|
||||
}
|
||||
|
@ -82,12 +91,7 @@ exports.doInTenant = (tenantId, task) => {
|
|||
} finally {
|
||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||
if (!using || using <= 1) {
|
||||
if (env.USE_COUCH) {
|
||||
await closeDB(exports.getGlobalDB())
|
||||
}
|
||||
// clear from context now that database is closed/task is finished
|
||||
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
||||
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
||||
await exports.closeTenancy()
|
||||
} else {
|
||||
cls.setOnContext(using - 1)
|
||||
}
|
||||
|
|
|
@ -23,24 +23,30 @@ exports.isDevApp = app => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a development app ID to a deployed app ID.
|
||||
* Generates a development app ID from a real app ID.
|
||||
* @returns {string} the dev app ID which can be used for dev database.
|
||||
*/
|
||||
exports.getProdAppID = appId => {
|
||||
// if dev, convert it
|
||||
if (appId.startsWith(APP_DEV_PREFIX)) {
|
||||
const id = appId.split(APP_DEV_PREFIX)[1]
|
||||
return `${APP_PREFIX}${id}`
|
||||
exports.getDevelopmentAppID = appId => {
|
||||
if (!appId || appId.startsWith(APP_DEV_PREFIX)) {
|
||||
return appId
|
||||
}
|
||||
return appId
|
||||
// split to take off the app_ element, then join it together incase any other app_ exist
|
||||
const split = appId.split(APP_PREFIX)
|
||||
split.shift()
|
||||
const rest = split.join(APP_PREFIX)
|
||||
return `${APP_DEV_PREFIX}${rest}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a deployed app ID to a development app ID.
|
||||
* Convert a development app ID to a deployed app ID.
|
||||
*/
|
||||
exports.getDevelopmentAppID = appId => {
|
||||
if (!appId.startsWith(APP_DEV_PREFIX)) {
|
||||
const id = appId.split(APP_PREFIX)[1]
|
||||
return `${APP_DEV_PREFIX}${id}`
|
||||
exports.getProdAppID = appId => {
|
||||
if (!appId || !appId.startsWith(APP_DEV_PREFIX)) {
|
||||
return appId
|
||||
}
|
||||
return appId
|
||||
// split to take off the app_dev element, then join it together incase any other app_ exist
|
||||
const split = appId.split(APP_DEV_PREFIX)
|
||||
split.shift()
|
||||
const rest = split.join(APP_DEV_PREFIX)
|
||||
return `${APP_PREFIX}${rest}`
|
||||
}
|
||||
|
|
|
@ -0,0 +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")
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe("cloud", () => {
|
||||
const TENANT_AWARE_URL = "http://default.env.com"
|
||||
|
||||
beforeEach(async () => {
|
||||
env._set("SELF_HOSTED", 0)
|
||||
env._set("MULTI_TENANCY", 1)
|
||||
env._set("PLATFORM_URL", ENV_URL)
|
||||
await clearSettingsConfig()
|
||||
})
|
||||
|
||||
it("gets the platform url from the environment without tenancy", async () => {
|
||||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||
const url = await getPlatformUrl({ tenantAware: false })
|
||||
expect(url).toBe(ENV_URL)
|
||||
})
|
||||
})
|
||||
|
||||
it("gets the platform url from the environment with tenancy", async () => {
|
||||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||
const url = await getPlatformUrl()
|
||||
expect(url).toBe(TENANT_AWARE_URL)
|
||||
})
|
||||
})
|
||||
|
||||
it("never gets the platform url from the database", async () => {
|
||||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||
await setDbPlatformUrl()
|
||||
const url = await getPlatformUrl()
|
||||
expect(url).toBe(TENANT_AWARE_URL)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getScopedConfig", () => {
|
||||
describe("settings config", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
env._set("SELF_HOSTED", 1)
|
||||
env._set("PLATFORM_URL", "")
|
||||
await clearSettingsConfig()
|
||||
})
|
||||
|
||||
it("returns the platform url with an existing config", async () => {
|
||||
await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => {
|
||||
await setDbPlatformUrl()
|
||||
const db = tenancy.getGlobalDB()
|
||||
const config = await getScopedConfig(db, { type: Configs.SETTINGS })
|
||||
expect(config.platformUrl).toBe(DB_URL)
|
||||
})
|
||||
})
|
||||
|
||||
it("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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -2,13 +2,14 @@ import { newid } from "../hashing"
|
|||
import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
||||
import * as env from "../environment"
|
||||
import { SEPARATOR, DocumentTypes } from "./constants"
|
||||
import { getTenantId, getGlobalDBName } from "../tenancy"
|
||||
import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy"
|
||||
import fetch from "node-fetch"
|
||||
import { doWithDB, allDbs } from "./index"
|
||||
import { getCouchInfo } from "./pouch"
|
||||
import { getAppMetadata } from "../cache/appMetadata"
|
||||
import { checkSlashesInUrl } from "../helpers"
|
||||
import { isDevApp, isDevAppID } from "./conversions"
|
||||
import { APP_PREFIX } from "./constants"
|
||||
|
||||
const UNICODE_MAX = "\ufff0"
|
||||
|
||||
|
@ -22,6 +23,18 @@ export * from "./constants"
|
|||
export * from "./conversions"
|
||||
export { default as Replication } from "./Replication"
|
||||
|
||||
/**
|
||||
* Generates a new app ID.
|
||||
* @returns {string} The new app ID which the app doc can be stored under.
|
||||
*/
|
||||
export const generateAppID = (tenantId = null) => {
|
||||
let id = APP_PREFIX
|
||||
if (tenantId) {
|
||||
id += `${tenantId}${SEPARATOR}`
|
||||
}
|
||||
return `${id}${newid()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
||||
|
@ -370,9 +383,7 @@ export const getScopedFullConfig = async function (
|
|||
// 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: {
|
||||
|
@ -387,19 +398,30 @@ export const getScopedFullConfig = async function (
|
|||
return scopedConfig && scopedConfig.doc
|
||||
}
|
||||
|
||||
export const getPlatformUrl = async (settings?: any) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -36,9 +44,14 @@ module.exports = {
|
|||
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
||||
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
||||
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
||||
BACKUPS_BUCKET_NAME: process.env.BACKUPS_BUCKET_NAME || "backups",
|
||||
APPS_BUCKET_NAME: process.env.APPS_BUCKET_NAME || "prod-budi-app-assets",
|
||||
TEMPLATES_BUCKET_NAME: process.env.TEMPLATES_BUCKET_NAME || "templates",
|
||||
GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global",
|
||||
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,
|
||||
_set(key, value) {
|
||||
process.env[key] = value
|
||||
module.exports[key] = value
|
||||
|
|
|
@ -49,4 +49,5 @@ exports.getTenantFeatureFlags = tenantId => {
|
|||
|
||||
exports.FeatureFlag = {
|
||||
LICENSING: "LICENSING",
|
||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ module.exports = {
|
|||
env: require("./environment"),
|
||||
accounts: require("./cloud/accounts"),
|
||||
tenancy: require("./tenancy"),
|
||||
context: require("../context"),
|
||||
featureFlags: require("./featureFlags"),
|
||||
events,
|
||||
analytics: require("./analytics"),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const { setTenantId, setGlobalDB, getGlobalDB } = require("../tenancy")
|
||||
const { closeDB } = require("../db")
|
||||
const ContextFactory = require("../context/FunctionContext")
|
||||
const { setTenantId, setGlobalDB, closeTenancy } = require("../tenancy")
|
||||
const cls = require("../context/FunctionContext")
|
||||
const { buildMatcherRegex, matches } = require("./matchers")
|
||||
|
||||
module.exports = (
|
||||
|
@ -11,17 +10,16 @@ module.exports = (
|
|||
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
|
||||
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
|
||||
|
||||
const updateCtxFn = ctx => {
|
||||
const allowNoTenant =
|
||||
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
|
||||
const allowQs = !!matches(ctx, allowQsOptions)
|
||||
const tenantId = setTenantId(ctx, { allowQs, allowNoTenant })
|
||||
setGlobalDB(tenantId)
|
||||
return async function (ctx, next) {
|
||||
return cls.run(async () => {
|
||||
const allowNoTenant =
|
||||
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
|
||||
const allowQs = !!matches(ctx, allowQsOptions)
|
||||
const tenantId = setTenantId(ctx, { allowQs, allowNoTenant })
|
||||
setGlobalDB(tenantId)
|
||||
const res = await next()
|
||||
await closeTenancy()
|
||||
return res
|
||||
})
|
||||
}
|
||||
const destroyFn = async () => {
|
||||
const db = getGlobalDB()
|
||||
await closeDB(db)
|
||||
}
|
||||
|
||||
return ContextFactory.getMiddleware(updateCtxFn, destroyFn)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
const { join } = require("path")
|
||||
const { tmpdir } = require("os")
|
||||
const env = require("../environment")
|
||||
|
||||
exports.ObjectStoreBuckets = {
|
||||
BACKUPS: "backups",
|
||||
APPS: "prod-budi-app-assets",
|
||||
TEMPLATES: "templates",
|
||||
GLOBAL: "global",
|
||||
GLOBAL_CLOUD: "prod-budi-tenant-uploads",
|
||||
BACKUPS: env.BACKUPS_BUCKET_NAME,
|
||||
APPS: env.APPS_BUCKET_NAME,
|
||||
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
||||
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
||||
GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME,
|
||||
}
|
||||
|
||||
exports.budibaseTempDir = function () {
|
||||
|
|
|
@ -96,6 +96,7 @@ const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionTypes.QUERY, PermissionLevels.WRITE),
|
||||
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
|
||||
new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
|
||||
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
|
||||
],
|
||||
},
|
||||
POWER: {
|
||||
|
|
|
@ -847,13 +847,6 @@ ast-types@0.9.6:
|
|||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
|
||||
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
|
||||
|
||||
async-hook-jl@^1.7.6:
|
||||
version "1.7.6"
|
||||
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
|
||||
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
|
||||
dependencies:
|
||||
stack-chain "^1.3.7"
|
||||
|
||||
async@~2.1.4:
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
|
||||
|
@ -1255,15 +1248,6 @@ clone-buffer@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
|
||||
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
|
||||
|
||||
cls-hooked@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
|
||||
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
|
||||
dependencies:
|
||||
async-hook-jl "^1.7.6"
|
||||
emitter-listener "^1.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
cluster-key-slot@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
|
||||
|
@ -1608,6 +1592,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"
|
||||
|
@ -1645,7 +1634,7 @@ electron-to-chromium@^1.3.896:
|
|||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5"
|
||||
integrity sha512-SuXbQD8D4EjsaBaJJxySHbC+zq8JrFfxtb4GIr4E9n1BcROyMcRrJCYQNpJ9N+Wjf5mFp7Wp0OHykd14JNEzzQ==
|
||||
|
||||
emitter-listener@^1.0.1:
|
||||
emitter-listener@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
|
||||
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
|
||||
|
@ -4789,7 +4778,7 @@ saxes@^5.0.1:
|
|||
dependencies:
|
||||
xmlchars "^2.2.0"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
@ -5034,11 +5023,6 @@ sshpk@^1.7.0:
|
|||
jsbn "~0.1.0"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
stack-chain@^1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
|
||||
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
|
||||
|
||||
stack-utils@^2.0.2:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"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.142",
|
||||
"@budibase/string-templates": "^1.0.167-alpha.8",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<script>
|
||||
import "@spectrum-css/fieldgroup/dist/index-vars.css"
|
||||
import "@spectrum-css/radio/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let direction = "vertical"
|
||||
export let value = []
|
||||
export let options = []
|
||||
export let error = null
|
||||
export let disabled = false
|
||||
export let getOptionLabel = option => option
|
||||
export let getOptionValue = option => option
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
let tempValue = value
|
||||
let isChecked = e.target.checked
|
||||
if (!tempValue.includes(e.target.value) && isChecked) {
|
||||
tempValue.push(e.target.value)
|
||||
}
|
||||
value = tempValue
|
||||
dispatch(
|
||||
"change",
|
||||
tempValue.filter(val => val !== e.target.value || isChecked)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
|
||||
{#if options && Array.isArray(options)}
|
||||
{#each options as option}
|
||||
<div
|
||||
title={getOptionLabel(option)}
|
||||
class="spectrum-Checkbox spectrum-FieldGroup-item"
|
||||
class:is-invalid={!!error}
|
||||
>
|
||||
<label
|
||||
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item"
|
||||
>
|
||||
<input
|
||||
on:change={onChange}
|
||||
value={getOptionValue(option)}
|
||||
type="checkbox"
|
||||
class="spectrum-Checkbox-input"
|
||||
{disabled}
|
||||
checked={value.includes(getOptionValue(option))}
|
||||
/>
|
||||
<span class="spectrum-Checkbox-box">
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="spectrum-Checkbox-label">{getOptionLabel(option)}</span>
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spectrum-Checkbox-input {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -156,8 +161,8 @@
|
|||
<input
|
||||
data-input
|
||||
type="text"
|
||||
{disabled}
|
||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||
class:is-disabled={disabled}
|
||||
{placeholder}
|
||||
{id}
|
||||
{value}
|
||||
|
@ -167,7 +172,7 @@
|
|||
type="button"
|
||||
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
|
||||
tabindex="-1"
|
||||
{disabled}
|
||||
class:is-disabled={disabled}
|
||||
class:is-invalid={!!error}
|
||||
on:click={flatpickr?.open}
|
||||
>
|
||||
|
@ -212,4 +217,7 @@
|
|||
:global(.flatpickr-calendar) {
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
}
|
||||
.is-disabled {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
return
|
||||
}
|
||||
searchTerm = null
|
||||
open = true
|
||||
open = !open
|
||||
}
|
||||
|
||||
const getSortedOptions = (options, getLabel, sort) => {
|
||||
|
@ -71,105 +71,73 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
{id}
|
||||
class="spectrum-Picker spectrum-Picker--sizeM"
|
||||
class:spectrum-Picker--quiet={quiet}
|
||||
{disabled}
|
||||
class:is-invalid={!!error}
|
||||
class:is-open={open}
|
||||
aria-haspopup="listbox"
|
||||
on:mousedown={onClick}
|
||||
>
|
||||
{#if fieldIcon}
|
||||
<span class="icon-Placeholder-Padding">
|
||||
<img src={fieldIcon} alt="icon" width="20" height="15" />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class="spectrum-Picker-label"
|
||||
class:is-placeholder={isPlaceholder}
|
||||
class:auto-width={autoWidth}
|
||||
<div use:clickOutside={() => (open = false)}>
|
||||
<button
|
||||
{id}
|
||||
class="spectrum-Picker spectrum-Picker--sizeM"
|
||||
class:spectrum-Picker--quiet={quiet}
|
||||
{disabled}
|
||||
class:is-invalid={!!error}
|
||||
class:is-open={open}
|
||||
aria-haspopup="listbox"
|
||||
on:mousedown={onClick}
|
||||
>
|
||||
{fieldText}
|
||||
</span>
|
||||
{#if error}
|
||||
{#if fieldIcon}
|
||||
<span class="icon-Placeholder-Padding">
|
||||
<img src={fieldIcon} alt="icon" width="20" height="15" />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span
|
||||
class="spectrum-Picker-label"
|
||||
class:is-placeholder={isPlaceholder}
|
||||
class:auto-width={autoWidth}
|
||||
>
|
||||
{fieldText}
|
||||
</span>
|
||||
{#if error}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label="Folder"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||
</svg>
|
||||
{/if}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
|
||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label="Folder"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||
</svg>
|
||||
{/if}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if open}
|
||||
<div
|
||||
use:clickOutside={() => (open = false)}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
class:auto-width={autoWidth}
|
||||
>
|
||||
{#if autocomplete}
|
||||
<Search
|
||||
value={searchTerm}
|
||||
on:change={event => (searchTerm = event.detail)}
|
||||
{disabled}
|
||||
placeholder="Search"
|
||||
/>
|
||||
{/if}
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#if placeholderOption}
|
||||
<li
|
||||
class="spectrum-Menu-item placeholder"
|
||||
class:is-selected={isPlaceholder}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => onSelectOption(null)}
|
||||
>
|
||||
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</li>
|
||||
</button>
|
||||
{#if open}
|
||||
<div
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
class:auto-width={autoWidth}
|
||||
>
|
||||
{#if autocomplete}
|
||||
<Search
|
||||
value={searchTerm}
|
||||
on:change={event => (searchTerm = event.detail)}
|
||||
{disabled}
|
||||
placeholder="Search"
|
||||
/>
|
||||
{/if}
|
||||
{#if filteredOptions.length}
|
||||
{#each filteredOptions as option, idx}
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#if placeholderOption}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
||||
class="spectrum-Menu-item placeholder"
|
||||
class:is-selected={isPlaceholder}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||
on:click={() => onSelectOption(null)}
|
||||
>
|
||||
{#if getOptionIcon(option, idx)}
|
||||
<span class="icon-Padding">
|
||||
<img
|
||||
src={getOptionIcon(option, idx)}
|
||||
alt="icon"
|
||||
width="20"
|
||||
height="15"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
|
@ -178,11 +146,44 @@
|
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if filteredOptions.length}
|
||||
{#each filteredOptions as option, idx}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||
>
|
||||
{#if getOptionIcon(option, idx)}
|
||||
<span class="icon-Padding">
|
||||
<img
|
||||
src={getOptionIcon(option, idx)}
|
||||
alt="icon"
|
||||
width="20"
|
||||
height="15"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spectrum-Popover {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -3,6 +3,7 @@ export { default as CoreSelect } from "./Select.svelte"
|
|||
export { default as CoreMultiselect } from "./Multiselect.svelte"
|
||||
export { default as CoreCheckbox } from "./Checkbox.svelte"
|
||||
export { default as CoreRadioGroup } from "./RadioGroup.svelte"
|
||||
export { default as CoreCheckboxGroup } from "./CheckboxGroup.svelte"
|
||||
export { default as CoreTextArea } from "./TextArea.svelte"
|
||||
export { default as CoreCombobox } from "./Combobox.svelte"
|
||||
export { default as CoreSwitch } from "./Switch.svelte"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
{
|
||||
"baseUrl": "http://localhost:4100",
|
||||
"video": false,
|
||||
"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
|
@ -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)")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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,14 +203,14 @@ 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)
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(1000)
|
||||
cy.wait(2000)
|
||||
|
||||
cy.applicationInAppTable(templateNameText)
|
||||
cy.deleteApp(templateNameText)
|
||||
|
|
|
@ -24,7 +24,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should add a URL param binding", () => {
|
||||
xit("should add a URL param binding", () => {
|
||||
const paramName = "foo"
|
||||
cy.createScreen(`/test/:${paramName}`)
|
||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||
|
|
|
@ -9,7 +9,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
|
||||
it("Should successfully create a screen", () => {
|
||||
cy.createScreen("/test")
|
||||
cy.createScreen("test")
|
||||
cy.get(".nav-items-container").within(() => {
|
||||
cy.contains("/test").should("exist")
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
@ -66,19 +68,20 @@ filterTests(["smoke", "all"], () => {
|
|||
.then(() => {
|
||||
cy.wait(1000)
|
||||
if (i == 0) {
|
||||
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||
cy.get(".spectrum-Menu").contains("Admin").click({ force: true })
|
||||
}
|
||||
if (i == 1) {
|
||||
cy.get(".spectrum-Popover").contains("Power").click()
|
||||
else if (i == 1) {
|
||||
cy.get(".spectrum-Menu").contains("Power").click({ force: true })
|
||||
}
|
||||
if (i == 2) {
|
||||
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||
else if (i == 2) {
|
||||
cy.get(".spectrum-Menu").contains("Basic").click({ force: true })
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
})
|
||||
cy.reload()
|
||||
}
|
||||
// Confirm roles exist within Configure roles table
|
||||
cy.wait(2000)
|
||||
|
|
|
@ -11,7 +11,7 @@ filterTests(["all"], () => {
|
|||
const queryName = "Cypress Test Query"
|
||||
const queryRename = "CT Query Rename"
|
||||
|
||||
it("Should add PostgreSQL data source without configuration", () => {
|
||||
xit("Should add PostgreSQL data source without configuration", () => {
|
||||
// Select PostgreSQL data source
|
||||
cy.selectExternalDatasource(datasource)
|
||||
// Attempt to fetch tables without applying configuration
|
||||
|
@ -107,7 +107,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should delete a relationship", () => {
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
|
||||
cy.reload()
|
||||
// Delete one relationship
|
||||
cy.get(".spectrum-Table")
|
||||
|
@ -155,7 +155,7 @@ filterTests(["all"], () => {
|
|||
|
||||
it("should switch to schema with no tables", () => {
|
||||
// Switch Schema - To one without any tables
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
|
||||
switchSchema("randomText")
|
||||
|
||||
// No tables displayed
|
||||
|
|
|
@ -26,6 +26,8 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
it("should revert a published app", () => {
|
||||
cy.navigateToFrontend()
|
||||
|
||||
// Add initial component - Paragraph
|
||||
cy.addComponent("Elements", "Paragraph")
|
||||
// Publish app
|
||||
|
@ -37,6 +39,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||
})
|
||||
|
||||
// Add second component - Button
|
||||
cy.addComponent("Elements", "Button")
|
||||
// Click Revert
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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, '-')
|
||||
|
||||
|
@ -14,15 +14,7 @@ filterTests(["all"], () => {
|
|||
}
|
||||
})
|
||||
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})
|
||||
}
|
||||
})
|
||||
cy.templateNavigation()
|
||||
})
|
||||
|
||||
it("should create and publish app with Job Application Tracker template", () => {
|
||||
|
@ -57,5 +49,183 @@ filterTests(["all"], () => {
|
|||
cy.window().its('open').should('be.calledOnce')
|
||||
})
|
||||
})
|
||||
|
||||
it("should add active/inactive vacancies", () => {
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
|
||||
// loop for active/inactive vacancies
|
||||
for (let i = 0; i < 2; i++) {
|
||||
// Vacancies section
|
||||
cy.get(".links").contains("Vacancies").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Create New").click()
|
||||
|
||||
// Add inactive vacancy
|
||||
// Title
|
||||
cy.get('[data-name="Title"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("Tester")
|
||||
})
|
||||
|
||||
// Closing Date
|
||||
cy.get('[data-name="Closing date"]').within(() => {
|
||||
cy.get('[aria-label=Calendar]').click({ force: true })
|
||||
})
|
||||
cy.get("[aria-current=date]").click()
|
||||
|
||||
// Department
|
||||
cy.get('[data-name="Department"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Employment Type
|
||||
cy.get('[data-name="Employment type"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Salary
|
||||
cy.get('[data-name="Salary ($)"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type(40000)
|
||||
})
|
||||
|
||||
// Description
|
||||
cy.get('[data-name="Description"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("description")
|
||||
})
|
||||
|
||||
// Responsibilities
|
||||
cy.get('[data-name="Responsibilities"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("Responsibilities")
|
||||
})
|
||||
|
||||
// Requirements
|
||||
cy.get('[data-name="Requirements"]').within(() => {
|
||||
cy.get(".spectrum-Textfield").type("Requirements")
|
||||
})
|
||||
|
||||
// Hiring manager
|
||||
cy.get('[data-name="Hiring manager"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Active
|
||||
if (i == 0) {
|
||||
cy.get('[data-name="Active"]').within(() => {
|
||||
cy.get(".spectrum-Checkbox-box").click({ force: true })
|
||||
})
|
||||
}
|
||||
|
||||
// Location
|
||||
cy.get('[data-name="Location"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click()
|
||||
})
|
||||
|
||||
// Save vacancy
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
cy.wait(1000)
|
||||
|
||||
// Check table was updated
|
||||
cy.get('[data-name="Vacancies Table"]').eq(i).should('contain', 'Tester')
|
||||
}
|
||||
})
|
||||
|
||||
xit("should filter applications by stage", () => {
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
cy.wait(1000)
|
||||
|
||||
// Applications section
|
||||
cy.get(".links").contains("Applications").click({ force: true })
|
||||
cy.wait(1000)
|
||||
|
||||
// Filter by stage - Confirm table updates
|
||||
cy.get(".spectrum-Picker").contains("Filter by stage").click({ force: true })
|
||||
cy.get(".spectrum-Menu").find('li').its('length').then(len => {
|
||||
for (let i = 1; i < len; i++) {
|
||||
cy.get(".spectrum-Menu-item").eq(i).click()
|
||||
const stage = cy.get(".spectrum-Picker-label")
|
||||
stage.invoke('text').then(stageText => {
|
||||
if (stageText == "1st interview") {
|
||||
cy.get(".placeholder").should('contain', 'No rows found')
|
||||
}
|
||||
else {
|
||||
cy.get(".spectrum-Table-row").should('contain', stageText)
|
||||
}
|
||||
cy.get(".spectrum-Picker").contains(stageText).click({ force: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
xit("should edit an application", () => {
|
||||
// Switch application from not hired to hired
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
cy.wait(1000)
|
||||
|
||||
// Not Hired section
|
||||
cy.get(".links").contains("Not hired").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// View application
|
||||
cy.get(".spectrum-Table").within(() => {
|
||||
cy.get(".spectrum-Button").contains("View").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
|
||||
// Update value for 'Staged'
|
||||
cy.get('[data-name="Stage"]').within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-item").contains("Hired").click()
|
||||
})
|
||||
|
||||
// Save application
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// Hired section
|
||||
cy.get(".links").contains("Hired").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// Verify Table size - Total rows = 2
|
||||
cy.get(".spectrum-Table").find(".spectrum-Table-row").its('length').then((len => {
|
||||
expect(len).to.eq(2)
|
||||
}))
|
||||
})
|
||||
|
||||
xit("should delete an application", () => {
|
||||
// Visit published app
|
||||
cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed)
|
||||
cy.wait(1000)
|
||||
|
||||
// Hired section
|
||||
cy.get(".links").contains("Hired").click({ force: true })
|
||||
cy.wait(500)
|
||||
|
||||
// View first application
|
||||
cy.get(".spectrum-Table-row").eq(0).within(() => {
|
||||
cy.get(".spectrum-Button").contains("View").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
|
||||
// Delete application
|
||||
cy.get(".spectrum-Button").contains("Delete").click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".spectrum-Button").contains("Confirm").click()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
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)
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, {
|
||||
onBeforeLoad(win) {
|
||||
cy.stub(win, 'open')
|
||||
}
|
||||
})
|
||||
cy.wait(2000)
|
||||
cy.templateNavigation()
|
||||
})
|
||||
|
||||
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
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
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 })
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -18,14 +18,14 @@ Cypress.Commands.add("login", () => {
|
|||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').first().type("test")
|
||||
cy.get('input[type="password"]').eq(1).type("test")
|
||||
cy.contains("Create super admin user").click()
|
||||
cy.contains("Create super admin user").click({ force: true })
|
||||
}
|
||||
if (url.includes("builder/auth/login") || url.includes("builder/admin")) {
|
||||
// login
|
||||
cy.contains("Sign in to Budibase").then(() => {
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').type("test")
|
||||
cy.get("button").first().click()
|
||||
cy.get("button").first().click({ force: true })
|
||||
cy.wait(1000)
|
||||
})
|
||||
}
|
||||
|
@ -39,6 +39,71 @@ Cypress.Commands.add("closeModal", () => {
|
|||
})
|
||||
})
|
||||
|
||||
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 })
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
||||
const shouldCreateDefaultTable =
|
||||
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
||||
|
@ -57,8 +122,14 @@ 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(".spectrum-ButtonGroup").contains("Create app").click()
|
||||
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 })
|
||||
cy.wait(10000)
|
||||
})
|
||||
if (shouldCreateDefaultTable) {
|
||||
|
@ -75,9 +146,6 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
const findAppName = val.some(val => val.name == name)
|
||||
if (findAppName) {
|
||||
if (val.length > 0) {
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
cy.searchForApplication(name)
|
||||
}
|
||||
const appId = val.reduce((acc, app) => {
|
||||
if (name === app.name) {
|
||||
acc = app.appId
|
||||
|
@ -92,7 +160,7 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
const appIdParsed = appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
cy.get(".spectrum-Icon").eq(0).click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Menu").then($menu => {
|
||||
if ($menu.text().includes("Unpublish")) {
|
||||
|
@ -102,7 +170,7 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
})
|
||||
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
cy.get(".spectrum-Icon").eq(0).click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
|
@ -128,7 +196,7 @@ Cypress.Commands.add("deleteAllApps", () => {
|
|||
const appIdParsed = val[i].appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
cy.get(".spectrum-Icon").eq(0).click({ force: true })
|
||||
})
|
||||
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
|
@ -145,7 +213,7 @@ Cypress.Commands.add("createTestApp", () => {
|
|||
const appName = "Cypress Tests"
|
||||
cy.deleteApp(appName)
|
||||
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||
cy.createScreen("home")
|
||||
//cy.createScreen("home")
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createTestTableWithData", () => {
|
||||
|
@ -246,12 +314,12 @@ Cypress.Commands.add("createUser", email => {
|
|||
|
||||
Cypress.Commands.add("addComponent", (category, component) => {
|
||||
if (category) {
|
||||
cy.get(`[data-cy="category-${category}"]`).click()
|
||||
cy.get(`[data-cy="category-${category}"]`).click({ force: true })
|
||||
}
|
||||
if (component) {
|
||||
cy.get(`[data-cy="component-${component}"]`).click()
|
||||
cy.get(`[data-cy="component-${component}"]`).click({ force: true })
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.wait(2000)
|
||||
cy.location().then(loc => {
|
||||
const params = loc.pathname.split("/")
|
||||
const componentId = params[params.length - 1]
|
||||
|
@ -456,7 +524,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)
|
||||
})
|
||||
|
@ -570,12 +643,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 })
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -596,3 +671,15 @@ Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
|
|||
.should("contain", method)
|
||||
.and("contain", queryPrettyName)
|
||||
})
|
||||
|
||||
Cypress.Commands.add("templateNavigation", () => {
|
||||
// Navigates to templates section
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
// Templates button needs clicked if apps already exist
|
||||
if (val.length > 0) {
|
||||
cy.get(".spectrum-Button").contains("Templates").click({ force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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'
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"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.142",
|
||||
"@budibase/client": "^1.0.142",
|
||||
"@budibase/frontend-core": "^1.0.142",
|
||||
"@budibase/string-templates": "^1.0.142",
|
||||
"@budibase/bbui": "^1.0.167-alpha.8",
|
||||
"@budibase/client": "^1.0.167-alpha.8",
|
||||
"@budibase/frontend-core": "^1.0.167-alpha.8",
|
||||
"@budibase/string-templates": "^1.0.167-alpha.8",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
@ -98,9 +100,13 @@
|
|||
"@testing-library/svelte": "^3.0.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"cypress": "^9.3.1",
|
||||
"cypress-multi-reporters": "^1.6.0",
|
||||
"cypress-terminal-report": "^1.4.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"mochawesome": "^7.1.3",
|
||||
"mochawesome-merge": "^4.2.1",
|
||||
"mochawesome-report-generator": "^6.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.44.0",
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"reporterEnabled": "mochawesome",
|
||||
"mochawesomeReporterOptions": {
|
||||
"reportDir": "cypress/reports",
|
||||
"quiet": true,
|
||||
"overwrite": false,
|
||||
"html": false,
|
||||
"json": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fetch = require("node-fetch")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
|
||||
const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL
|
||||
const OUTCOME = process.env.CYPRESS_OUTCOME
|
||||
const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL
|
||||
const GIT_SHA = process.env.GITHUB_SHA
|
||||
const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL
|
||||
|
||||
async function generateReport() {
|
||||
// read the report file
|
||||
const REPORT_PATH = path.resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"cypress",
|
||||
"reports",
|
||||
"testReport.json"
|
||||
)
|
||||
const report = fs.readFileSync(REPORT_PATH, "utf-8")
|
||||
return JSON.parse(report)
|
||||
}
|
||||
|
||||
async function discordCypressResultsNotification(report) {
|
||||
const {
|
||||
suites,
|
||||
tests,
|
||||
passes,
|
||||
pending,
|
||||
failures,
|
||||
duration,
|
||||
passPercent,
|
||||
skipped,
|
||||
} = report.stats
|
||||
|
||||
const options = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: `**Nightly Tests Status**: ${OUTCOME}`,
|
||||
embeds: [
|
||||
{
|
||||
title: "Budi QA Bot",
|
||||
description: `Nightly Tests`,
|
||||
url: GITHUB_ACTIONS_RUN_URL,
|
||||
color: OUTCOME === "success" ? 3066993 : 15548997,
|
||||
timestamp: new Date(),
|
||||
footer: {
|
||||
icon_url: "http://bbui.budibase.com/budibase-logo.png",
|
||||
text: "Budibase QA Bot",
|
||||
},
|
||||
thumbnail: {
|
||||
url: "http://bbui.budibase.com/budibase-logo.png",
|
||||
},
|
||||
author: {
|
||||
name: "Budibase QA Bot",
|
||||
url: "https://discordapp.com",
|
||||
icon_url: "http://bbui.budibase.com/budibase-logo.png",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Commit",
|
||||
value: `https://github.com/Budibase/budibase/commit/${GIT_SHA}`,
|
||||
},
|
||||
{
|
||||
name: "Cypress Dashboard URL",
|
||||
value: DASHBOARD_URL || "None Supplied",
|
||||
},
|
||||
{
|
||||
name: "Github Actions Run URL",
|
||||
value: GITHUB_ACTIONS_RUN_URL || "None Supplied",
|
||||
},
|
||||
{
|
||||
name: "Test Suites",
|
||||
value: suites,
|
||||
},
|
||||
{
|
||||
name: "Tests",
|
||||
value: tests,
|
||||
},
|
||||
{
|
||||
name: "Passed",
|
||||
value: passes,
|
||||
},
|
||||
{
|
||||
name: "Pending",
|
||||
value: pending,
|
||||
},
|
||||
{
|
||||
name: "Skipped",
|
||||
value: skipped,
|
||||
},
|
||||
{
|
||||
name: "Failures",
|
||||
value: failures,
|
||||
},
|
||||
{
|
||||
name: "Duration",
|
||||
value: `${duration / 1000} Seconds`,
|
||||
},
|
||||
{
|
||||
name: "Pass Percentage",
|
||||
value: Math.floor(passPercent),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
const response = await fetch(WEBHOOK_URL, options)
|
||||
|
||||
if (response.status >= 400) {
|
||||
const text = await response.text()
|
||||
console.error(
|
||||
`Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const report = await generateReport()
|
||||
await discordCypressResultsNotification(report)
|
||||
}
|
||||
|
||||
run()
|
|
@ -53,6 +53,7 @@
|
|||
: { schema: {} }
|
||||
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
|
||||
const onChange = Utils.sequential(async (e, key) => {
|
||||
try {
|
||||
|
@ -330,6 +331,7 @@
|
|||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -12,4 +12,4 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<CopyInput {value} copyValue={fullWebhookURL(value)} />
|
||||
<CopyInput value={fullWebhookURL(value)} />
|
||||
|
|
|
@ -60,7 +60,7 @@ export function getBindings({
|
|||
)
|
||||
|
||||
const label = path == null ? column : `${path}.0.${column}`
|
||||
const binding = path == null ? `[${column}]` : `${path}.0.[${column}]`
|
||||
const binding = path == null ? `[${column}]` : `[${path}].0.[${column}]`
|
||||
// only supply a description for relationship paths
|
||||
const description =
|
||||
path == null
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
export let unpublishApp
|
||||
export let releaseLock
|
||||
export let editIcon
|
||||
export let copyAppId
|
||||
</script>
|
||||
|
||||
<div class="title">
|
||||
|
@ -102,6 +103,9 @@
|
|||
<MenuItem on:click={() => unpublishApp(app)} icon="GlobeRemove">
|
||||
Unpublish
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => copyAppId(app)} icon="Copy">
|
||||
Copy App ID
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{#if !app.deployed}
|
||||
<MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem>
|
||||
|
|
|
@ -10,17 +10,32 @@
|
|||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
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()
|
||||
})
|
||||
|
@ -42,7 +57,7 @@
|
|||
}
|
||||
|
||||
const resolveAppName = (template, name) => {
|
||||
if (template && !name) {
|
||||
if (template && !template.fromFile) {
|
||||
return template.name
|
||||
}
|
||||
return name ? name.trim() : null
|
||||
|
@ -105,6 +120,22 @@
|
|||
// Create user
|
||||
await API.updateOwnMetadata({ roleId: $values.roleId })
|
||||
await auth.setInitInfo({})
|
||||
|
||||
// Create a default home screen if no template was selected
|
||||
if (template == null) {
|
||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||
defaultScreenTemplate.routing.route = "/home"
|
||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||
try {
|
||||
await store.actions.screens.save(defaultScreenTemplate)
|
||||
} catch (err) {
|
||||
console.error("Could not create a default application screen", err)
|
||||
notifications.warning(
|
||||
"Encountered an issue creating the default screen."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||
} catch (error) {
|
||||
creating = false
|
||||
|
@ -142,15 +173,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
|
||||
|
|
|
@ -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>
|
|
@ -1,5 +1,8 @@
|
|||
<script>
|
||||
import { Input, ModalContent, Modal, Body } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let dynamicVariables
|
||||
export let datasource
|
||||
|
@ -35,6 +38,7 @@
|
|||
name = null
|
||||
binding = null
|
||||
dynamicVariables[copiedName] = copiedBinding
|
||||
dispatch("change", dynamicVariables)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -299,6 +299,7 @@
|
|||
{dynamicVariables}
|
||||
bind:binding={varBinding}
|
||||
bind:this={addVariableModal}
|
||||
on:change={saveQuery}
|
||||
/>
|
||||
{#if query && queryConfig}
|
||||
<div class="inner">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -31,7 +31,20 @@
|
|||
$: menu = buildMenu($auth.isAdmin)
|
||||
|
||||
const buildMenu = admin => {
|
||||
let menu = [{ title: "Apps", href: "/builder/portal/apps" }]
|
||||
let menu = [
|
||||
{
|
||||
title: "Apps",
|
||||
href: "/builder/portal/apps",
|
||||
},
|
||||
]
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Usage",
|
||||
href: "/builder/portal/settings/usage",
|
||||
},
|
||||
])
|
||||
}
|
||||
if (admin) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
|
@ -160,7 +173,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="user-dropdown">
|
||||
<ActionMenu align="right">
|
||||
<ActionMenu align="right" dataCy="user-menu">
|
||||
<div slot="control" class="avatar">
|
||||
<Avatar
|
||||
size="M"
|
||||
|
@ -169,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}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
Body,
|
||||
Search,
|
||||
Divider,
|
||||
Helpers,
|
||||
} from "@budibase/bbui"
|
||||
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
|
@ -262,6 +263,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
const copyAppId = async app => {
|
||||
await Helpers.copyToClipboard(app.prodId)
|
||||
notifications.success("App ID copied to clipboard.")
|
||||
}
|
||||
|
||||
function createAppFromTemplateUrl(templateKey) {
|
||||
// validate the template key just to make sure
|
||||
const templateParts = templateKey.split("/")
|
||||
|
@ -395,6 +401,7 @@
|
|||
<div class="appTable">
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<AppRow
|
||||
{copyAppId}
|
||||
{releaseLock}
|
||||
{editIcon}
|
||||
{app}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -298,7 +298,7 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
{#if providers.google}
|
||||
<Divider />
|
||||
<Divider size="S" />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">
|
||||
<div class="provider-title">
|
||||
|
@ -337,7 +337,7 @@
|
|||
</Layout>
|
||||
{/if}
|
||||
{#if providers.oidc}
|
||||
<Divider />
|
||||
<Divider size="S" />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">
|
||||
<div class="provider-title">
|
||||
|
|
|
@ -69,6 +69,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 {
|
||||
|
@ -113,7 +133,7 @@
|
|||
values below and click activate.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<Divider size="S" />
|
||||
{#if smtpConfig}
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">SMTP</Heading>
|
||||
|
@ -157,10 +177,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">
|
||||
|
@ -187,4 +214,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>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
</Body>
|
||||
<Input
|
||||
type="email"
|
||||
label="Username"
|
||||
label="Email"
|
||||
bind:value={$email}
|
||||
error={$touched && $error}
|
||||
/>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
import { Page } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Page>
|
||||
<Page maxWidth="90ch">
|
||||
<slot />
|
||||
</Page>
|
||||
|
|
|
@ -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>
|
|
@ -6,3 +6,4 @@ export { email } from "./email"
|
|||
export { auth } from "./auth"
|
||||
export { oidc } from "./oidc"
|
||||
export { templates } from "./templates"
|
||||
export { licensing } from "./licensing"
|
||||
|
|
|
@ -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()
|
|
@ -2095,7 +2095,7 @@ chalk@^3.0.0:
|
|||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.0:
|
||||
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
|
@ -2181,6 +2181,15 @@ cliui@^6.0.0:
|
|||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
@ -2368,6 +2377,14 @@ cssstyle@^2.3.0:
|
|||
dependencies:
|
||||
cssom "~0.3.6"
|
||||
|
||||
cypress-multi-reporters@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.6.0.tgz#2c6833b92e3df412c233657c55009e2d5e1cc7c1"
|
||||
integrity sha512-JN9yMzDmPwwirzi95N2FC8VJZ0qp+uUJ1ixYHpJFaAtGgIx15LjVmASqQaxnDh8q57jIIJ6C0o7imiLU6N1YNQ==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
lodash "^4.17.15"
|
||||
|
||||
cypress-terminal-report@^1.4.1:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/cypress-terminal-report/-/cypress-terminal-report-1.4.2.tgz#4eeaf2c6a063b42271ec686aff4ab0d6f7b252a6"
|
||||
|
@ -2440,6 +2457,11 @@ data-urls@^2.0.0:
|
|||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
dateformat@^4.5.1:
|
||||
version "4.6.3"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
|
||||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
dayjs@^1.10.4:
|
||||
version "1.10.7"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
||||
|
@ -2545,6 +2567,11 @@ diff@^4.0.1:
|
|||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
diff@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
||||
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||
|
@ -2736,6 +2763,11 @@ escalade@^3.1.1:
|
|||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
escape-html@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
|
@ -3075,6 +3107,24 @@ from@~0:
|
|||
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
|
||||
integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
|
||||
|
||||
fs-extra@^10.0.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
|
||||
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-extra@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
|
||||
integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
|
||||
|
@ -3104,6 +3154,11 @@ fsevents@^2.1.2, fsevents@~2.3.2:
|
|||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
fsu@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834"
|
||||
integrity sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A==
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
|
@ -3114,7 +3169,7 @@ gensync@^1.0.0-beta.2:
|
|||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
get-caller-file@^2.0.1:
|
||||
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
@ -3190,6 +3245,18 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
|
|||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.1.6:
|
||||
version "7.2.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.2.tgz#29deb38e1ef90f132d5958abe9c3ee8e87f3c318"
|
||||
integrity sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.1.1"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
global-dirs@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
|
||||
|
@ -3216,6 +3283,11 @@ globby@10.0.1:
|
|||
merge2 "^1.2.3"
|
||||
slash "^3.0.0"
|
||||
|
||||
graceful-fs@^4.1.2:
|
||||
version "4.2.10"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
|
||||
|
||||
graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||
|
@ -4065,7 +4137,7 @@ joi@^17.4.0:
|
|||
"@sideway/formula" "^3.0.0"
|
||||
"@sideway/pinpoint" "^2.0.0"
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
@ -4136,7 +4208,7 @@ json-schema@0.4.0:
|
|||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||
|
@ -4262,6 +4334,26 @@ lodash.debounce@^4.0.8:
|
|||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
||||
|
||||
lodash.isempty@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
|
||||
integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=
|
||||
|
||||
lodash.isfunction@^3.0.9:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
|
||||
integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
|
||||
|
||||
lodash.isobject@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
|
||||
integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=
|
||||
|
||||
lodash.isstring@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
|
||||
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
|
||||
|
||||
lodash.once@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
|
@ -4297,6 +4389,13 @@ log-update@^4.0.0:
|
|||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
|
@ -4423,6 +4522,13 @@ minimatch@^3.0.4:
|
|||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^3.1.1:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
|
@ -4436,6 +4542,49 @@ mixin-deep@^1.2.0:
|
|||
for-in "^1.0.2"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
mochawesome-merge@^4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/mochawesome-merge/-/mochawesome-merge-4.2.1.tgz#484e5c500dd5d88b33adb440e6dfef80d9109a18"
|
||||
integrity sha512-G7+LqIKgixShKG4FyWDn1PIrzpKEwCofrJip/VzdqghNGqZl4S5MNoXx5YDfk9KLe+pr4qGa1TOzCc/oVw/8Kw==
|
||||
dependencies:
|
||||
fs-extra "^7.0.1"
|
||||
glob "^7.1.6"
|
||||
yargs "^15.3.1"
|
||||
|
||||
mochawesome-report-generator@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mochawesome-report-generator/-/mochawesome-report-generator-6.2.0.tgz#65a30a11235ba7a68e1cf0ca1df80d764b93ae78"
|
||||
integrity sha512-Ghw8JhQFizF0Vjbtp9B0i//+BOkV5OWcQCPpbO0NGOoxV33o+gKDYU0Pr2pGxkIHnqZ+g5mYiXF7GMNgAcDpSg==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
dateformat "^4.5.1"
|
||||
escape-html "^1.0.3"
|
||||
fs-extra "^10.0.0"
|
||||
fsu "^1.1.1"
|
||||
lodash.isfunction "^3.0.9"
|
||||
opener "^1.5.2"
|
||||
prop-types "^15.7.2"
|
||||
tcomb "^3.2.17"
|
||||
tcomb-validation "^3.3.0"
|
||||
validator "^13.6.0"
|
||||
yargs "^17.2.1"
|
||||
|
||||
mochawesome@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/mochawesome/-/mochawesome-7.1.3.tgz#07b358138f37f5b07b51a1b255d84babfa36fa83"
|
||||
integrity sha512-Vkb3jR5GZ1cXohMQQ73H3cZz7RoxGjjUo0G5hu0jLaW+0FdUxUwg3Cj29bqQdh0rFcnyV06pWmqmi5eBPnEuNQ==
|
||||
dependencies:
|
||||
chalk "^4.1.2"
|
||||
diff "^5.0.0"
|
||||
json-stringify-safe "^5.0.1"
|
||||
lodash.isempty "^4.4.0"
|
||||
lodash.isfunction "^3.0.9"
|
||||
lodash.isobject "^3.0.2"
|
||||
lodash.isstring "^4.0.1"
|
||||
mochawesome-report-generator "^6.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
uuid "^8.3.2"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
@ -4573,6 +4722,11 @@ nwsapi@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
|
||||
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
|
||||
|
@ -4625,6 +4779,11 @@ onetime@^5.1.0, onetime@^5.1.2:
|
|||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
opener@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||
|
||||
optionator@^0.8.1:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||
|
@ -4831,6 +4990,15 @@ prompts@^2.0.1:
|
|||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.7.2:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
property-expr@^2.0.2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
|
||||
|
@ -4876,6 +5044,11 @@ queue-microtask@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
|
@ -5452,7 +5625,7 @@ string-length@^4.0.1:
|
|||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
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==
|
||||
|
@ -5559,6 +5732,18 @@ synchronous-promise@^2.0.13:
|
|||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e"
|
||||
integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==
|
||||
|
||||
tcomb-validation@^3.3.0:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/tcomb-validation/-/tcomb-validation-3.4.1.tgz#a7696ec176ce56a081d9e019f8b732a5a8894b65"
|
||||
integrity sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA==
|
||||
dependencies:
|
||||
tcomb "^3.0.0"
|
||||
|
||||
tcomb@^3.0.0, tcomb@^3.2.17:
|
||||
version "3.2.29"
|
||||
resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-3.2.29.tgz#32404fe9456d90c2cf4798682d37439f1ccc386c"
|
||||
integrity sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==
|
||||
|
||||
terminal-link@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
|
||||
|
@ -5858,6 +6043,11 @@ validate-npm-package-license@^3.0.1:
|
|||
spdx-correct "^3.0.0"
|
||||
spdx-expression-parse "^3.0.0"
|
||||
|
||||
validator@^13.6.0:
|
||||
version "13.7.0"
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
|
||||
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
|
||||
|
||||
verror@1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
||||
|
@ -6032,6 +6222,11 @@ y18n@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
|
@ -6045,7 +6240,12 @@ yargs-parser@^18.1.2:
|
|||
camelcase "^5.0.0"
|
||||
decamelize "^1.2.0"
|
||||
|
||||
yargs@^15.4.1:
|
||||
yargs-parser@^21.0.0:
|
||||
version "21.0.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35"
|
||||
integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==
|
||||
|
||||
yargs@^15.3.1, yargs@^15.4.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
|
||||
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||
|
@ -6062,6 +6262,19 @@ yargs@^15.4.1:
|
|||
y18n "^4.0.0"
|
||||
yargs-parser "^18.1.2"
|
||||
|
||||
yargs@^17.2.1:
|
||||
version "17.5.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.0.tgz#2706c5431f8c119002a2b106fc9f58b9bb9097a3"
|
||||
integrity sha512-3sLxVhbAB5OC8qvVRebCLWuouhwh/rswsiDYx3WGxajUk/l4G20SKfrKKFeNIHboUFt2JFgv2yfn+5cgOr/t5A==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.0.0"
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -2338,7 +2338,11 @@
|
|||
"type": "boolean",
|
||||
"label": "Autocomplete",
|
||||
"key": "autocomplete",
|
||||
"defaultValue": false
|
||||
"defaultValue": false,
|
||||
"dependsOn": {
|
||||
"setting": "optionsType",
|
||||
"value": "select"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
|
@ -2346,6 +2350,43 @@
|
|||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Type",
|
||||
"key": "optionsType",
|
||||
"defaultValue": "select",
|
||||
"placeholder": "Pick an options type",
|
||||
"options": [
|
||||
{
|
||||
"label": "Select",
|
||||
"value": "select"
|
||||
},
|
||||
{
|
||||
"label": "Checkboxes",
|
||||
"value": "checkbox"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Direction",
|
||||
"key": "direction",
|
||||
"defaultValue": "vertical",
|
||||
"options": [
|
||||
{
|
||||
"label": "Horizontal",
|
||||
"value": "horizontal"
|
||||
},
|
||||
{
|
||||
"label": "Vertical",
|
||||
"value": "vertical"
|
||||
}
|
||||
],
|
||||
"dependsOn": {
|
||||
"setting": "optionsType",
|
||||
"value": "checkbox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Options source",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.142",
|
||||
"@budibase/frontend-core": "^1.0.142",
|
||||
"@budibase/string-templates": "^1.0.142",
|
||||
"@budibase/bbui": "^1.0.167-alpha.8",
|
||||
"@budibase/frontend-core": "^1.0.167-alpha.8",
|
||||
"@budibase/string-templates": "^1.0.167-alpha.8",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
|
|
@ -283,7 +283,8 @@
|
|||
@media print {
|
||||
#spectrum-root,
|
||||
#clip-root,
|
||||
#app-root {
|
||||
#app-root,
|
||||
#app-body {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
if (
|
||||
formContext &&
|
||||
$builderStore.inBuilder &&
|
||||
$componentStore?.selectedComponentPath?.includes($component.id)
|
||||
$componentStore.selectedComponentPath?.includes($component.id)
|
||||
) {
|
||||
formContext.formApi.setStep(step)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { CoreMultiselect } from "@budibase/bbui"
|
||||
import { CoreMultiselect, CoreCheckboxGroup } from "@budibase/bbui"
|
||||
import Field from "./Field.svelte"
|
||||
import { getOptions } from "./optionsParser"
|
||||
export let field
|
||||
|
@ -15,6 +15,8 @@
|
|||
export let customOptions
|
||||
export let autocomplete = false
|
||||
export let onChange
|
||||
export let optionsType = "select"
|
||||
export let direction = "vertical"
|
||||
|
||||
let fieldState
|
||||
let fieldApi
|
||||
|
@ -61,17 +63,31 @@
|
|||
bind:fieldSchema
|
||||
>
|
||||
{#if fieldState}
|
||||
<CoreMultiselect
|
||||
value={fieldState.value || []}
|
||||
error={fieldState.error}
|
||||
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||
id={fieldState.fieldId}
|
||||
disabled={fieldState.disabled}
|
||||
on:change={handleChange}
|
||||
{placeholder}
|
||||
{options}
|
||||
{autocomplete}
|
||||
/>
|
||||
{#if !optionsType || optionsType === "select"}
|
||||
<CoreMultiselect
|
||||
value={fieldState.value || []}
|
||||
error={fieldState.error}
|
||||
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||
id={fieldState.fieldId}
|
||||
disabled={fieldState.disabled}
|
||||
on:change={handleChange}
|
||||
{placeholder}
|
||||
{options}
|
||||
{autocomplete}
|
||||
/>
|
||||
{:else if optionsType === "checkbox"}
|
||||
<CoreCheckboxGroup
|
||||
value={fieldState.value || []}
|
||||
id={fieldState.fieldId}
|
||||
disabled={fieldState.disabled}
|
||||
error={fieldState.error}
|
||||
{options}
|
||||
{direction}
|
||||
on:change={handleChange}
|
||||
getOptionLabel={flatOptions ? x => x : x => x.label}
|
||||
getOptionValue={flatOptions ? x => x : x => x.value}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</Field>
|
||||
|
|
|
@ -40,6 +40,15 @@ export const getOptions = (
|
|||
|
||||
// Extract custom options
|
||||
if (optionsSource === "custom" && customOptions) {
|
||||
customOptions.forEach(option => {
|
||||
if (typeof option.value === "string") {
|
||||
if (option.value.toLowerCase() === "true") {
|
||||
option.value = true
|
||||
} else if (option.value.toLowerCase() === "false") {
|
||||
option.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
return customOptions
|
||||
}
|
||||
|
||||
|
|
|
@ -66,4 +66,9 @@
|
|||
.tab-content {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
@media print {
|
||||
.devtools {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -71,4 +71,9 @@
|
|||
.dev-preview-header :global(.spectrum-Picker-label) {
|
||||
color: white !important;
|
||||
}
|
||||
@media print {
|
||||
.dev-preview-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -155,7 +155,7 @@
|
|||
icon="Duplicate"
|
||||
on:click={() => {
|
||||
builderStore.actions.duplicateComponent(
|
||||
$builderStore.selectedComponent._id
|
||||
$builderStore.selectedComponentId
|
||||
)
|
||||
}}
|
||||
title="Duplicate component"
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
builderStore,
|
||||
uploadStore,
|
||||
rowSelectionStore,
|
||||
componentStore,
|
||||
} from "stores"
|
||||
import { styleable } from "utils/styleable"
|
||||
import { linkable } from "utils/linkable"
|
||||
|
@ -24,6 +25,7 @@ export default {
|
|||
screenStore,
|
||||
builderStore,
|
||||
uploadStore,
|
||||
componentStore,
|
||||
styleable,
|
||||
linkable,
|
||||
getAction,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"description": "Budibase frontend core libraries used in builder and client",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.142",
|
||||
"@budibase/bbui": "^1.0.167-alpha.8",
|
||||
"lodash": "^4.17.21",
|
||||
"svelte": "^3.46.2"
|
||||
}
|
||||
|
|
|
@ -20,6 +20,17 @@ export const buildConfigEndpoints = API => ({
|
|||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a global config
|
||||
* @param id the id of the config to delete
|
||||
* @param rev the revision of the config to delete
|
||||
*/
|
||||
deleteConfig: async ({ id, rev }) => {
|
||||
return await API.delete({
|
||||
url: `/api/global/configs/${id}/${rev}`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the config for a certain tenant.
|
||||
* @param tenantId the tenant ID to get the config for
|
||||
|
|
|
@ -27,4 +27,13 @@ export const buildLicensingEndpoints = API => ({
|
|||
url: "/api/global/license/refresh",
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the usage information for the tenant
|
||||
*/
|
||||
getQuotaUsage: async () => {
|
||||
return API.get({
|
||||
url: "/api/global/license/usage",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -29,7 +29,7 @@ export const buildTemplateEndpoints = API => ({
|
|||
*/
|
||||
getAppTemplates: async () => {
|
||||
return await API.get({
|
||||
url: "/api/templates?type=app",
|
||||
url: "/api/templates",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
module FirebaseMock {
|
||||
const firebase: any = {}
|
||||
|
||||
firebase.Firestore = function () {
|
||||
this.get = jest.fn(() => [
|
||||
{
|
||||
data: jest.fn(() => ({ result: "test" })),
|
||||
},
|
||||
])
|
||||
|
||||
this.update = jest.fn()
|
||||
this.set = jest.fn()
|
||||
this.delete = jest.fn()
|
||||
|
||||
this.doc = jest.fn(() => ({
|
||||
update: this.update,
|
||||
set: this.set,
|
||||
delete: this.delete,
|
||||
get: jest.fn(() => ({
|
||||
data: jest.fn(() => ({ result: "test" })),
|
||||
})),
|
||||
id: "test_id",
|
||||
}))
|
||||
|
||||
this.where = jest.fn(() => ({
|
||||
get: this.get,
|
||||
}))
|
||||
|
||||
this.collection = jest.fn(() => ({
|
||||
doc: this.doc,
|
||||
where: this.where,
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = firebase
|
||||
}
|
|
@ -14,21 +14,13 @@ module PgMock {
|
|||
function Client() {}
|
||||
|
||||
Client.prototype.query = query
|
||||
Client.prototype.end = jest.fn()
|
||||
Client.prototype.connect = jest.fn()
|
||||
Client.prototype.release = jest.fn()
|
||||
|
||||
function Pool() {}
|
||||
|
||||
const on = jest.fn()
|
||||
Pool.prototype.query = query
|
||||
Pool.prototype.connect = jest.fn(() => {
|
||||
// @ts-ignore
|
||||
return new Client()
|
||||
})
|
||||
Pool.prototype.on = on
|
||||
|
||||
pg.Client = Client
|
||||
pg.Pool = Pool
|
||||
pg.queryMock = query
|
||||
pg.on = on
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.142",
|
||||
"version": "1.0.167-alpha.8",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -10,6 +10,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist/ && tsc -p tsconfig.build.json && mv dist/src/* dist/ && rimraf dist/src/ && yarn postbuild",
|
||||
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
|
||||
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
|
||||
"test": "jest --coverage --maxWorkers=2",
|
||||
"test:watch": "jest --watch",
|
||||
|
@ -68,10 +69,10 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/backend-core": "^1.0.142",
|
||||
"@budibase/client": "^1.0.142",
|
||||
"@budibase/pro": "1.0.142",
|
||||
"@budibase/string-templates": "^1.0.142",
|
||||
"@budibase/backend-core": "^1.0.167-alpha.8",
|
||||
"@budibase/client": "^1.0.167-alpha.8",
|
||||
"@budibase/pro": "1.0.167-alpha.8",
|
||||
"@budibase/string-templates": "^1.0.167-alpha.8",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
@ -145,12 +146,14 @@
|
|||
"@budibase/standard-components": "^0.9.139",
|
||||
"@jest/test-sequencer": "^24.8.0",
|
||||
"@types/apidoc": "^0.50.0",
|
||||
"@types/bson": "^4.2.0",
|
||||
"@types/bull": "^3.15.1",
|
||||
"@types/google-spreadsheet": "^3.1.5",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/koa": "^2.13.3",
|
||||
"@types/koa-router": "^7.4.2",
|
||||
"@types/lodash": "4.14.180",
|
||||
"@types/mongodb": "3.6.3",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/oracledb": "^5.2.1",
|
||||
"@types/redis": "^4.0.11",
|
||||
|
|
|
@ -9,7 +9,6 @@ const SERVER_PORT = "4100"
|
|||
const WORKER_PORT = "4200"
|
||||
|
||||
// @ts-ignore
|
||||
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
||||
process.env.NODE_ENV = "cypress"
|
||||
process.env.ENABLE_ANALYTICS = "false"
|
||||
process.env.JWT_SECRET = "budibase"
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
const { cloneDeep } = require("lodash")
|
||||
const { definitions } = require("../../integrations")
|
||||
const { getTenantId } = require("@budibase/backend-core/tenancy")
|
||||
const { SourceNames } = require("../../definitions/datasource")
|
||||
const googlesheets = require("../../integrations/googlesheets")
|
||||
const env = require("../../environment")
|
||||
const { featureFlags } = require("@budibase/backend-core")
|
||||
|
||||
exports.fetch = async function (ctx) {
|
||||
ctx.status = 200
|
||||
const defs = cloneDeep(definitions)
|
||||
|
||||
// for google sheets integration google verification
|
||||
if (env.EXCLUDE_QUOTAS_TENANTS) {
|
||||
const excludedTenants = env.EXCLUDE_QUOTAS_TENANTS.split(",")
|
||||
const tenantId = getTenantId()
|
||||
if (excludedTenants.includes(tenantId)) {
|
||||
defs[SourceNames.GOOGLE_SHEETS] = googlesheets.schema
|
||||
}
|
||||
if (featureFlags.isEnabled(featureFlags.FeatureFlag.GOOGLE_SHEETS)) {
|
||||
defs[SourceNames.GOOGLE_SHEETS] = googlesheets.schema
|
||||
}
|
||||
|
||||
ctx.body = defs
|
||||
|
|
|
@ -175,9 +175,10 @@ module External {
|
|||
const thisRow: Row = {}
|
||||
// filter the row down to what is actually the row (not joined)
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
const value = row[`${table.name}.${fieldName}`] || row[fieldName]
|
||||
const pathValue = row[`${table.name}.${fieldName}`]
|
||||
const value = pathValue != null ? pathValue : row[fieldName]
|
||||
// all responses include "select col as table.col" so that overlaps are handled
|
||||
if (value) {
|
||||
if (value != null) {
|
||||
thisRow[fieldName] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,9 @@ export async function patch(ctx: any): Promise<any> {
|
|||
return save(ctx)
|
||||
}
|
||||
try {
|
||||
const { row, table } = await pickApi(tableId).patch(ctx)
|
||||
const { row, table } = await quotas.addQuery(() =>
|
||||
pickApi(tableId).patch(ctx)
|
||||
)
|
||||
ctx.status = 200
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
|
||||
|
@ -42,7 +44,7 @@ export async function patch(ctx: any): Promise<any> {
|
|||
}
|
||||
}
|
||||
|
||||
const saveRow = async (ctx: any) => {
|
||||
export const save = async (ctx: any) => {
|
||||
const appId = ctx.appId
|
||||
const tableId = getTableId(ctx)
|
||||
const body = ctx.request.body
|
||||
|
@ -51,7 +53,9 @@ const saveRow = async (ctx: any) => {
|
|||
return patch(ctx)
|
||||
}
|
||||
try {
|
||||
const { row, table } = await pickApi(tableId).save(ctx)
|
||||
const { row, table } = await quotas.addRow(() =>
|
||||
quotas.addQuery(() => pickApi(tableId).save(ctx))
|
||||
)
|
||||
ctx.status = 200
|
||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
||||
ctx.message = `${table.name} saved successfully`
|
||||
|
@ -61,14 +65,10 @@ const saveRow = async (ctx: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
export async function save(ctx: any) {
|
||||
await quotas.addRow(() => saveRow(ctx))
|
||||
}
|
||||
|
||||
export async function fetchView(ctx: any) {
|
||||
const tableId = getTableId(ctx)
|
||||
try {
|
||||
ctx.body = await pickApi(tableId).fetchView(ctx)
|
||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetchView(ctx))
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export async function fetchView(ctx: any) {
|
|||
export async function fetch(ctx: any) {
|
||||
const tableId = getTableId(ctx)
|
||||
try {
|
||||
ctx.body = await pickApi(tableId).fetch(ctx)
|
||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).fetch(ctx))
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ export async function fetch(ctx: any) {
|
|||
export async function find(ctx: any) {
|
||||
const tableId = getTableId(ctx)
|
||||
try {
|
||||
ctx.body = await pickApi(tableId).find(ctx)
|
||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx))
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
|
@ -98,14 +98,16 @@ export async function destroy(ctx: any) {
|
|||
const tableId = getTableId(ctx)
|
||||
let response, row
|
||||
if (inputs.rows) {
|
||||
let { rows } = await pickApi(tableId).bulkDestroy(ctx)
|
||||
let { rows } = await quotas.addQuery(() =>
|
||||
pickApi(tableId).bulkDestroy(ctx)
|
||||
)
|
||||
await quotas.removeRows(rows.length)
|
||||
response = rows
|
||||
for (let row of rows) {
|
||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
|
||||
}
|
||||
} else {
|
||||
let resp = await pickApi(tableId).destroy(ctx)
|
||||
let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx))
|
||||
await quotas.removeRow()
|
||||
response = resp.response
|
||||
row = resp.row
|
||||
|
@ -121,7 +123,7 @@ export async function search(ctx: any) {
|
|||
const tableId = getTableId(ctx)
|
||||
try {
|
||||
ctx.status = 200
|
||||
ctx.body = await pickApi(tableId).search(ctx)
|
||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).search(ctx))
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
|
@ -139,7 +141,9 @@ export async function validate(ctx: any) {
|
|||
export async function fetchEnrichedRow(ctx: any) {
|
||||
const tableId = getTableId(ctx)
|
||||
try {
|
||||
ctx.body = await pickApi(tableId).fetchEnrichedRow(ctx)
|
||||
ctx.body = await quotas.addQuery(() =>
|
||||
pickApi(tableId).fetchEnrichedRow(ctx)
|
||||
)
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
|
@ -148,7 +152,7 @@ export async function fetchEnrichedRow(ctx: any) {
|
|||
export const exportRows = async (ctx: any) => {
|
||||
const tableId = getTableId(ctx)
|
||||
try {
|
||||
ctx.body = await pickApi(tableId).exportRows(ctx)
|
||||
ctx.body = await quotas.addQuery(() => pickApi(tableId).exportRows(ctx))
|
||||
} catch (err) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
const fetch = require("node-fetch")
|
||||
const { downloadTemplate } = require("../../utilities/fileSystem")
|
||||
const env = require("../../environment")
|
||||
|
||||
// development flag, can be used to test against templates exported locally
|
||||
const DEFAULT_TEMPLATES_BUCKET =
|
||||
"prod-budi-templates.s3-eu-west-1.amazonaws.com"
|
||||
|
||||
exports.fetch = async function (ctx) {
|
||||
const { type = "app" } = ctx.query
|
||||
let type = env.TEMPLATE_REPOSITORY
|
||||
let response,
|
||||
error = false
|
||||
try {
|
||||
|
|
|
@ -6,7 +6,7 @@ exports.csv = function (headers, rows) {
|
|||
.map(header => {
|
||||
let val = row[header]
|
||||
val =
|
||||
typeof val === "object"
|
||||
typeof val === "object" && !(val instanceof Date)
|
||||
? `"${JSON.stringify(val).replace(/"/g, "'")}"`
|
||||
: `"${val}"`
|
||||
return val.trim()
|
||||
|
|
|
@ -1,9 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`viewBuilder Calculate and filter creates a view with the calculation statistics and filter schema 1`] = `
|
||||
Object {
|
||||
"map": "function (doc) {
|
||||
if ((doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && !(
|
||||
doc[\\"myField\\"] === undefined ||
|
||||
doc[\\"myField\\"] === null ||
|
||||
doc[\\"myField\\"] === \\"\\" ||
|
||||
(Array.isArray(doc[\\"myField\\"]) && doc[\\"myField\\"].length === 0)
|
||||
)) && (doc[\\"age\\"] > 17)) {
|
||||
emit(doc[\\"_id\\"], doc[\\"myField\\"]);
|
||||
}
|
||||
}",
|
||||
"meta": Object {
|
||||
"calculation": "stats",
|
||||
"field": "myField",
|
||||
"filters": Array [
|
||||
Object {
|
||||
"condition": "MT",
|
||||
"key": "age",
|
||||
"value": 17,
|
||||
},
|
||||
],
|
||||
"groupBy": undefined,
|
||||
"schema": Object {
|
||||
"avg": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"count": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"field": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"max": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"min": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"sum": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"sumsqr": Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
},
|
||||
"reduce": "_stats",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = `
|
||||
Object {
|
||||
"map": "function (doc) {
|
||||
if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
|
||||
if ((doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && !(
|
||||
doc[\\"myField\\"] === undefined ||
|
||||
doc[\\"myField\\"] === null ||
|
||||
doc[\\"myField\\"] === \\"\\" ||
|
||||
(Array.isArray(doc[\\"myField\\"]) && doc[\\"myField\\"].length === 0)
|
||||
)) ) {
|
||||
emit(doc[\\"_id\\"], doc[\\"myField\\"]);
|
||||
}
|
||||
}",
|
||||
|
|
|
@ -44,4 +44,22 @@ describe("viewBuilder", () => {
|
|||
})).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Calculate and filter", () => {
|
||||
it("creates a view with the calculation statistics and filter schema", () => {
|
||||
expect(viewTemplate({
|
||||
"name": "Calculate View",
|
||||
"field": "myField",
|
||||
"calculation": "stats",
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"filters": [
|
||||
{
|
||||
"value": 17,
|
||||
"condition": "MT",
|
||||
"key": "age",
|
||||
}
|
||||
]
|
||||
})).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue