Merge branch 'develop' of github.com:Budibase/budibase into new-design-ui

This commit is contained in:
Andrew Kingston 2022-07-11 10:33:50 +01:00
commit 16116f2877
142 changed files with 5014 additions and 5056 deletions

14
.vscode/settings.json vendored
View File

@ -3,5 +3,17 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": true
}, },
"editor.defaultFormatter": "svelte.svelte-vscode" "editor.defaultFormatter": "svelte.svelte-vscode",
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"debug.javascript.terminalOptions": {
"skipFiles": [
"${workspaceFolder}/packages/backend-core/node_modules/**",
"<node_internals>/**"
]
},
} }

View File

@ -11,8 +11,8 @@ sources:
- https://github.com/Budibase/budibase - https://github.com/Budibase/budibase
- https://budibase.com - https://budibase.com
type: application type: application
version: 0.2.10 version: 0.2.11
appVersion: 1.0.48 appVersion: 1.0.214
dependencies: dependencies:
- name: couchdb - name: couchdb
version: 3.6.1 version: 3.6.1

View File

@ -122,6 +122,10 @@ spec:
value: {{ .Values.globals.automationMaxIterations | quote }} value: {{ .Values.globals.automationMaxIterations | quote }}
- name: TENANT_FEATURE_FLAGS - name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }} value: {{ .Values.globals.tenantFeatureFlags | quote }}
- name: BB_ADMIN_USER_EMAIL
value: { { .Values.globals.bbAdminUserEmail | quote } }
- name: BB_ADMIN_USER_PASSWORD
value: { { .Values.globals.bbAdminUserPassword | quote } }
image: budibase/apps:{{ .Values.globals.appVersion }} image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always imagePullPolicy: Always

View File

@ -18,4 +18,8 @@ MINIO_PORT=4004
COUCH_DB_PORT=4005 COUCH_DB_PORT=4005
REDIS_PORT=6379 REDIS_PORT=6379
WATCHTOWER_PORT=6161 WATCHTOWER_PORT=6161
BUDIBASE_ENVIRONMENT=PRODUCTION BUDIBASE_ENVIRONMENT=PRODUCTION
# An admin user can be automatically created initially if these are set
BB_ADMIN_USER_EMAIL=
BB_ADMIN_USER_PASSWORD=

View File

@ -23,6 +23,8 @@ services:
ENABLE_ANALYTICS: "true" ENABLE_ANALYTICS: "true"
REDIS_URL: redis-service:6379 REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD} REDIS_PASSWORD: ${REDIS_PASSWORD}
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
depends_on: depends_on:
- worker-service - worker-service
- redis-service - redis-service

View File

@ -19,3 +19,7 @@ COUCH_DB_PORT=4005
REDIS_PORT=6379 REDIS_PORT=6379
WATCHTOWER_PORT=6161 WATCHTOWER_PORT=6161
BUDIBASE_ENVIRONMENT=PRODUCTION BUDIBASE_ENVIRONMENT=PRODUCTION
# An admin user can be automatically created initially if these are set
BB_ADMIN_USER_EMAIL=
BB_ADMIN_USER_PASSWORD=

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.212-alpha.8", "version": "1.0.219",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"
@ -15,4 +15,4 @@
] ]
} }
} }
} }

View File

@ -40,7 +40,8 @@
"dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1", "dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server", "dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
"test": "lerna run test", "test": "lerna run test && yarn test:pro",
"test:pro": "bash scripts/pro/test.sh",
"lint:eslint": "eslint packages", "lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier", "lint": "yarn run lint:eslint && yarn run lint:prettier",

View File

@ -5,4 +5,5 @@ module.exports = {
app: require("./src/cache/appMetadata"), app: require("./src/cache/appMetadata"),
writethrough: require("./src/cache/writethrough"), writethrough: require("./src/cache/writethrough"),
...generic, ...generic,
cache: generic,
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.212-alpha.8", "version": "1.0.219",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/types": "^1.0.212-alpha.8", "@budibase/types": "^1.0.219-alpha.17",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
@ -36,6 +36,7 @@
"passport-google-oauth": "2.0.0", "passport-google-oauth": "2.0.0",
"passport-jwt": "4.0.0", "passport-jwt": "4.0.0",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"passport-oauth2-refresh": "^2.1.0",
"posthog-node": "1.3.0", "posthog-node": "1.3.0",
"pouchdb": "7.3.0", "pouchdb": "7.3.0",
"pouchdb-find": "7.2.2", "pouchdb-find": "7.2.2",
@ -58,6 +59,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@budibase/types": "^1.0.219",
"@shopify/jest-koa-mocks": "3.1.5", "@shopify/jest-koa-mocks": "3.1.5",
"@types/jest": "27.5.1", "@types/jest": "27.5.1",
"@types/koa": "2.0.52", "@types/koa": "2.0.52",
@ -78,4 +80,4 @@
"typescript": "4.7.3" "typescript": "4.7.3"
}, },
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
} }

View File

@ -2,6 +2,9 @@ const passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
const JwtStrategy = require("passport-jwt").Strategy const JwtStrategy = require("passport-jwt").Strategy
const { getGlobalDB } = require("./tenancy") const { getGlobalDB } = require("./tenancy")
const refresh = require("passport-oauth2-refresh")
const { Configs } = require("./constants")
const { getScopedConfig } = require("./db/utils")
const { const {
jwt, jwt,
local, local,
@ -12,10 +15,13 @@ const {
tenancy, tenancy,
appTenancy, appTenancy,
authError, authError,
ssoCallbackUrl,
csrf, csrf,
internalApi, internalApi,
} = require("./middleware") } = require("./middleware")
const { invalidateUser } = require("./cache/user")
// Strategies // Strategies
passport.use(new LocalStrategy(local.options, local.authenticate)) passport.use(new LocalStrategy(local.options, local.authenticate))
passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) passport.use(new JwtStrategy(jwt.options, jwt.authenticate))
@ -34,6 +40,124 @@ passport.deserializeUser(async (user, done) => {
} }
}) })
async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) {
const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig)
let enrichedConfig
let strategy
try {
enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl)
if (!enrichedConfig) {
throw new Error("OIDC Config contents invalid")
}
strategy = await oidc.strategyFactory(enrichedConfig)
} catch (err) {
console.error(err)
throw new Error("Could not refresh OAuth Token")
}
refresh.use(strategy, {
setRefreshOAuth2() {
return strategy._getOAuth2Client(enrichedConfig)
},
})
return new Promise(resolve => {
refresh.requestNewAccessToken(
Configs.OIDC,
refreshToken,
(err, accessToken, refreshToken, params) => {
resolve({ err, accessToken, refreshToken, params })
}
)
})
}
async function refreshGoogleAccessToken(db, config, refreshToken) {
let callbackUrl = await google.getCallbackUrl(db, config)
let strategy
try {
strategy = await google.strategyFactory(config, callbackUrl)
} catch (err) {
console.error(err)
throw new Error("Error constructing OIDC refresh strategy", err)
}
refresh.use(strategy)
return new Promise(resolve => {
refresh.requestNewAccessToken(
Configs.GOOGLE,
refreshToken,
(err, accessToken, refreshToken, params) => {
resolve({ err, accessToken, refreshToken, params })
}
)
})
}
async function refreshOAuthToken(refreshToken, configType, configId) {
const db = getGlobalDB()
const config = await getScopedConfig(db, {
type: configType,
group: {},
})
let chosenConfig = {}
let refreshResponse
if (configType === Configs.OIDC) {
// configId - retrieved from cookie.
chosenConfig = config.configs.filter(c => c.uuid === configId)[0]
if (!chosenConfig) {
throw new Error("Invalid OIDC configuration")
}
refreshResponse = await refreshOIDCAccessToken(
db,
chosenConfig,
refreshToken
)
} else {
chosenConfig = config
refreshResponse = await refreshGoogleAccessToken(
db,
chosenConfig,
refreshToken
)
}
return refreshResponse
}
async function updateUserOAuth(userId, oAuthConfig) {
const details = {
accessToken: oAuthConfig.accessToken,
refreshToken: oAuthConfig.refreshToken,
}
try {
const db = getGlobalDB()
const dbUser = await db.get(userId)
//Do not overwrite the refresh token if a valid one is not provided.
if (typeof details.refreshToken !== "string") {
delete details.refreshToken
}
dbUser.oauth2 = {
...dbUser.oauth2,
...details,
}
await db.put(dbUser)
await invalidateUser(userId)
} catch (e) {
console.error("Could not update OAuth details for current user", e)
}
}
module.exports = { module.exports = {
buildAuthMiddleware: authenticated, buildAuthMiddleware: authenticated,
passport, passport,
@ -46,4 +170,7 @@ module.exports = {
authError, authError,
buildCsrfMiddleware: csrf, buildCsrfMiddleware: csrf,
internalApi, internalApi,
refreshOAuthToken,
updateUserOAuth,
ssoCallbackUrl,
} }

View File

@ -314,6 +314,7 @@ function getContextDB(key, opts) {
toUseAppId = getDevelopmentAppID(appId) toUseAppId = getDevelopmentAppID(appId)
break break
} }
db = dangerousGetDB(toUseAppId, opts) db = dangerousGetDB(toUseAppId, opts)
try { try {
cls.setOnContext(key, db) cls.setOnContext(key, db)

View File

@ -1,42 +0,0 @@
exports.SEPARATOR = "_"
exports.UNICODE_MAX = "\ufff0"
const PRE_APP = "app"
const PRE_DEV = "dev"
exports.DocumentTypes = {
USER: "us",
WORKSPACE: "workspace",
CONFIG: "config",
TEMPLATE: "template",
APP: PRE_APP,
DEV: PRE_DEV,
APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`,
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
ROLE: "role",
MIGRATIONS: "migrations",
DEV_INFO: "devinfo",
}
exports.StaticDatabases = {
GLOBAL: {
name: "global-db",
docs: {
apiKeys: "apikeys",
usageQuota: "usage_quota",
licenseInfo: "license_info",
},
},
// contains information about tenancy and so on
PLATFORM_INFO: {
name: "global-info",
docs: {
tenants: "tenants",
install: "install",
},
},
}
exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR
exports.APP_DEV = exports.APP_DEV_PREFIX =
exports.DocumentTypes.APP_DEV + exports.SEPARATOR

View File

@ -0,0 +1,58 @@
export const SEPARATOR = "_"
export const UNICODE_MAX = "\ufff0"
/**
* Can be used to create a few different forms of querying a view.
*/
export enum AutomationViewModes {
ALL = "all",
AUTOMATION = "automation",
STATUS = "status",
}
export enum ViewNames {
USER_BY_EMAIL = "by_email",
BY_API_KEY = "by_api_key",
USER_BY_BUILDERS = "by_builders",
LINK = "by_link",
ROUTING = "screen_routes",
AUTOMATION_LOGS = "automation_logs",
}
export enum DocumentTypes {
USER = "us",
WORKSPACE = "workspace",
CONFIG = "config",
TEMPLATE = "template",
APP = "app",
DEV = "dev",
APP_DEV = "app_dev",
APP_METADATA = "app_metadata",
ROLE = "role",
MIGRATIONS = "migrations",
DEV_INFO = "devinfo",
AUTOMATION_LOG = "log_au",
}
export const StaticDatabases = {
GLOBAL: {
name: "global-db",
docs: {
apiKeys: "apikeys",
usageQuota: "usage_quota",
licenseInfo: "license_info",
},
},
// contains information about tenancy and so on
PLATFORM_INFO: {
name: "global-info",
docs: {
tenants: "tenants",
install: "install",
},
},
}
export const APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR
export const APP_DEV = exports.DocumentTypes.APP_DEV + exports.SEPARATOR
export const APP_DEV_PREFIX = APP_DEV

View File

@ -1,7 +1,7 @@
import { newid } from "../hashing" import { newid } from "../hashing"
import { DEFAULT_TENANT_ID, Configs } from "../constants" import { DEFAULT_TENANT_ID, Configs } from "../constants"
import env from "../environment" import env from "../environment"
import { SEPARATOR, DocumentTypes, UNICODE_MAX } from "./constants" import { SEPARATOR, DocumentTypes, UNICODE_MAX, ViewNames } from "./constants"
import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy" import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy"
import fetch from "node-fetch" import fetch from "node-fetch"
import { doWithDB, allDbs } from "./index" import { doWithDB, allDbs } from "./index"
@ -12,12 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions"
import { APP_PREFIX } from "./constants" import { APP_PREFIX } from "./constants"
import * as events from "../events" import * as events from "../events"
export const ViewNames = {
USER_BY_EMAIL: "by_email",
BY_API_KEY: "by_api_key",
USER_BY_BUILDERS: "by_builders",
}
export * from "./constants" export * from "./constants"
export * from "./conversions" export * from "./conversions"
export { default as Replication } from "./Replication" export { default as Replication } from "./Replication"
@ -61,6 +55,13 @@ export function getDocParams(
} }
} }
/**
* Retrieve the correct index for a view based on default design DB.
*/
export function getQueryIndex(viewName: ViewNames) {
return `database/${viewName}`
}
/** /**
* Generates a new workspace ID. * Generates a new workspace ID.
* @returns {string} The new workspace ID which the workspace doc can be stored under. * @returns {string} The new workspace ID which the workspace doc can be stored under.
@ -386,7 +387,9 @@ export const getScopedFullConfig = async function (
if (type === Configs.SETTINGS) { if (type === Configs.SETTINGS) {
if (scopedConfig && scopedConfig.doc) { if (scopedConfig && scopedConfig.doc) {
// overrides affected by environment variables // overrides affected by environment variables
scopedConfig.doc.config.platformUrl = await getPlatformUrl() scopedConfig.doc.config.platformUrl = await getPlatformUrl({
tenantAware: true,
})
scopedConfig.doc.config.analyticsEnabled = scopedConfig.doc.config.analyticsEnabled =
await events.analytics.enabled() await events.analytics.enabled()
} else { } else {
@ -395,7 +398,7 @@ export const getScopedFullConfig = async function (
doc: { doc: {
_id: generateConfigID({ type, user, workspace }), _id: generateConfigID({ type, user, workspace }),
config: { config: {
platformUrl: await getPlatformUrl(), platformUrl: await getPlatformUrl({ tenantAware: true }),
analyticsEnabled: await events.analytics.enabled(), analyticsEnabled: await events.analytics.enabled(),
}, },
}, },

View File

@ -40,7 +40,7 @@ const env = {
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
PLATFORM_URL: process.env.PLATFORM_URL, PLATFORM_URL: process.env.PLATFORM_URL || "",
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS, TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,

View File

@ -13,6 +13,7 @@ import deprovisioning from "./context/deprovision"
import auth from "./auth" import auth from "./auth"
import constants from "./constants" import constants from "./constants"
import * as dbConstants from "./db/constants" import * as dbConstants from "./db/constants"
import logging from "./logging"
// mimic the outer package exports // mimic the outer package exports
import * as db from "./pkg/db" import * as db from "./pkg/db"
@ -49,6 +50,7 @@ const core = {
deprovisioning, deprovisioning,
installation, installation,
errors, errors,
logging,
...errorClasses, ...errorClasses,
} }

View File

@ -1,10 +1,10 @@
const NonErrors = ["AccountError"] const NonErrors = ["AccountError"]
function isSuppressed(e) { function isSuppressed(e?: any) {
return e && e["suppressAlert"] return e && e["suppressAlert"]
} }
module.exports.logAlert = (message, e) => { export function logAlert(message: string, e?: any) {
if (e && NonErrors.includes(e.name) && isSuppressed(e)) { if (e && NonErrors.includes(e.name) && isSuppressed(e)) {
return return
} }
@ -14,3 +14,7 @@ module.exports.logAlert = (message, e) => {
} }
console.error(`bb-alert: ${message} ${errorJson}`) console.error(`bb-alert: ${message} ${errorJson}`)
} }
export default {
logAlert,
}

View File

@ -94,7 +94,6 @@ module.exports = (
user = await getUser(userId, session.tenantId) user = await getUser(userId, session.tenantId)
} }
user.csrfToken = session.csrfToken user.csrfToken = session.csrfToken
delete user.password
authenticated = true authenticated = true
} catch (err) { } catch (err) {
error = err error = err
@ -128,6 +127,8 @@ module.exports = (
} }
if (!user && tenantId) { if (!user && tenantId) {
user = { tenantId } user = { tenantId }
} else {
delete user.password
} }
// be explicit // be explicit
if (authenticated !== true) { if (authenticated !== true) {

View File

@ -2,7 +2,7 @@ const jwt = require("./passport/jwt")
const local = require("./passport/local") const local = require("./passport/local")
const google = require("./passport/google") const google = require("./passport/google")
const oidc = require("./passport/oidc") const oidc = require("./passport/oidc")
const { authError } = require("./passport/utils") const { authError, ssoCallbackUrl } = require("./passport/utils")
const authenticated = require("./authenticated") const authenticated = require("./authenticated")
const auditLog = require("./auditLog") const auditLog = require("./auditLog")
const tenancy = require("./tenancy") const tenancy = require("./tenancy")
@ -20,6 +20,7 @@ module.exports = {
tenancy, tenancy,
authError, authError,
internalApi, internalApi,
ssoCallbackUrl,
datasource: { datasource: {
google: datasourceGoogle, google: datasourceGoogle,
}, },

View File

@ -1,6 +1,7 @@
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
const { ssoCallbackUrl } = require("./utils")
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
const { Configs } = require("../../../constants")
const buildVerifyFn = saveUserFn => { const buildVerifyFn = saveUserFn => {
return (accessToken, refreshToken, profile, done) => { return (accessToken, refreshToken, profile, done) => {
@ -57,5 +58,10 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
) )
} }
} }
exports.getCallbackUrl = async function (db, config) {
return ssoCallbackUrl(db, config, Configs.GOOGLE)
}
// expose for testing // expose for testing
exports.buildVerifyFn = buildVerifyFn exports.buildVerifyFn = buildVerifyFn

View File

@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) {
if (await compare(password, dbUser.password)) { if (await compare(password, dbUser.password)) {
const sessionId = newid() const sessionId = newid()
const tenantId = getTenantId() const tenantId = getTenantId()
await createASession(dbUser._id, { sessionId, tenantId }) await createASession(dbUser._id, { sessionId, tenantId })
dbUser.token = jwt.sign( dbUser.token = jwt.sign(

View File

@ -1,6 +1,8 @@
const fetch = require("node-fetch") const fetch = require("node-fetch")
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
const { authenticateThirdParty } = require("./third-party-common") const { authenticateThirdParty } = require("./third-party-common")
const { ssoCallbackUrl } = require("./utils")
const { Configs } = require("../../../constants")
const buildVerifyFn = saveUserFn => { const buildVerifyFn = saveUserFn => {
/** /**
@ -89,11 +91,24 @@ function validEmail(value) {
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
* @returns Dynamically configured Passport OIDC Strategy * @returns Dynamically configured Passport OIDC Strategy
*/ */
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { exports.strategyFactory = async function (config, saveUserFn) {
try { try {
const { clientID, clientSecret, configUrl } = config const verify = buildVerifyFn(saveUserFn)
const strategy = new OIDCStrategy(config, verify)
strategy.name = "oidc"
return strategy
} catch (err) {
console.error(err)
throw new Error("Error constructing OIDC authentication strategy", err)
}
}
exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) {
try {
const { clientID, clientSecret, configUrl } = enrichedConfig
if (!clientID || !clientSecret || !callbackUrl || !configUrl) { if (!clientID || !clientSecret || !callbackUrl || !configUrl) {
//check for remote config and all required elements
throw new Error( throw new Error(
"Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl"
) )
@ -109,24 +124,24 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
const body = await response.json() const body = await response.json()
const verify = buildVerifyFn(saveUserFn) return {
return new OIDCStrategy( issuer: body.issuer,
{ authorizationURL: body.authorization_endpoint,
issuer: body.issuer, tokenURL: body.token_endpoint,
authorizationURL: body.authorization_endpoint, userInfoURL: body.userinfo_endpoint,
tokenURL: body.token_endpoint, clientID: clientID,
userInfoURL: body.userinfo_endpoint, clientSecret: clientSecret,
clientID: clientID, callbackURL: callbackUrl,
clientSecret: clientSecret, }
callbackURL: callbackUrl,
},
verify
)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
throw new Error("Error constructing OIDC authentication strategy", err) throw new Error("Error constructing OIDC authentication configuration", err)
} }
} }
exports.getCallbackUrl = async function (db, config) {
return ssoCallbackUrl(db, config, Configs.OIDC)
}
// expose for testing // expose for testing
exports.buildVerifyFn = buildVerifyFn exports.buildVerifyFn = buildVerifyFn

View File

@ -48,8 +48,8 @@ describe("oidc", () => {
it("should create successfully create an oidc strategy", async () => { it("should create successfully create an oidc strategy", async () => {
const oidc = require("../oidc") const oidc = require("../oidc")
const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl)
await oidc.strategyFactory(oidcConfig, callbackUrl) await oidc.strategyFactory(enrichedConfig, callbackUrl)
expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl)

View File

@ -1,3 +1,7 @@
const { isMultiTenant, getTenantId } = require("../../tenancy")
const { getScopedConfig } = require("../../db/utils")
const { Configs } = require("../../constants")
/** /**
* Utility to handle authentication errors. * Utility to handle authentication errors.
* *
@ -5,6 +9,7 @@
* @param {*} message Message that will be returned in the response body * @param {*} message Message that will be returned in the response body
* @param {*} err (Optional) error that will be logged * @param {*} err (Optional) error that will be logged
*/ */
exports.authError = function (done, message, err = null) { exports.authError = function (done, message, err = null) {
return done( return done(
err, err,
@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) {
{ message: message } { message: message }
) )
} }
exports.ssoCallbackUrl = async (db, config, type) => {
// incase there is a callback URL from before
if (config && config.callbackURL) {
return config.callbackURL
}
const publicConfig = await getScopedConfig(db, {
type: Configs.SETTINGS,
})
let callbackUrl = `/api/global/auth`
if (isMultiTenant()) {
callbackUrl += `/${getTenantId()}`
}
callbackUrl += `/${type}/callback`
return `${publicConfig.platformUrl}${callbackUrl}`
}

View File

@ -4123,6 +4123,11 @@ passport-oauth1@1.x.x:
passport-strategy "1.x.x" passport-strategy "1.x.x"
utils-merge "1.x.x" utils-merge "1.x.x"
passport-oauth2-refresh@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4"
integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A==
passport-oauth2@1.x.x: passport-oauth2@1.x.x:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b"

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.212-alpha.8", "version": "1.0.219",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.212-alpha.8", "@budibase/string-templates": "^1.0.219",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",
@ -86,4 +86,4 @@
"svelte-portal": "^1.0.0" "svelte-portal": "^1.0.0"
}, },
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
} }

View File

@ -13,6 +13,7 @@
export let size = "M" export let size = "M"
export let active = false export let active = false
export let fullWidth = false export let fullWidth = false
export let noPadding = false
function longPress(element) { function longPress(element) {
if (!longPressable) return if (!longPressable) return
@ -41,6 +42,7 @@
class:spectrum-ActionButton--quiet={quiet} class:spectrum-ActionButton--quiet={quiet}
class:spectrum-ActionButton--emphasized={emphasized} class:spectrum-ActionButton--emphasized={emphasized}
class:is-selected={selected} class:is-selected={selected}
class:noPadding
class:fullWidth class:fullWidth
class="spectrum-ActionButton spectrum-ActionButton--size{size}" class="spectrum-ActionButton spectrum-ActionButton--size{size}"
class:active class:active
@ -86,4 +88,8 @@
.is-selected:not(.spectrum-ActionButton--emphasized) { .is-selected:not(.spectrum-ActionButton--emphasized) {
background: var(--spectrum-global-color-gray-300); background: var(--spectrum-global-color-gray-300);
} }
.noPadding {
padding: 0;
min-width: 0;
}
</style> </style>

View File

@ -40,5 +40,6 @@
on:change={onChange} on:change={onChange}
on:pick on:pick
on:type on:type
on:blur
/> />
</Field> </Field>

View File

@ -52,7 +52,10 @@
{id} {id}
type="text" type="text"
on:focus={() => (focus = true)} on:focus={() => (focus = true)}
on:blur={() => (focus = false)} on:blur={() => {
focus = false
dispatch("blur")
}}
on:change={onType} on:change={onType}
value={value || ""} value={value || ""}
placeholder={placeholder || ""} placeholder={placeholder || ""}

View File

@ -1,15 +1,20 @@
<script> <script>
import { ActionButton } from "../"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let type = "info" export let type = "info"
export let icon = "Info" export let icon = "Info"
export let message = "" export let message = ""
export let dismissable = false export let dismissable = false
export let actionMessage = null
export let action = null
export let wide = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
</script> </script>
<div class="spectrum-Toast spectrum-Toast--{type}"> <div class="spectrum-Toast spectrum-Toast--{type}" class:wide>
{#if icon} {#if icon}
<svg <svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon" class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon"
@ -19,8 +24,13 @@
<use xlink:href="#spectrum-icon-18-{icon}" /> <use xlink:href="#spectrum-icon-18-{icon}" />
</svg> </svg>
{/if} {/if}
<div class="spectrum-Toast-body"> <div class="spectrum-Toast-body" class:actionBody={!!action}>
<div class="spectrum-Toast-content">{message || ""}</div> <div class="spectrum-Toast-content">{message || ""}</div>
{#if action}
<ActionButton quiet emphasized on:click={action}>
<div style="color: white; font-weight: 600;">{actionMessage}</div>
</ActionButton>
{/if}
</div> </div>
{#if dismissable} {#if dismissable}
<div class="spectrum-Toast-buttons"> <div class="spectrum-Toast-buttons">
@ -46,4 +56,15 @@
.spectrum-Toast { .spectrum-Toast {
pointer-events: all; pointer-events: all;
} }
.wide {
width: 100%;
}
.actionBody {
justify-content: space-between;
display: flex;
width: 100%;
align-items: center;
}
</style> </style>

View File

@ -8,13 +8,15 @@
<Portal target=".modal-container"> <Portal target=".modal-container">
<div class="notifications"> <div class="notifications">
{#each $notifications as { type, icon, message, id, dismissable } (id)} {#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
<div transition:fly={{ y: -30 }}> <div transition:fly={{ y: -30 }}>
<Notification <Notification
{type} {type}
{icon} {icon}
{message} {message}
{dismissable} {dismissable}
{action}
{wide}
on:dismiss={() => notifications.dismiss(id)} on:dismiss={() => notifications.dismiss(id)}
/> />
</div> </div>

View File

@ -20,7 +20,16 @@ export const createNotificationStore = () => {
setTimeout(() => (block = false), timeout) setTimeout(() => (block = false), timeout)
} }
const send = (message, type = "default", icon = "", autoDismiss = true) => { const send = (
message,
{
type = "default",
icon = "",
autoDismiss = true,
action = null,
wide = false,
}
) => {
if (block) { if (block) {
return return
} }
@ -28,7 +37,15 @@ export const createNotificationStore = () => {
_notifications.update(state => { _notifications.update(state => {
return [ return [
...state, ...state,
{ id: _id, type, message, icon, dismissable: !autoDismiss }, {
id: _id,
type,
message,
icon,
dismissable: !autoDismiss,
action,
wide,
},
] ]
}) })
if (autoDismiss) { if (autoDismiss) {
@ -50,10 +67,11 @@ export const createNotificationStore = () => {
return { return {
subscribe, subscribe,
send, send,
info: msg => send(msg, "info", "Info"), info: msg => send(msg, { type: "info", icon: "Info" }),
error: msg => send(msg, "error", "Alert", false), error: msg =>
warning: msg => send(msg, "warning", "Alert"), send(msg, { type: "error", icon: "Alert", autoDismiss: false }),
success: msg => send(msg, "success", "CheckmarkCircle"), warning: msg => send(msg, { type: "warning", icon: "Alert" }),
success: msg => send(msg, { type: "success", icon: "CheckmarkCircle" }),
blockNotifications, blockNotifications,
dismiss: dismissNotification, dismiss: dismissNotification,
} }

View File

@ -5,7 +5,7 @@
const displayLimit = 5 const displayLimit = 5
$: badges = value?.slice(0, displayLimit) ?? [] $: badges = Array.isArray(value) ? value.slice(0, displayLimit) : []
$: leftover = (value?.length ?? 0) - badges.length $: leftover = (value?.length ?? 0) - badges.length
</script> </script>

View File

@ -26,12 +26,20 @@
array: ArrayRenderer, array: ArrayRenderer,
internal: InternalRenderer, internal: InternalRenderer,
} }
$: type = schema?.type ?? "string" $: type = getType(schema)
$: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
$: width = schema?.width || "150px" $: width = schema?.width || "150px"
$: cellValue = getCellValue(value, schema.template) $: cellValue = getCellValue(value, schema.template)
const getType = schema => {
// Use a string renderer for dates if we use a custom template
if (schema?.type === "datetime" && schema?.template) {
return "string"
}
return schema?.type || "string"
}
const getCellValue = (value, template) => { const getCellValue = (value, template) => {
if (!template) { if (!template) {
return value return value

View File

@ -37,6 +37,7 @@
export let autoSortColumns = true export let autoSortColumns = true
export let compact = false export let compact = false
export let customPlaceholder = false export let customPlaceholder = false
export let placeholderText = "No rows found"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -405,7 +406,7 @@
> >
<use xlink:href="#spectrum-icon-18-Table" /> <use xlink:href="#spectrum-icon-18-Table" />
</svg> </svg>
<div>No rows found</div> <div>{placeholderText}</div>
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -16,4 +16,4 @@
"runMode": 1, "runMode": 1,
"openMode": 0 "openMode": 0
} }
} }

View File

@ -10,6 +10,7 @@ filterTests(['all'], () => {
it("should add Radio Buttons options picker on form, add data, and confirm", () => { it("should add Radio Buttons options picker on form, add data, and confirm", () => {
cy.navigateToFrontend() cy.navigateToFrontend()
cy.wait(500)
cy.addComponent("Form", "Form") cy.addComponent("Form", "Form")
cy.addComponent("Form", "Options Picker").then((componentId) => { cy.addComponent("Form", "Options Picker").then((componentId) => {
// Provide field setting // Provide field setting
@ -36,5 +37,9 @@ filterTests(['all'], () => {
}) })
cy.addCustomSourceOptions(totalRadioButtons) cy.addCustomSourceOptions(totalRadioButtons)
} }
after(() => {
cy.deleteAllApps()
})
}) })
}) })

View File

@ -9,10 +9,11 @@ filterTests(["smoke", "all"], () => {
before(() => { before(() => {
cy.login() cy.login()
cy.deleteApp("Cypress Tests") cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests") cy.createApp("Cypress Tests", false)
// Create new user // Create new user
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) cy.wait(500)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.createUser(bbUserEmail) cy.createUser(bbUserEmail)
cy.contains("bbuser").click() cy.contains("bbuser").click()
cy.wait(500) cy.wait(500)

View File

@ -6,11 +6,11 @@ filterTests(["smoke", "all"], () => {
before(() => { before(() => {
cy.login() cy.login()
cy.deleteApp("Cypress Tests") cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests") cy.createApp("Cypress Tests", false)
}) })
it("should create a user via basic onboarding", () => { it("should create a user via basic onboarding", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.createUser("bbuser@test.com") cy.createUser("bbuser@test.com")
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser") cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser")
}) })
@ -43,19 +43,20 @@ filterTests(["smoke", "all"], () => {
const uuid = () => Cypress._.random(0, 1e6) const uuid = () => Cypress._.random(0, 1e6)
const name = uuid() const name = uuid()
if(i < 1){ if(i < 1){
cy.createApp(name) cy.createApp(name, false)
} else { } else {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) cy.wait(1000)
cy.get(interact.CREATE_APP_BUTTON, { timeout: 2000 }).click({ force: true })
cy.createAppFromScratch(name) cy.createAppFromScratch(name)
} }
} }
} }
}) })
// Navigate back to the user // Navigate back to the user
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 500}) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click() cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click()
cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).contains("bbuser").click() cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click()
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
cy.get(interact.SPECTRUM_TABLE, { timeout: 3000}) cy.get(interact.SPECTRUM_TABLE, { timeout: 3000})
.eq(1) .eq(1)
@ -64,24 +65,24 @@ filterTests(["smoke", "all"], () => {
.find(interact.SPECTRUM_TABLE_CELL) .find(interact.SPECTRUM_TABLE_CELL)
.eq(0) .eq(0)
.click() .click()
cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 500 }) cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 })
.contains("Choose an option") .contains("Choose an option")
.click() .click()
.then(() => { .then(() => {
if (i == 0) { if (i == 0) {
cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Admin").click({ force: true }) cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Admin").click({ force: true })
} }
else if (i == 1) { else if (i == 1) {
cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Power").click({ force: true }) cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Power").click({ force: true })
} }
else if (i == 2) { else if (i == 2) {
cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Basic").click({ force: true }) cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Basic").click({ force: true })
} }
cy.get(interact.SPECTRUM_BUTTON, { timeout: 1000 }) cy.get(interact.SPECTRUM_BUTTON, { timeout: 2000 })
.contains("Update role") .contains("Update role")
.click({ force: true }) .click({ force: true })
}) })
cy.reload() cy.reload({ timeout: 5000 })
cy.wait(1000) cy.wait(1000)
} }
// Confirm roles exist within Configure roles table // Confirm roles exist within Configure roles table
@ -173,14 +174,16 @@ filterTests(["smoke", "all"], () => {
it("Should edit user details within user details page", () => { it("Should edit user details within user details page", () => {
// Add First name // Add First name
cy.get(interact.FIELD, { timeout: 500 }).eq(2).within(() => { cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).type("bb") cy.wait(500)
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
}) })
// Add Last name // Add Last name
cy.get(interact.FIELD).eq(3).within(() => { cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("test") cy.wait(500)
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
}) })
cy.get(interact.FIELD).eq(0).click() cy.get(interact.FIELD, { timeout: 1000 }).eq(0).click()
// Reload page // Reload page
cy.reload() cy.reload()
@ -188,8 +191,8 @@ filterTests(["smoke", "all"], () => {
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb") cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
}) })
cy.get(interact.FIELD).eq(3).within(() => { cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).should('have.value', "test") cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
}) })
}) })

View File

@ -103,6 +103,8 @@ filterTests(["smoke", "all"], () => {
} }
cy.get("button").contains("Update password").click({ force: true }) cy.get("button").contains("Update password").click({ force: true })
}) })
// Remove users name
cy.updateUserInformation()
}) })
}) })
}) })

View File

@ -5,7 +5,8 @@ filterTests(["all"], () => {
context("Application Overview screen", () => { context("Application Overview screen", () => {
before(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() cy.deleteAllApps()
cy.createApp("Cypress Tests")
}) })
it("Should be accessible from the applications list", () => { it("Should be accessible from the applications list", () => {
@ -81,13 +82,14 @@ filterTests(["all"], () => {
}) })
it("Should reflect the app deployment state", () => { it("Should reflect the app deployment state", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Edit") .contains("Edit")
.eq(0) .eq(0)
.click({ force: true }) .click({ force: true })
cy.get(".toprightnav button.spectrum-Button") cy.wait(500)
cy.get(".toprightnav button.spectrum-Button", { timeout: 2000 })
.contains("Publish") .contains("Publish")
.click({ force: true }) .click({ force: true })
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']") cy.get(".spectrum-Modal [data-cy='deploy-app-modal']")
@ -300,7 +302,7 @@ filterTests(["all"], () => {
}) })
it("Should allow editing of the app details.", () => { it("Should allow editing of the app details.", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Manage") .contains("Manage")
.eq(0) .eq(0)
@ -315,7 +317,8 @@ filterTests(["all"], () => {
cy.updateAppName("sample name") cy.updateAppName("sample name")
//publish and check its disabled //publish and check its disabled
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.wait(500)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Edit") .contains("Edit")
.eq(0) .eq(0)
@ -331,8 +334,8 @@ filterTests(["all"], () => {
cy.wait(1000) cy.wait(1000)
}) })
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
cy.get(".appTable .app-row-actions button", { timeout: 1000 }) cy.get(".appTable .app-row-actions button", { timeout: 5000 })
.contains("Manage") .contains("Manage")
.eq(0) .eq(0)
.click({ force: true }) .click({ force: true })

View File

@ -6,11 +6,12 @@ filterTests(['all'], () => {
context("Publish Application Workflow", () => { context("Publish Application Workflow", () => {
before(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() cy.deleteAllApps()
cy.createApp("Cypress Tests", false)
}) })
it("Should reflect the unpublished status correctly", () => { it("Should reflect the unpublished status correctly", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0)
.within(() => { .within(() => {
@ -29,6 +30,7 @@ filterTests(['all'], () => {
it("Should publish an application and correctly reflect that", () => { it("Should publish an application and correctly reflect that", () => {
//Assuming the previous test was run and the unpublished app is open in edit mode. //Assuming the previous test was run and the unpublished app is open in edit mode.
cy.closeModal()
cy.get(interact.TOPRIGHTNAV_BUTTON_SPECTRUM).contains("Publish").click({ force : true }) cy.get(interact.TOPRIGHTNAV_BUTTON_SPECTRUM).contains("Publish").click({ force : true })
cy.get(interact.DEPLOY_APP_MODAL).should("be.visible") cy.get(interact.DEPLOY_APP_MODAL).should("be.visible")
@ -72,7 +74,7 @@ filterTests(['all'], () => {
it("Should unpublish an application using the link and reflect the status change", () => { it("Should unpublish an application using the link and reflect the status change", () => {
//Assuming the previous test app exists and is published //Assuming the previous test app exists and is published
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(interact.APP_TABLE_STATUS).eq(0) cy.get(interact.APP_TABLE_STATUS).eq(0)
.within(() => { .within(() => {
@ -85,19 +87,21 @@ filterTests(['all'], () => {
cy.get(interact.APP_TABLE_APP_NAME).click({ force: true }) cy.get(interact.APP_TABLE_APP_NAME).click({ force: true })
}) })
cy.get(interact.DEPLOYMENT_TOP_NAV).click() cy.closeModal()
cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true })
cy.get(interact.UNPUBLISH_MODAL)
.within(() => { cy.get("[data-cy='publish-popover-menu']")
cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }) .within(() => {
}) cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true })
})
cy.get(interact.UNPUBLISH_MODAL).should("be.visible") cy.get(interact.UNPUBLISH_MODAL).should("be.visible")
.within(() => { .within(() => {
cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true } cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }
)}) )})
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 })
cy.wait(500)
cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished") cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished")
}) })

View File

@ -51,14 +51,8 @@ filterTests(['smoke', 'all'], () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
// Start create app process. If apps already exist, click second button // Start create app process. If apps already exist, click second button
cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) cy.wait(1000)
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.get(interact.CREATE_APP_BUTTON, { timeout: 3000 }).click({ force: true })
.its("body")
.then(val => {
if (val.length > 0) {
cy.get(interact.CREATE_APP_BUTTON).click({ force: true })
}
})
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.get(interact.SPECTRUM_MODAL).within(() => { cy.get(interact.SPECTRUM_MODAL).within(() => {
@ -93,7 +87,7 @@ filterTests(['smoke', 'all'], () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.createApp(appName, false) cy.createApp(appName, false)
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.applicationInAppTable(appName) cy.applicationInAppTable(appName)
cy.deleteApp(appName) cy.deleteApp(appName)

View File

@ -5,7 +5,6 @@ filterTests(['smoke', 'all'], () => {
context("Create a View", () => { context("Create a View", () => {
before(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() cy.createTestApp()
cy.createTable("data") cy.createTable("data")
cy.addColumn("data", "group", "Text") cy.addColumn("data", "group", "Text")

View File

@ -111,6 +111,7 @@ filterTests(["all"], () => {
// Save relationship & reload page // Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload() cy.reload()
cy.wait(1000)
}) })
// Confirm table length & relationship name // Confirm table length & relationship name
cy.get(".spectrum-Table", { timeout: 1000 }) cy.get(".spectrum-Table", { timeout: 1000 })

View File

@ -151,7 +151,7 @@ filterTests(["all"], () => {
cy.get("@query").its("response.body").should("not.be.empty") cy.get("@query").its("response.body").should("not.be.empty")
// Save query // Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".hierarchy-items-container").should("contain", queryName) cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should("contain", queryName)
}) })
it("should switch to schema with no tables", () => { it("should switch to schema with no tables", () => {
@ -217,24 +217,24 @@ filterTests(["all"], () => {
it("should edit a query name", () => { it("should edit a query name", () => {
// Access query // Access query
cy.get(".hierarchy-items-container") cy.get(".hierarchy-items-container", { timeout: 2000 })
.contains(queryName + " (1)") .contains(queryName + " (1)")
.click() .click()
// Rename query // Rename query
cy.get(".spectrum-Form-item") cy.wait(1000)
cy.get(".spectrum-Form-item", { timeout: 2000 })
.eq(0) .eq(0)
.within(() => { .within(() => {
cy.get("input").clear().type(queryRename) cy.get("input").clear().type(queryRename)
}) })
// Run and Save query // Run and Save query
cy.get(".spectrum-Button").contains("Run Query").click({ force: true }) cy.get(".spectrum-Button", { timeout: 2000 }).contains("Run Query").click({ force: true })
cy.wait(500) cy.wait(1000)
cy.get(".spectrum-Button", { timeout: 500 }).contains("Save Query").click({ force: true }) cy.get(".spectrum-Button", { timeout: 2000 }).contains("Save Query").click({ force: true })
//cy.reload() cy.reload({ timeout: 5000 })
//cy.wait(500) cy.get(".nav-item", { timeout: 2000 }).should("contain", queryRename)
cy.get(".nav-item").should("contain", queryRename)
}) })
it("should delete a query", () => { it("should delete a query", () => {
@ -251,6 +251,7 @@ filterTests(["all"], () => {
.contains("Delete Query") .contains("Delete Query")
.click({ force: true }) .click({ force: true })
// Confirm deletion // Confirm deletion
cy.reload({ timeout: 5000 })
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName) cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
}) })

View File

@ -12,7 +12,7 @@ filterTests(["all"], () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
const appRename = "Cypress Renamed" const appRename = "Cypress Renamed"
// Rename app, Search for app, Confirm name was changed // Rename app, Search for app, Confirm name was changed
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, appRename) renameApp(appName, appRename)
cy.reload() cy.reload()
cy.searchForApplication(appRename) cy.searchForApplication(appRename)
@ -39,7 +39,7 @@ filterTests(["all"], () => {
.click({ force: true }) .click({ force: true })
}) })
// Rename app, Search for app, Confirm name was changed // Rename app, Search for app, Confirm name was changed
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, appRename, true) renameApp(appName, appRename, true)
cy.get(interact.APP_TABLE).find(interact.WRAPPER).should("have.length", 1) cy.get(interact.APP_TABLE).find(interact.WRAPPER).should("have.length", 1)
cy.applicationInAppTable(appRename) cy.applicationInAppTable(appRename)
@ -47,7 +47,7 @@ filterTests(["all"], () => {
it("Should try to rename an application to have no name", () => { it("Should try to rename an application to have no name", () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, " ", false, true) renameApp(appName, " ", false, true)
// Close modal and confirm name has not been changed // Close modal and confirm name has not been changed
cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }).contains("Cancel").click() cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }).contains("Cancel").click()
@ -57,7 +57,7 @@ filterTests(["all"], () => {
xit("Should create two applications with the same name", () => { xit("Should create two applications with the same name", () => {
// It is not possible to have applications with the same name // It is not possible to have applications with the same name
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(interact.SPECTRUM_BUTTON), { timeout: 500 } cy.get(interact.SPECTRUM_BUTTON), { timeout: 500 }
.contains("Create app") .contains("Create app")
.click({ force: true }) .click({ force: true })
@ -80,18 +80,15 @@ filterTests(["all"], () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
const numberName = 12345 const numberName = 12345
const specialCharName = "£$%^" const specialCharName = "£$%^"
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
renameApp(appName, numberName) renameApp(appName, numberName)
cy.reload()
cy.applicationInAppTable(numberName) cy.applicationInAppTable(numberName)
cy.reload()
renameApp(numberName, specialCharName) renameApp(numberName, specialCharName)
cy.get(interact.ERROR).should( cy.get(interact.ERROR).should(
"have.text", "have.text",
"App name must be letters, numbers and spaces only" "App name must be letters, numbers and spaces only"
) )
// Set app name back to Cypress Tests // Set app name back to Cypress Tests
cy.reload()
renameApp(numberName, appName) renameApp(numberName, appName)
}) })

View File

@ -134,15 +134,18 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
const shouldCreateDefaultTable = const shouldCreateDefaultTable =
typeof addDefaultTable != "boolean" ? true : addDefaultTable typeof addDefaultTable != "boolean" ? true : addDefaultTable
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
cy.get(`[data-cy="create-app-btn"]`, { timeout: 2000 }).click({ force: true }) cy.wait(1000)
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
// If apps already exist // If apps already exist
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({
force: true,
})
} }
}) })
@ -400,17 +403,19 @@ Cypress.Commands.add("createAppFromScratch", appName => {
Cypress.Commands.add("createTable", (tableName, initialTable) => { Cypress.Commands.add("createTable", (tableName, initialTable) => {
if (!initialTable) { if (!initialTable) {
cy.navigateToDataSection() cy.navigateToDataSection()
cy.get(`[data-cy="new-table"]`).click() cy.get(`[data-cy="new-table"]`, { timeout: 2000 }).click()
} }
cy.wait(2000) cy.wait(2000)
cy.get(".item") cy.get(".item", { timeout: 2000 })
.contains("Budibase DB") .contains("Budibase DB")
.click({ force: true }) .click({ force: true })
.then(() => { .then(() => {
cy.get(".spectrum-Button").contains("Continue").click({ force: true }) cy.get(".spectrum-Button", { timeout: 2000 })
.contains("Continue")
.click({ force: true })
}) })
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
cy.get("input", { timeout: 1000 }).first().type(tableName).blur() cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
cy.get(".spectrum-ButtonGroup").contains("Create").click() cy.get(".spectrum-ButtonGroup").contains("Create").click()
}) })
cy.contains(tableName).should("be.visible") cy.contains(tableName).should("be.visible")
@ -504,12 +509,13 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
// DESIGN AREA // DESIGN AREA
Cypress.Commands.add("addComponent", (category, component) => { Cypress.Commands.add("addComponent", (category, component) => {
if (category) { if (category) {
cy.get(`[data-cy="category-${category}"]`, { timeout: 1000 }).click({ cy.get(`[data-cy="category-${category}"]`, { timeout: 3000 }).click({
force: true, force: true,
}) })
} }
cy.wait(500)
if (component) { if (component) {
cy.get(`[data-cy="component-${component}"]`, { timeout: 1000 }).click({ cy.get(`[data-cy="component-${component}"]`, { timeout: 3000 }).click({
force: true, force: true,
}) })
} }
@ -517,7 +523,7 @@ Cypress.Commands.add("addComponent", (category, component) => {
cy.location().then(loc => { cy.location().then(loc => {
const params = loc.pathname.split("/") const params = loc.pathname.split("/")
const componentId = params[params.length - 1] const componentId = params[params.length - 1]
cy.getComponent(componentId).should("exist") cy.getComponent(componentId, { timeout: 3000 }).should("exist")
return cy.wrap(componentId) return cy.wrap(componentId)
}) })
}) })
@ -621,8 +627,8 @@ Cypress.Commands.add("navigateToFrontend", () => {
// Clicks on Design tab and then the Home nav item // Clicks on Design tab and then the Home nav item
cy.wait(500) cy.wait(500)
cy.contains("Design").click() cy.contains("Design").click()
cy.get(".spectrum-Search").type("/") cy.get(".spectrum-Search", { timeout: 2000 }).type("/")
cy.get(".nav-item").contains("home").click() cy.get(".nav-item", { timeout: 2000 }).contains("home").click()
}) })
Cypress.Commands.add("navigateToDataSection", () => { Cypress.Commands.add("navigateToDataSection", () => {
@ -782,7 +788,7 @@ Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
// MISC // MISC
Cypress.Commands.add("closeModal", () => { Cypress.Commands.add("closeModal", () => {
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => {
cy.get(".close-icon").click() cy.get(".close-icon").click()
cy.wait(1000) // Wait for modal to close cy.wait(1000) // Wait for modal to close
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.212-alpha.8", "version": "1.0.219",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -69,14 +69,15 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.212-alpha.8", "@budibase/bbui": "^1.0.219",
"@budibase/client": "^1.0.212-alpha.8", "@budibase/client": "^1.0.219",
"@budibase/frontend-core": "^1.0.212-alpha.8", "@budibase/frontend-core": "^1.0.219",
"@budibase/string-templates": "^1.0.212-alpha.8", "@budibase/string-templates": "^1.0.219",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",
"codemirror": "^5.59.0", "codemirror": "^5.59.0",
"dayjs": "^1.11.2",
"downloadjs": "1.4.7", "downloadjs": "1.4.7",
"lodash": "4.17.21", "lodash": "4.17.21",
"posthog-js": "1.4.5", "posthog-js": "1.4.5",
@ -120,4 +121,4 @@
"vite": "^2.1.5" "vite": "^2.1.5"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
} }

View File

@ -53,7 +53,7 @@ export default class IntercomClient {
* @returns Intercom global object * @returns Intercom global object
*/ */
show(user = {}) { show(user = {}) {
if (!this.initialised) return if (!this.initialised || !user?.admin) return
return window.Intercom("boot", { return window.Intercom("boot", {
app_id: this.token, app_id: this.token,

View File

@ -49,6 +49,95 @@ export const getBindableProperties = (asset, componentId) => {
] ]
} }
/**
* Gets all rest bindable data fields
*/
export const getRestBindings = () => {
const userBindings = getUserBindings()
return [...userBindings, ...getAuthBindings()]
}
/**
* Gets all rest bindable auth fields
*/
export const getAuthBindings = () => {
let bindings = []
const safeUser = makePropSafe("user")
const safeOAuth2 = makePropSafe("oauth2")
const safeAccessToken = makePropSafe("accessToken")
const authBindings = [
{
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
readable: `Current User.OAuthToken`,
key: "accessToken",
},
]
bindings = Object.keys(authBindings).map(key => {
const fieldBinding = authBindings[key]
return {
type: "context",
runtimeBinding: fieldBinding.runtime,
readableBinding: fieldBinding.readable,
fieldSchema: { type: "string", name: fieldBinding.key },
providerId: "user",
}
})
return bindings
}
/**
* Utility - convert a key/value map to an array of custom 'context' bindings
* @param {object} valueMap Key/value pairings
* @param {string} prefix A contextual string prefix/path for a user readable binding
* @return {object[]} An array containing readable/runtime binding objects
*/
export const toBindingsArray = (valueMap, prefix) => {
if (!valueMap) {
return []
}
return Object.keys(valueMap).reduce((acc, binding) => {
if (!binding || !valueMap[binding]) {
return acc
}
acc.push({
type: "context",
runtimeBinding: binding,
readableBinding: `${prefix}.${binding}`,
})
return acc
}, [])
}
/**
* Utility - coverting a map of readable bindings to runtime
*/
export const readableToRuntimeMap = (bindings, ctx) => {
if (!bindings || !ctx) {
return {}
}
return Object.keys(ctx).reduce((acc, key) => {
let parsedQuery = readableToRuntimeBinding(bindings, ctx[key])
acc[key] = parsedQuery
return acc
}, {})
}
/**
* Utility - coverting a map of runtime bindings to readable
*/
export const runtimeToReadableMap = (bindings, ctx) => {
if (!bindings || !ctx) {
return {}
}
return Object.keys(ctx).reduce((acc, key) => {
let parsedQuery = runtimeToReadableBinding(bindings, ctx[key])
acc[key] = parsedQuery
return acc
}, {})
}
/** /**
* Gets the bindable properties exposed by a certain component. * Gets the bindable properties exposed by a certain component.
*/ */
@ -298,7 +387,6 @@ const getUserBindings = () => {
providerId: "user", providerId: "user",
}) })
}) })
return bindings return bindings
} }

View File

@ -5,6 +5,7 @@ import { cloneDeep } from "lodash/fp"
const initialAutomationState = { const initialAutomationState = {
automations: [], automations: [],
showTestPanel: false,
blockDefinitions: { blockDefinitions: {
TRIGGER: [], TRIGGER: [],
ACTION: [], ACTION: [],
@ -19,6 +20,17 @@ export const getAutomationStore = () => {
} }
const automationActions = store => ({ const automationActions = store => ({
definitions: async () => {
const response = await API.getAutomationDefinitions()
store.update(state => {
state.blockDefinitions = {
TRIGGER: response.trigger,
ACTION: response.action,
}
return state
})
return response
},
fetch: async () => { fetch: async () => {
const responses = await Promise.all([ const responses = await Promise.all([
API.getAutomations(), API.getAutomations(),
@ -109,6 +121,20 @@ const automationActions = store => ({
return state return state
}) })
}, },
getLogs: async ({ automationId, startDate, status, page } = {}) => {
return await API.getAutomationLogs({
automationId,
startDate,
status,
page,
})
},
clearLogErrors: async ({ automationId, appId } = {}) => {
return await API.clearAutomationLogErrors({
automationId,
appId,
})
},
addTestDataToAutomation: data => { addTestDataToAutomation: data => {
store.update(state => { store.update(state => {
state.selectedAutomation.addTestData(data) state.selectedAutomation.addTestData(data)
@ -117,11 +143,10 @@ const automationActions = store => ({
}, },
addBlockToAutomation: (block, blockIdx) => { addBlockToAutomation: (block, blockIdx) => {
store.update(state => { store.update(state => {
const newBlock = state.selectedAutomation.addBlock( state.selectedBlock = state.selectedAutomation.addBlock(
cloneDeep(block), cloneDeep(block),
blockIdx blockIdx
) )
state.selectedBlock = newBlock
return state return state
}) })
}, },

View File

@ -65,7 +65,7 @@
<ActionButton <ActionButton
disabled={!$automationStore.selectedAutomation?.testResults} disabled={!$automationStore.selectedAutomation?.testResults}
on:click={() => { on:click={() => {
$automationStore.selectedAutomation.automation.showTestPanel = true $automationStore.showTestPanel = true
}} }}
size="M">Test Details</ActionButton size="M">Test Details</ActionButton
> >

View File

@ -1,6 +1,4 @@
<script> <script>
import FlowItemHeader from "./FlowItemHeader.svelte"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { import {
Icon, Icon,
@ -16,6 +14,7 @@
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import ActionModal from "./ActionModal.svelte" import ActionModal from "./ActionModal.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte"
export let block export let block
export let testDataModal export let testDataModal

View File

@ -7,12 +7,19 @@
export let blockComplete export let blockComplete
export let showTestStatus = false export let showTestStatus = false
export let showParameters = {} export let showParameters = {}
export let testResult
export let isTrigger
$: testResult = $: {
$automationStore.selectedAutomation?.testResults?.steps.filter(step => if (!testResult) {
block.id ? step.id === block.id : step.stepId === block.stepId testResult =
) $automationStore.selectedAutomation?.testResults?.steps.filter(step =>
$: isTrigger = block.type === "TRIGGER" block.id ? step.id === block.id : step.stepId === block.stepId
)[0]
}
}
$: isTrigger = isTrigger || block.type === "TRIGGER"
$: status = updateStatus(testResult, isTrigger)
async function onSelect(block) { async function onSelect(block) {
await automationStore.update(state => { await automationStore.update(state => {
@ -20,6 +27,19 @@
return state return state
}) })
} }
function updateStatus(results, isTrigger) {
if (!results) {
return {}
}
if (results.outputs?.status?.toLowerCase() === "stopped") {
return { yellow: true, message: "Stopped" }
} else if (results.outputs?.success || isTrigger) {
return { positive: true, message: "Success" }
} else {
return { negative: true, message: "Error" }
}
}
</script> </script>
<div class="blockSection"> <div class="blockSection">
@ -60,16 +80,13 @@
</div> </div>
</div> </div>
<div class="blockTitle"> <div class="blockTitle">
{#if showTestStatus && testResult && testResult[0]} {#if showTestStatus && testResult}
<div style="float: right;"> <div style="float: right;">
<StatusLight <StatusLight
positive={isTrigger || testResult[0].outputs?.success} positive={status?.positive}
negative={!testResult[0].outputs?.success} yellow={status?.yellow}
><Body size="XS" negative={status?.negative}
>{testResult[0].outputs?.success || isTrigger ><Body size="XS">{status?.message}</Body></StatusLight
? "Success"
: "Error"}</Body
></StatusLight
> >
</div> </div>
{/if} {/if}

View File

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

View File

@ -0,0 +1,137 @@
<script>
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
export let automation
export let testResults
export let width = "400px"
let showParameters
let blocks
function prepTestResults(results) {
return results?.steps.filter(x => x.stepId !== "LOOP" || [])
}
function textArea(results, message) {
if (!results) {
return message
}
return JSON.stringify(results, null, 2)
}
$: filteredResults = prepTestResults(testResults)
$: {
blocks = []
if (automation) {
if (automation.definition.trigger) {
blocks.push(automation.definition.trigger)
}
blocks = blocks
.concat(automation.definition.steps || [])
.filter(x => x.stepId !== "LOOP")
} else if (filteredResults) {
blocks = filteredResults || []
// make sure there is an ID for each block being displayed
let count = 0
for (let block of blocks) {
block.id = count++
}
}
}
</script>
<div class="container">
{#each blocks as block, idx}
<div class="block" style={width ? `width: ${width}` : ""}>
{#if block.stepId !== "LOOP"}
<FlowItemHeader
showTestStatus={true}
bind:showParameters
{block}
isTrigger={idx === 0}
testResult={filteredResults?.[idx]}
/>
{#if showParameters && showParameters[block.id]}
<Divider noMargin />
{#if filteredResults?.[idx]?.outputs.iterations}
<div style="display: flex; padding: 10px 10px 0px 12px;">
<Icon name="Reuse" />
<div style="margin-left: 10px;">
<Label>
This loop ran {filteredResults?.[idx]?.outputs.iterations} times.</Label
>
</div>
</div>
{/if}
<div class="tabs">
<Tabs quiet noPadding selected="Input">
<Tab title="Input">
<div style="padding: 10px 10px 10px 10px;">
<TextArea
minHeight="80px"
disabled
value={textArea(filteredResults?.[idx]?.inputs, "No input")}
/>
</div></Tab
>
<Tab title="Output">
<div style="padding: 10px 10px 10px 10px;">
<TextArea
minHeight="100px"
disabled
value={textArea(
filteredResults?.[idx]?.outputs,
"No output"
)}
/>
</div>
</Tab>
</Tabs>
</div>
{/if}
{/if}
</div>
{#if blocks.length - 1 !== idx}
<div class="separator" />
{/if}
{/each}
</div>
<style>
.container {
padding: 0 30px 0 30px;
height: 100%;
}
.tabs {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
flex: 1 1 auto;
}
.block {
display: inline-block;
width: 400px;
height: auto;
font-size: 16px;
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px 4px 4px 4px;
}
.separator {
width: 1px;
height: 40px;
border-left: 1px dashed var(--grey-4);
color: var(--grey-4);
/* center horizontally */
text-align: center;
margin-left: 50%;
}
</style>

View File

@ -1,11 +1,11 @@
<script> <script>
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui" import { Icon, Divider } from "@budibase/bbui"
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte" import TestDisplay from "./TestDisplay.svelte"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
export let automation export let automation
export let testResults
let showParameters
let blocks let blocks
$: { $: {
@ -17,13 +17,15 @@
blocks = blocks blocks = blocks
.concat(automation.definition.steps || []) .concat(automation.definition.steps || [])
.filter(x => x.stepId !== "LOOP") .filter(x => x.stepId !== "LOOP")
} else if (testResults) {
blocks = testResults.steps || []
}
}
$: {
if (!testResults) {
testResults = $automationStore.selectedAutomation?.testResults
} }
} }
$: testResults =
$automationStore.selectedAutomation?.testResults?.steps.filter(
x => x.stepId !== "LOOP" || []
)
</script> </script>
<div class="title"> <div class="title">
@ -34,7 +36,7 @@
<div style="padding-right: var(--spacing-xl)"> <div style="padding-right: var(--spacing-xl)">
<Icon <Icon
on:click={async () => { on:click={async () => {
$automationStore.selectedAutomation.automation.showTestPanel = false $automationStore.showTestPanel = false
}} }}
hoverable hoverable
name="Close" name="Close"
@ -44,59 +46,9 @@
<Divider /> <Divider />
<div class="container"> <TestDisplay {automation} {testResults} />
{#each blocks as block, idx}
<div class="block">
{#if block.stepId !== "LOOP"}
<FlowItemHeader showTestStatus={true} bind:showParameters {block} />
{#if showParameters && showParameters[block.id]}
<Divider noMargin />
{#if testResults?.[idx]?.outputs.iterations}
<div style="display: flex; padding: 10px 10px 0px 12px;">
<Icon name="Reuse" />
<div style="margin-left: 10px;">
<Label>
This loop ran {testResults?.[idx]?.outputs.iterations} times.</Label
>
</div>
</div>
{/if}
<div class="tabs">
<Tabs quiet noPadding selected="Input">
<Tab title="Input">
<div style="padding: 10px 10px 10px 10px;">
<TextArea
minHeight="80px"
disabled
value={JSON.stringify(testResults?.[idx]?.inputs, null, 2)}
/>
</div></Tab
>
<Tab title="Output">
<div style="padding: 10px 10px 10px 10px;">
<TextArea
minHeight="100px"
disabled
value={JSON.stringify(testResults?.[idx]?.outputs, null, 2)}
/>
</div>
</Tab>
</Tabs>
</div>
{/if}
{/if}
</div>
{#if blocks.length - 1 !== idx}
<div class="separator" />
{/if}
{/each}
</div>
<style> <style>
.container {
padding: 0px 30px 0px 30px;
}
.title { .title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -106,15 +58,6 @@
justify-content: space-between; justify-content: space-between;
} }
.tabs {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
position: relative;
flex: 1 1 auto;
}
.title-text { .title-text {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -124,23 +67,4 @@
.title :global(h1) { .title :global(h1) {
flex: 1 1 auto; flex: 1 1 auto;
} }
.block {
display: inline-block;
width: 400px;
font-size: 16px;
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px 4px 4px 4px;
}
.separator {
width: 1px;
height: 40px;
border-left: 1px dashed var(--grey-4);
color: var(--grey-4);
/* center horizontally */
text-align: center;
margin-left: 50%;
}
</style> </style>

View File

@ -22,10 +22,8 @@
RelationshipTypes, RelationshipTypes,
ALLOWABLE_STRING_OPTIONS, ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS, ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_JSON_OPTIONS,
ALLOWABLE_STRING_TYPES, ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES, ALLOWABLE_NUMBER_TYPES,
ALLOWABLE_JSON_TYPES,
SWITCHABLE_TYPES, SWITCHABLE_TYPES,
} from "constants/backend" } from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils" import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
@ -255,11 +253,6 @@
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1 ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
) { ) {
return ALLOWABLE_NUMBER_OPTIONS return ALLOWABLE_NUMBER_OPTIONS
} else if (
originalName &&
ALLOWABLE_JSON_TYPES.indexOf(field.type) !== -1
) {
return ALLOWABLE_JSON_OPTIONS
} else if (!external) { } else if (!external) {
return [ return [
...Object.values(fieldDefinitions), ...Object.values(fieldDefinitions),

View File

@ -5,8 +5,9 @@
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte" import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api" import { API } from "api"
import { ModalContent, Select } from "@budibase/bbui" import { ModalContent, Select, Link } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
import { goto } from "@roxi/routify"
export let row = {} export let row = {}
@ -87,6 +88,15 @@
onConfirm={saveRow} onConfirm={saveRow}
> >
<ErrorsBox {errors} /> <ErrorsBox {errors} />
<!-- need to explain to the user the readonly fields -->
{#if !creating}
<div>
A user's email, role, first and last names cannot be changed from within
the app builder. Please go to the <Link
on:click={$goto("/builder/portal/manage/users")}>user portal</Link
> to do this.
</div>
{/if}
<RowFieldControl <RowFieldControl
meta={{ ...tableSchema.email, name: "Email" }} meta={{ ...tableSchema.email, name: "Email" }}
bind:value={row.email} bind:value={row.email}

View File

@ -10,11 +10,31 @@
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte" import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte" import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
import {
getRestBindings,
readableToRuntimeBinding,
runtimeToReadableMap,
} from "builderStore/dataBinding"
import { cloneDeep } from "lodash/fp"
export let datasource export let datasource
export let queries export let queries
let addHeader let addHeader
let parsedHeaders = runtimeToReadableMap(
getRestBindings(),
cloneDeep(datasource?.config?.defaultHeaders)
)
const onDefaultHeaderUpdate = headers => {
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
return acc
}, {})
datasource.config.defaultHeaders = flatHeaders
}
</script> </script>
<Divider size="S" /> <Divider size="S" />
@ -30,9 +50,10 @@
</Body> </Body>
<KeyValueBuilder <KeyValueBuilder
bind:this={addHeader} bind:this={addHeader}
bind:object={datasource.config.defaultHeaders} bind:object={parsedHeaders}
on:change on:change={evt => onDefaultHeaderUpdate(evt.detail)}
noAddButton noAddButton
bindings={getRestBindings()}
/> />
<div> <div>
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}> <ActionButton icon="Add" on:click={() => addHeader.addEntry()}>

View File

@ -2,6 +2,8 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { ModalContent, Layout, Select, Body, Input } from "@budibase/bbui" import { ModalContent, Layout, Select, Body, Input } from "@budibase/bbui"
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes" import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte"
import { getAuthBindings } from "builderStore/dataBinding"
export let configs export let configs
export let currentConfig export let currentConfig
@ -203,11 +205,23 @@
/> />
{/if} {/if}
{#if form.type === AUTH_TYPES.BEARER} {#if form.type === AUTH_TYPES.BEARER}
<Input <BindableCombobox
label="Token" label="Token"
bind:value={form.bearer.token} value={form.bearer.token}
on:change={onFieldChange} bindings={getAuthBindings()}
on:blur={() => (blurred.bearer.token = true)} on:change={e => {
form.bearer.token = e.detail
console.log(e.detail)
onFieldChange()
}}
on:blur={() => {
blurred.bearer.token = true
onFieldChange()
}}
allowJS={false}
placeholder="Token"
appendBindingsAsOptions={true}
drawerEnabled={false}
error={blurred.bearer.token ? errors.bearer.token : null} error={blurred.bearer.token ? errors.bearer.token : null}
/> />
{/if} {/if}

View File

@ -4,396 +4,40 @@
</script> </script>
<svg <svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
{width} {width}
{height} {height}
xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 150 150"
viewBox="0 0 1478.201 1195.111" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
xmlns:xlink="http://www.w3.org/1999/xlink"
> >
<g transform="matrix(.569 0 0 .569 199.451 -82.735)"> <g
<linearGradient ><path
id="a" style="opacity:1"
gradientUnits="userSpaceOnUse" fill="#f44336"
x1="-2901.952" d="M 0.5,-0.5 C 20.1667,-0.5 39.8333,-0.5 59.5,-0.5C 59.5,19.5 59.5,39.5 59.5,59.5C 39.5,59.5 19.5,59.5 -0.5,59.5C -0.5,39.8333 -0.5,20.1667 -0.5,0.5C 0.166667,0.5 0.5,0.166667 0.5,-0.5 Z"
y1="923.573" /></g
x2="-2061.249" >
y2="1420.331" <g
gradientTransform="matrix(.1234 0 0 -.1234 1158.33 1550.273)" ><path
> style="opacity:0.999"
<stop offset="0" stop-color="#909ca9" /> fill="#4caf4f"
<stop offset="1" stop-color="#ededee" /> d="M 67.5,-0.5 C 87.1667,-0.5 106.833,-0.5 126.5,-0.5C 126.5,0.166667 126.833,0.5 127.5,0.5C 127.5,20.1667 127.5,39.8333 127.5,59.5C 107.5,59.5 87.5,59.5 67.5,59.5C 67.5,39.5 67.5,19.5 67.5,-0.5 Z"
</linearGradient> /></g
<path >
fill="url(#a)" <g
d="M1410.773 814.195l-286.9 93.683-249.599 110.161-69.829 18.435c-17.784 ><path
16.916-36.431 34.049-56.599 51.397-22.119 19.082-42.72 36.433-58.553 style="opacity:1"
49.008-17.564 13.88-43.587 39.902-56.814 56.38-19.735 24.721-35.348 fill="#2095f3"
50.96-42.071 71.13-11.928 36.433-6.07 73.297 16.916 107.346 29.492 43.369 d="M -0.5,67.5 C 19.5,67.5 39.5,67.5 59.5,67.5C 59.5,87.5 59.5,107.5 59.5,127.5C 39.8333,127.5 20.1667,127.5 0.5,127.5C 0.5,126.833 0.166667,126.5 -0.5,126.5C -0.5,106.833 -0.5,87.1667 -0.5,67.5 Z"
88.261 87.606 156.785 117.749 34.916 15.4 93.683 35.132 137.92 46.19 /></g
73.512 18.651 215.771 38.819 294.054 41.857 15.828.65 37.082.65 37.947 0 >
1.737-1.088 13.881-24.289 27.979-53.129 48.142-98.238 82.838-190.402 <g
101.703-269.119 11.276-47.706 20.166-111.246 26.019-186.492 1.521-21.036 ><path
2.169-91.514.868-115.37-1.953-39.033-5.423-70.692-10.84-101.703-.868-4.555-1.088-8.676-.652-8.892.865-.65 style="opacity:1"
3.467-1.517 38.815-11.712l-7.153-16.912v-.005h.004zm-65.49 38.386c2.602 0 fill="#fec107"
9.539 66.573 11.273 108.646.436 8.89.216 14.745-.216 14.745-1.733 d="M 127.5,67.5 C 127.5,87.1667 127.5,106.833 127.5,126.5C 126.833,126.5 126.5,126.833 126.5,127.5C 106.833,127.5 87.1667,127.5 67.5,127.5C 67.5,107.5 67.5,87.5 67.5,67.5C 87.5,67.5 107.5,67.5 127.5,67.5 Z"
0-36.649-20.599-61.583-36.212-21.687-13.663-62.888-40.988-69.393-46.192-2.173-1.517-1.957-1.733 /></g
15.828-7.807 30.14-10.194 101.706-33.18 104.091-33.18zm-146.161 >
48.143c1.953 0 6.937 2.816 18.865 10.191 44.671 27.974 105.393 61.805
131.415 73.083 8.022 3.469 8.887 2.166-9.542 14.746-39.468 26.889-88.697
53.344-148.983 80.018-10.624 4.771-19.514 8.456-19.73 8.456-.432 0
.865-5.418 2.598-11.925 14.53-54.001 22.772-108.647
23.208-152.452.216-21.687.216-21.687 2.169-22.334-.436.217-.22.217 0
.217zm-30.142 11.492c1.297 1.299.432 49.877-1.304 63.104-3.903
31.662-9.975 61.153-19.947 94.335-2.386 8.018-4.558 14.745-4.987
15.177-.872
1.083-30.581-27.975-40.339-39.251-16.916-19.518-30.141-39.035-39.9-58.117-4.988-9.759-12.793-28.84-12.144-29.492
3.469-2.385 117.753-46.622 118.621-45.756zm-141.826 55.731c.216 0 .432 0
.652.216.432.434 1.953 3.905 3.254 7.807 6.937 18.867 22.548 46.624 35.997
64.407 14.746 19.518 34.048 40.334 50.091 53.996 5.207 4.337 9.975 8.456
10.624 9.108 1.304 1.302 1.737 1.083-33.612 14.53-40.981 15.613-85.656
31.226-136.835 47.706a6825.474 6825.474 0 0 0-36.643
11.928c-1.955.652-1.303-.434 4.335-9.323 25.371-39.686 63.97-117.536
85.657-172.618 3.687-9.542 7.373-19.082 8.025-21.251.868-3.038 1.95-4.121
4.768-5.64 1.518-.43 3.038-.866 3.687-.866zm-43.367 17.999c.649.436-10.411
23.637-21.254 44.889-21.036 40.985-44.022 81.323-74.815 130.331-5.204
8.456-10.19 16.265-10.842 17.132-1.083 1.519-1.519
1.083-4.988-5.638-7.373-14.53-13.447-33.181-16.699-50.313-3.254-16.916-2.602-46.406
1.086-64.621 2.816-13.444 2.602-13.227 9.107-16.481 27.757-14.095
117.537-56.166 118.405-55.299zm374.073 15.182v9.107c0 48.359-5.204
114.716-12.797 163.077-1.301 8.456-2.389 15.393-2.602 15.613 0
0-6.288-1.733-13.661-3.905-32.527-10.193-67.875-25.156-99.754-42.718-21.038-11.494-51.612-30.363-50.743-31.231.213-.215
9.323-4.986 19.947-10.625 42.509-22.118 83.274-45.972 118.622-69.609
13.229-8.892 33.176-23.202 37.518-27.107l3.47-2.602zm-537.802 64.185c.867
0 .65 1.735-.651 9.542-.868 5.64-1.951 16.049-2.382 23.202-1.739 31.662
3.469 55.084 19.082 87.177 4.337 8.892 7.809 16.265 7.589 16.48-1.519
1.303-145.074 43.375-190.183 55.734-13.444 3.685-25.152 6.939-26.024
7.153-1.515.436-1.733.22-1.083-3.47 4.987-31.875 29.276-73.512
63.104-108.644 22.554-23.419 40.554-37.08 71.347-54.648 22.119-12.575
56.165-31.439 58.767-32.309.002-.217.218-.217.434-.217zm338.295
60.503c.216-.216 5.42 2.605 11.708 6.29 46.408 26.891 111.03 51.83 166.108
64.623l4.991 1.086-6.941 3.899c-28.84 16.049-123.606 55.515-220.538
91.732-14.098 5.202-27.975 10.409-30.581 11.492-2.602 1.083-4.988
1.735-4.988 1.519 0-.22 3.906-7.809 8.89-17.132 27.107-50.744
54.433-112.547 68.311-155.485 1.739-4.12 2.82-7.805 3.04-8.024zm-34.48
11.278c.22.221-1.517 4.771-3.687 9.975-18.865 45.756-43.59 95.636-75.249
151.583-8.022 14.314-14.746 25.808-14.966 25.808-.213
0-6.721-3.906-14.527-8.676-45.976-28.192-86.743-62.888-113.414-96.501l-3.905-4.771
19.732-5.422c70.696-19.298 130.762-40.116 190.4-65.704 8.459-3.471
15.4-6.292 15.616-6.292zm214.253 74.815s.217.217 0 0c.216 4.988-10.844
49.661-19.953 81.969-7.589 27.107-14.098 48.361-26.022 85.874-5.204
16.485-9.755 30.143-9.975 30.143-.216
0-1.517-.216-2.818-.647-64.405-11.714-122.089-27.977-176.303-49.661-15.182-6.074-36.866-15.833-38.167-16.916-.432-.438
12.58-6.506 29.06-13.663 98.669-43.154 201.024-92.164 236.153-113.196
4.119-2.603 7.373-3.903 8.025-3.903zm-494.646 16.916c.434.432-27.107
40.118-65.709 94.114-13.444 18.867-29.057 40.985-34.911 49.225-5.856
8.241-14.746 21.253-19.734 29.06l-9.112
14.096-9.759-8.24c-11.494-9.544-31.442-29.927-40.333-41.204-18.651-23.201-31.226-47.706-36.214-70.04-2.386-10.411-2.386-15.618-.22-16.265
3.252-.867 61.153-14.53 115.37-27.11 30.143-6.937 65.054-15.177
77.632-18.213 12.579-3.041 22.774-5.423 22.99-5.423zm27.756 10.626l6.937
7.806c31.231 34.914 63.108 60.724 101.708 83.272 6.941 3.906 12.144 7.373
11.708 7.594-1.514 1.083-134.016 48.136-195.385 69.389-34.478
12.143-62.888 21.901-63.102 21.901-.216
0-2.169-1.299-4.341-2.818l-3.901-2.82 6.288-9.106c20.383-29.493
45.976-61.803 101.707-129.028l38.381-46.19zm173.053 123.822c.213-.215
9.755 3.252 21.464 7.594 28.195 10.624 50.527 17.345 80.456 24.936 36.866
9.326 90.211 18.434 121.657 21.035 4.771.432 7.373.868 6.505
1.519-1.521.868-33.395 11.494-56.816 18.867-37.302 11.708-151.149
45.32-243.962 71.995-17.132 4.987-31.879 9.108-32.746
9.323-2.166.436-9.325-1.519-9.325-2.386 0-.431 5.204-7.153 11.494-14.527
31.225-37.3 62.238-78.935 88.044-118.403 7.154-10.846 13.229-19.736
13.229-19.953zm-38.17 1.087c.216.216-15.179 24.936-42.066 67.439-11.496
17.999-24.291 38.383-28.846 45.54-4.337 6.939-10.842 17.784-14.527
23.854l-6.29
11.061-3.252-.868c-7.809-2.169-62.672-21.471-77.202-27.325-18-7.157-36.649-15.829-50.529-23.202-17.346-9.326-39.03-23.206-37.297-23.637.433-.216
30.143-8.243 65.922-17.999 94.984-25.809 147.678-40.77 182.161-51.612
6.29-1.952 11.71-3.471 11.926-3.251zm269.985 63.318h.216c.868 2.171-34.26
99.755-47.06 130.547-2.815 6.939-3.896 8.677-5.417
8.456-3.687-.213-54.646-7.37-85.66-11.925-53.994-8.24-144.641-24.073-167.409-29.275l-5.204-1.083
32.307-7.378c69.396-15.613 102.791-24.069 136.619-34.478 42.722-13.011
85.011-29.276 127.729-49.225 6.722-3.037 12.361-5.422 13.879-5.639z"
/>
<linearGradient
id="b"
gradientUnits="userSpaceOnUse"
x1="-2882.7"
y1="10288.81"
x2="-2206.249"
y2="10288.81"
gradientTransform="matrix(.1234 0 0 -.1234 1158.33 1550.273)"
>
<stop offset="0" stop-color="#939fab" />
<stop offset="1" stop-color="#dcdee1" />
</linearGradient>
<path
fill="url(#b)"
d="M1114.983 145.414c-4.771-.647-81.757 27.11-131.415 47.275-67.01
27.327-119.052 53.351-151.148 75.899-11.925 8.461-26.891 23.422-29.273
29.276-.867 2.169-1.303 4.771-1.303 7.373l29.06 27.541 69.175 22.119
164.594 29.493 188.228 32.312 1.953-16.264c-.649
0-1.085-.216-1.73-.216l-24.728-3.905-4.984-8.89c-25.59-45.107-53.781-101.056-70.261-138.789-12.793-29.276-24.938-63.102-31.662-87.391-3.687-14.746-4.119-15.613-6.501-15.829v-.005h-.005zm-3.474
11.063h.223c.213.214 1.081 6.29 1.95 13.442 3.683 30.364 10.411 59.635
21.035 91.297 8.022 23.855 8.022 22.555-1.301
19.734-22.119-6.07-121.221-23.202-193-33.177-11.494-1.519-21.253-3.036-21.253-3.252-.867-.867
51.827-28.41 75.031-39.25 29.709-13.665 111.246-47.711
117.315-48.794zm-209.047 97.15l8.461 2.816c45.97 15.616 161.551 37.736
225.31 42.94 7.154.651 13.229 1.303 13.442 1.303.216.216-5.852
3.469-13.661 7.154-30.79 15.397-64.621 34.264-88.042 48.794-6.937
4.335-13.229 7.807-14.094 7.807-.868
0-5.42-.868-10.191-1.519l-8.674-1.303-21.683-21.253c-38.167-37.08-68.094-65.704-79.588-76.549l-11.28-10.19zm-8.671
6.721l30.576 38.168c16.696 21.035 33.611 41.635 37.301 46.187 3.683 4.557
6.721 8.245 6.505
8.461-.868.65-44.236-7.809-67.226-13.011-23.637-5.423-33.395-8.025-47.924-12.577l-11.928-3.905v-3.038c.216-14.53
18.651-36.214 49.877-58.331l2.819-1.954zm259.791 52.046c.869 0 1.95 1.951
4.552 7.806 7.373 16.263 30.364 60.07 35.997 68.526 1.74 2.822 4.771
3.038-25.802-1.95-73.512-11.93-97.152-15.829-97.152-16.263 0-.216
2.169-1.735 4.988-3.254 22.771-12.575 45.756-28.624 66.142-45.756
4.988-4.121 9.542-8.024 10.407-8.676.216-.433.652-.649.868-.433z"
/>
<radialGradient
id="c"
cx="-14217.448"
cy="7277.705"
r="898.12"
gradientTransform="matrix(-.1185 -.0178 -.036 .237 -198.955 -1314.415)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#ee352c" />
<stop offset="1" stop-color="#a91d22" />
</radialGradient>
<path
fill="url(#c)"
d="M804.66 294.828s-4.768 7.593-.215 18.87c2.822 6.937 11.061 15.393
20.384 24.069 0 0 96.5 94.114 108.211 107.561 53.344 61.585 76.549 122.305
78.718 206.012 1.301 53.78-8.894 101.054-34.264 155.919-45.106
98.453-140.307 207.098-287.117 327.67l21.472-7.157c13.878-10.411
32.745-21.467 76.982-45.756 102.137-55.952 217.071-107.346 358.028-160.258
202.971-76.335 536.715-165.681
726.676-194.736l19.737-3.038-3.038-4.771c-17.345-26.891-29.276-43.587-43.59-61.369-41.633-51.612-92.157-93.463-153.964-128.161-85.007-47.489-194.956-84.571-334.173-112.112-26.239-5.207-83.923-15.181-130.763-22.337-99.321-15.393-163.51-26.021-234.203-38.165-25.37-4.339-63.323-10.843-88.478-16.263-13.011-2.822-37.947-8.676-57.464-15.398-15.613-6.075-38.168-12.147-42.939-30.58zm55.952
54.216c.214-.214 3.683 1.083 8.24 2.602 8.24 2.816 18.865 6.07 31.446
9.542a1599.47 1599.47 0 0 0 28.624 7.589c13.011 3.251 23.852 6.288 24.068
6.288 1.521 1.519 23.424 71.558 30.797 98.449 2.815 10.195 4.988 18.867
4.771
18.867-.223.22-2.605-3.469-5.423-8.456-25.373-44.673-65.491-89.995-111.899-126.428-6.069-4.333-10.624-8.237-10.624-8.453zm106.692
29.492c1.085 0 5.856.651 11.708 1.951 36.866 8.24 103.008 20.818 145.293
27.975 7.157 1.083 12.797 2.387 12.797 2.818 0 .436-2.605 1.951-5.859
3.688-7.153 3.685-35.997 20.815-45.536 27.322-24.073 16.047-45.756
33.395-61.371 49.008-6.288 6.29-11.712 11.494-11.712
11.494s-1.297-3.685-2.386-8.242c-7.802-30.143-24.069-74.816-38.815-106.258-2.386-4.986-4.339-9.541-4.339-9.973
0 .433 0 .217.22.217zm187.795 35.781c1.301.432 3.47 7.806 7.806 24.069
8.025 31.446 11.712 66.576 10.411 99.321-.436 9.108-.868 17.564-1.304
18.651l-.649
2.166-11.276-3.685c-23.204-7.373-60.935-18.435-93.245-27.541-18.436-4.988-33.395-9.542-33.395-9.975
0-1.303 26.891-28.192 38.383-38.383 21.898-19.303 81.316-65.275
83.269-64.623zm14.963 2.166c.652-.647 89.779 14.746 130.331 22.554 30.145
5.854 73.948 14.963 76.549 16.049 1.301.432-3.254 3.034-17.784
9.539-57.248 25.808-99.754 49.008-142.036 77.202-11.06 7.373-20.386
13.444-20.602 13.444-.216 0-.433-6.287-.433-13.878
0-41.201-8.241-82.838-23.424-117.968-1.517-3.47-2.818-6.722-2.601-6.942zm230.516
45.542c.652.65-2.169 18.217-4.771 28.624-7.806 32.312-28.84 80.24-54.643
125.343-4.558 8.024-8.677 14.53-9.114
14.746-.429.216-6.285-3.038-13.009-6.941-25.154-14.746-53.778-28.624-85.007-41.637-8.671-3.685-16.263-6.723-16.48-7.153-1.521-1.303
68.308-47.493 105.174-69.612 29.276-17.781 76.982-44.239
77.85-43.37zm16.48 2.601c1.953 0 41.421 10.844 62.019 16.916 50.963 15.181
109.512 36.648 147.679 53.996l15.828 7.159-11.056 2.6c-93.245
21.467-173.049 46.192-250.034 77.418-6.289 2.602-11.928 4.771-12.357
4.771-.436 0 1.733-4.987 4.552-11.061 23.204-49.225 38.167-100.62
41.85-144.427.221-4.121.867-7.372 1.519-7.372zm-392.938 90.213c.649-.652
30.793 6.506 47.057 11.056 24.721 6.942 77.198 24.505 77.198 25.808 0
.216-5.853 5.204-12.79 11.278-28.408 23.637-55.734 48.572-88.481
80.234-9.759 9.328-17.997 16.917-18.429 16.917-.436
0-.649-1.304-.436-3.038 4.987-36.433
3.906-83.272-3.034-130.763-.653-6.074-1.302-11.276-1.085-11.492zm633.433.652c.429.431-13.881
22.984-22.988 35.777-13.009 18.649-32.098 43.375-75.252 97.588-22.765
28.622-48.358 60.936-56.812 71.778-8.678 10.842-15.831 19.948-16.051
19.948-.216
0-3.031-3.901-6.069-8.671-24.289-36.433-53.349-68.311-87.829-96.935-6.505-5.423-13.658-11.278-16.044-13.013-2.386-1.734-4.339-3.469-4.339-3.685
0-.649 36.862-16.483 64.841-27.757 49.01-19.952 115.794-43.805
165.892-59.203 26.24-8.239 54.215-16.263 54.651-15.827zm16.696
4.334c.865-.215 6.072 2.387 12.361 6.07 52.697 30.143 104.305 68.962
145.077 108.864 11.492 11.278 39.9 40.77 39.464 40.986 0
0-9.975.867-21.683 1.733-91.296 6.942-208.178 26.239-320.511 53.345-7.589
1.733-14.31 3.252-14.746 3.252-.429 0 8.025-8.456 18.653-18.647
65.922-63.538 96.067-103.656 131.628-175.22 4.986-10.623 9.325-19.731
9.757-20.383-.216 0-.216 0 0 0zm-482.936 49.446c3.038.647 31.229 13.88
52.48 24.503 19.517 9.755 48.794 25.372 50.311 26.671.216.216-10.195
5.638-22.984 11.928-40.772 20.384-75.684 39.682-112.118 61.802-10.408
6.29-19.082 11.497-19.298 11.497-.868 0-.652-.872 5.204-11.497
19.518-35.561 35.129-78.065 44.023-119.486.864-3.252 1.733-5.418
2.382-5.418zm-28.192 5.202c.652.652-6.721 27.323-11.273 41.853-8.894
27.541-23.856 62.02-38.383 88.043-3.474 6.069-8.677 14.961-11.496
19.948l-5.42
8.674-12.144-11.707c-14.094-13.663-25.59-22.12-40.333-29.712-5.859-3.033-10.411-5.638-10.411-6.069
0-1.735 37.082-35.347 65.49-59.635 20.383-17.566 63.321-52.045
63.97-51.395zm172.404 70.913l10.627 6.937c24.282 15.833 52.906 36.866
74.813 55.298 12.357 10.19 36.21 31.662 40.985 36.866l2.598 2.822-17.561
4.986c-99.321 27.538-176.087 52.043-265.649 85.007-9.975 3.685-18.433
6.721-19.085 6.721-1.297 0-2.385 1.083 19.954-19.519 57.251-52.691
107.992-110.812 145.726-167.411l7.592-11.707zm-45.324
11.276c.432.432-29.276 42.284-47.06 65.922-21.251 28.192-58.985
75.465-85.007 106.256-10.84 12.797-20.163 23.422-20.599
23.64-.652.216-.868-3.036-.868-8.024
0-26.242-6.721-54.216-18.433-78.068-4.988-9.975-5.856-12.361-4.768-13.444
4.119-3.688 67.223-39.686 107.123-61.153 26.89-14.312 68.956-35.563
69.612-35.129zm-274.107 67.225c.652 0 5.64 2.6 11.279 5.638 13.878 7.589
26.239 16.046 37.298 25.156.432.432-5.204 4.988-12.577 10.406-20.602
14.746-51.828 38.385-70.041 52.915-19.088 15.18-19.734 15.613-17.568
12.361 14.314-21.903 21.467-34.264 29.06-50.093 6.721-14.094 13.442-30.793
18.213-45.323 1.734-6.289 3.904-11.06 4.336-11.06zm73.083
57.248c1.081-.214 2.386 1.735 8.238 10.411 12.361 18.429 21.903 43.154
24.292 63.104l.429 4.339-29.705 11.494c-53.133 20.599-102.139
40.985-135.322 56.162-9.322 4.339-25.587 12.144-36.211 17.352-10.627
5.418-19.301 9.539-19.301 9.323s6.721-5.204 14.961-11.278c64.844-47.055
121.007-98.669 163.076-150.279 4.555-5.423 8.677-10.411
9.107-10.627l.436-.001zm-33.612 8.242c.868.867-23.853 28.84-40.768
45.971-41.853 42.723-83.273 76.12-134.669 108.649-6.505 4.119-12.359
7.804-13.011 8.24-1.519.867.432-1.303 22.986-25.808 14.314-15.397
25.155-28.408 37.516-44.453 8.24-10.624 9.759-12.143 21.688-20.604
31.878-22.987 105.39-72.864 106.258-71.995z"
/>
</g>
<path
fill="#231F1F"
d="M265.747 900.102c-2.276 0-4.553.217-6.809.217-45.975 2.45-76.983
22.683-95.113 62.195-15.506 35.735-13.813 82.446.174 118.4 16.265 35.131
42.547 53.672 86.416 60.675 9.282 1.52 15.506 6.616 33.483 27.606l22.12
25.915h40.118l-26.676-26.892c-14.746-14.745-26.673-27.584-26.673-28.712
0-1.127 5.641-3.599 12.469-5.68 22.51-6.812 41.203-24.202 54.279-50.854
10.583-21.402 12.102-28.018 13.619-54.646
3.969-79.26-37.82-128.813-107.409-128.247l.002.023zm35.173 207.27c-19.517
9.453-47.857 11.34-66.356
4.553-19.127-7.025-37.646-26.889-45.975-49.377-9.259-24.591-7.937-69.956
2.646-90.386 17.023-32.528 39.534-47.49 72.43-47.49 48.792 0 76.549 29.884
80.171 86.048 2.863 46.885-12.838 82.058-42.895
96.632l-.021.02zm693.025-139.568c-16.828 0-29.709 6.811-38.385 20.231l-6.809
10.627v-27.628h-29.123v165.678h29.104v-52.956c0-48.424.604-54.084
7.371-67.335 9.326-18.172 25.371-27.234 40.879-22.897l10.408
3.036v-28.712h-13.445v-.044zm-171.098-1.519c-5.705 0-11.756.76-17.781
2.084-38.971 10.19-60.938 47.489-59.594 85.873 0 32.139 6.244 48.206 21.752
65.057 31.77 26.065 60.502 28.146 99.275 14.161 6.615-2.819 13.814-6.072
13.814-6.072v-26.065l-13.814 7.156c-31.379 13.661-55.016
13.661-73.949-2.43-12.076-12.296-17.391-27.042-19.84-43.868h117.426v-22.339c0-45.539-27.41-74.294-67.313-73.557h.024zm-47.492
72.647s4.338-28.407 20.428-39.554c7.744-5.466 16.633-8.11 25.328-8.11 8.719
0 17.414 2.818 24.592 8.306 14.748 11.341 17.219 39.143 17.219
39.143h-87.566v.215h-.001zm-702.111-29.881c-31.573-19.128-45.582-32.921-43.869-49.185
4.9-44.997 60.503-38.773
91.295-21.749l.219-30.272s-17.024-7.373-41.421-7.764c-37.429-.564-61.63
11.709-72.97 36.691-16.656 36.865-1.908 64.665 51.396 95.677 29.925 17.412
43.152 32.528 43.152 49.008 0 34.047-41.05 45.931-83.401
24.57-8.716-4.337-16.09-7.959-16.48-7.959-1.519 9.651-.736 32.745-.736
32.745s13.012 5.466 32.527 9.236c48.4 9.65 92.445-13.054 96.608-49.919
3.622-34.609-8.893-52.761-56.318-81.104l-.002.025zm1178.454-43.155c-5.682
0-11.711.78-18 2.103-38.924 10.192-60.85 47.492-59.354 85.876 0 32.095 6.225
48.011 21.729 64.838 31.771 26.089 60.504 28.191 99.473 14.184 6.592-2.818
13.77-6.026 13.77-6.026v-26.109l-13.791 7.197c-31.443 13.619-55.082
13.619-73.947-2.471-12.145-12.274-17.414-26.847-19.865-43.871h117.232v-22.336c0-45.321-27.412-74.099-67.313-73.339l.066-.046zm-47.492
72.646s4.381-28.365 20.449-39.729c7.721-5.485 16.611-8.132 25.307-8.132
8.674 0 17.414 2.819 24.594 8.327 14.746 11.342 17.219 39.338 17.219
39.338h-87.545l-.024.196zm-533.809-29.123c-31.573-19.083-45.54-32.92-43.848-49.185
4.9-45.02 60.504-38.773
91.296-21.749l.218-30.272s-17.024-7.374-41.421-7.722c-37.429-.563-61.63
11.711-72.991 36.692-16.633 36.864-1.692 64.666 51.437 95.677 29.884 17.393
43.111 32.312 43.111 48.792 0 34.047-41.029 46.126-83.381
24.569-8.674-4.337-16.046-7.916-16.48-7.916-1.519 9.649-.736 32.746-.736
32.746s12.858 5.27 32.31 9.237c48.445 9.672 92.51-13.012 96.653-49.877
3.6-34.437-8.891-52.587-56.167-80.952v-.04zm752.421-42.005c-16.828 0-29.859
6.829-38.383 20.254l-6.811
10.582v-27.583h-29.123V1136.3h29.102v-52.954c0-48.403.584-54.085
7.375-67.313 9.324-18.15 25.369-27.235 40.875-22.878l10.408
3.035v-28.775h-13.443zm-984.021
41.05V902.941h-29.361v233.728h123.478v-27.604h-94.116v-100.601zm679.015
32.896l-24.201 62.975-23.27-63.322-23.637-70.173h-30.055c19.475 55.212
40.658 111.376 62.02 165.829 9.26.216 18.541 0 27.799 0l32.682-82.058
33.287-83.75h-28.732s-12.688 33.266-25.914 70.521l.021-.022zM506.455
839.251c4.728 0 8.674-1.516 11.927-4.769 3.208-3.211 4.9-6.984 4.9-11.711
0-4.728-1.692-8.675-4.9-11.711-3.253-3.035-7.005-4.555-11.711-4.555-4.769
0-8.717 1.52-11.927 4.728-3.252 3.211-4.727 7.158-4.727 11.712 0 4.771 1.519
8.716 4.727 11.711 3.037 3.034 6.984 4.553 11.711
4.553v.042zm-10.408-26.889c2.818-2.818 6.245-4.121 10.625-4.121 4.121 0
7.548 1.303 10.411 4.121 2.819 2.819 4.337 6.245 4.337 10.409 0 4.163-1.518
7.764-4.337 10.582-2.862 2.817-6.29 4.163-10.411 4.163-4.185
0-7.59-1.301-10.408-4.163-2.819-2.818-4.337-6.419-4.337-10.582 0-4.164
1.301-7.589 4.12-10.409zm7.003 11.928h1.908c1.346 0 2.668 1.3 3.795
3.773l2.279 5.116h3.577l-2.818-5.683c-1.149-2.275-2.276-3.598-3.6-3.969
1.67-.39 2.992-.953 3.947-2.082.952-.974 1.3-2.298 1.3-3.795
0-1.734-.542-3.034-1.69-3.989-1.302-1.084-3.384-1.669-6.074-1.669h-6.026v21.187h3.035v-8.891l.367.002zm0-9.846h2.647c1.908
0 3.253.39 3.99.953.716.564.911 1.303.911 2.646 0 2.45-1.52 3.601-4.337
3.601h-3.252v-7.2h.041zm-485.018
7.958c0-7.373-.216-12.858-.39-16.09h.174c.758 3.814 1.691 6.657 2.45
8.543l28.19 62.975h4.728l28.19-63.538c.761-1.733 1.52-4.337
2.452-7.959h.216c-.563 6.29-.758 11.754-.758
16.112v55.581h9.648v-82.622h-12.1L54.919 852.87c-.955 2.276-2.278
5.683-3.969
10.193h-.392c-.563-2.234-1.886-5.639-3.772-9.803l-25.33-58.053H8.598v82.621h9.281v-55.385l.153-.041zm96.045.154h8.329v51.458h-8.329v-51.458zm4.164-18.868c1.736
0 3.21-.587 4.337-1.734 1.15-1.129 1.91-2.603 1.91-4.337
0-1.692-.565-3.211-1.887-4.337-1.171-1.15-2.668-1.737-4.381-1.737-1.69
0-3.208.587-4.338 1.737-1.146 1.126-1.907 2.645-1.907 4.337 0 1.887.586
3.208 1.907 4.337 1.304 1.147 2.647 1.734 4.338 1.734h.021zm63.54
71.455v-9.066c-4.555 3.405-9.456 5.098-14.53 5.098-6.07
0-10.995-2.081-14.595-6.07-3.577-3.947-5.485-9.436-5.485-16.266 0-7.156
1.908-12.84 5.854-17.177 3.795-4.163 8.719-6.245 14.748-6.245 4.922 0 9.647
1.52 14.009 4.557v-9.65c-3.968-2.082-8.5-3.037-13.619-3.037-9.456 0-16.827
3.037-22.335 8.894-5.466 5.854-8.285 13.813-8.285 23.42 0 8.543 2.45 15.722
7.548 21.209 5.312 5.637 12.102 8.5 20.428 8.5 6.438-.178 11.707-1.523
16.262-4.167zm23.831-27.433c0-6.788 1.518-12.273 4.337-16.049 2.647-3.403
5.855-5.116 9.65-5.116 3.21 0 5.486.585 7.155
1.908v-9.846c-1.3-.563-3.187-.758-5.637-.758-3.405 0-6.439 1.146-9.107
3.253-2.819 2.231-5.074 5.638-6.397
9.975h-.216v-12.08h-9.433v58.985h9.454V847.71h.194zm54.279 31.443c8.892 0
16.048-2.863 21.36-8.543 5.29-5.641 7.936-13.229 7.936-22.686
0-9.647-2.427-17.021-7.372-22.51-4.9-5.483-11.711-8.132-20.603-8.132s-16.048
2.647-21.36 7.764c-5.681 5.641-8.674 13.599-8.674 23.813 0 8.891 2.429
16.265 7.548 21.751 5.29 5.68 12.295 8.521 21.165
8.521v.022zm-13.445-48.055c3.6-3.795 8.329-5.683 14.182-5.683 6.074 0 10.627
1.888 14.01 5.683 3.404 3.969 5.097 9.63 5.097 17.197 0 7.198-1.519
12.859-4.729 16.654-3.208 3.969-7.936 6.071-14.183 6.071-6.071
0-10.777-2.104-14.377-6.071-3.577-3.99-5.291-9.456-5.291-16.654-.368-7.156
1.519-13.01 5.291-17.197zm84.141 42.916c3.599-3.208 5.509-7.155 5.509-12.102
0-4.337-1.52-7.936-4.338-10.777-2.3-2.275-5.854-4.337-10.994-6.419-4.556-1.906-7.374-3.6-8.893-4.923-1.517-1.517-2.45-3.402-2.45-6.071
0-2.45.955-4.337 2.821-5.855 1.908-1.516 4.337-2.253 7.59-2.253 5.096 0
9.454 1.343 13.443 4.185v-9.456c-3.816-1.906-7.958-2.817-12.686-2.817-6.071
0-11.189 1.671-14.964 4.899-3.969 3.212-5.854 7.375-5.854 12.274 0 4.337 1.3
7.938 3.771 10.582 2.082 2.256 5.641 4.556 10.583 6.614 4.729 2.083 7.938
3.968 9.65 5.485 1.691 1.52 2.45 3.405 2.45 5.641 0 5.506-3.772 8.349-11.146
8.349-5.682 0-10.776-1.866-15.333-5.638v10.189c4.121 2.475 9.066 3.601 14.53
3.601 7.005-.368 12.49-2.081 16.264-5.486l.047-.022zm45.019-56.73c-8.893
0-16.048 2.647-21.361 7.764-5.638 5.641-8.674 13.599-8.674 23.813 0 8.891
2.452 16.265 7.547 21.751 5.313 5.68 12.295 8.521 21.187 8.521 9.107 0
16.048-2.861 21.36-8.545 5.313-5.637 7.958-13.227 7.958-22.683
0-9.65-2.472-17.022-7.374-22.509-5.115-5.487-11.927-8.133-20.601-8.133l-.042.021zm18.345
31.012c0 7.198-1.518 12.859-4.727 16.654-3.21 3.969-7.938 6.071-14.184
6.071-6.074 0-10.778-2.104-14.379-6.071-3.577-3.99-5.29-9.456-5.29-16.654
0-7.59 1.888-13.444 5.683-17.393 3.576-3.773 8.306-5.682 14.182-5.682 5.854
0 10.561 1.907 13.964 5.682 3.037 4.163 4.729 9.824 4.729
17.393h.022zm25.547 29.513h9.433v-51.068h13.813v-7.938H428.93v-9.108c0-8.282
3.208-12.446 9.845-12.446 2.234 0 4.511.563 6.203
1.518v-8.521c-1.692-.759-3.969-.932-6.812-.932-5.095 0-9.258 1.519-12.664
4.727-3.969 3.773-6.071 8.674-6.071
15.312v9.672h-9.978v7.936h9.978v50.876l.067-.028zm38.75-16.091c0 11.538
5.098 17.414 15.506 17.414 3.774 0 6.614-.606 8.891-1.951v-8.11c-1.734
1.302-3.795 1.91-6.071 1.91-3.208
0-5.464-.762-6.788-2.475-1.345-1.689-2.103-4.554-2.103-8.501v-33.286h14.961v-7.938h-14.961v-17.39c-3.253
1.127-6.44 2.082-9.456
3.034v14.355h-10.192v7.938h10.192v34.979l.021.021zm1014.88
108.73c-3.209-3.034-7.004-4.553-11.709-4.553-4.77 0-8.719 1.519-11.928
4.771-3.209 3.188-4.729 7.155-4.729 11.711 0 4.728 1.52 8.675 4.705 11.709
3.211 3.036 7.156 4.556 11.928 4.556 4.705 0 8.674-1.52 11.928-4.729
3.188-3.253 4.879-7.004
4.879-11.709-.174-4.771-1.887-8.719-5.096-11.754l.022-.002zm-1.517
22.338c-2.82 2.818-6.246 4.119-10.41 4.119-4.119
0-7.545-1.301-10.408-4.119-2.818-2.863-4.338-6.441-4.338-10.627 0-4.121
1.301-7.545 4.164-10.408 2.818-2.817 6.225-4.121 10.582-4.121 4.121 0 7.549
1.304 10.41 4.121 2.818 2.863 4.336 6.287 4.336 10.408 0 4.382-1.301
7.764-4.336 10.627zm-8.502-9.651c1.691-.39 3.037-1.149 3.969-2.081.955-.977
1.303-2.301 1.303-3.815
0-1.692-.543-3.037-1.691-3.969-1.301-1.085-3.404-1.671-6.07-1.671h-6.029v21.164h3.037v-8.891h1.885c1.303
0 2.604 1.3 3.773 3.773l2.254
5.096h3.602l-2.818-5.683c-.977-2.472-2.105-3.601-3.252-3.97l.037.047zm-2.082-1.907h-3.252v-7.155h2.668c1.887
0 3.209.345 3.969.932.758.563.932 1.301.932 2.646 0 2.45-1.518 3.579-4.336
3.579l.019-.002zM933.443
816.353h2.646v-21.187h7.002v-2.646h-16.652v2.646h7.006v21.187h-.002zm16.047-15.917c0-2.062
0-3.753-.152-4.705.174 1.126.564 1.887.738 2.45l8.133
18.172h1.301l8.152-18.347c.219-.563.393-1.301.76-2.275-.174 1.887-.174
3.401-.174 4.553v16.048h2.82V792.52h-3.406l-7.371 16.438c-.174.587-.762
1.734-1.129
3.037h-.217c-.152-.761-.541-1.519-1.084-2.818l-7.373-16.655h-3.816v23.854h2.666v-15.917l.152-.023z"
/>
</svg> </svg>

View File

@ -0,0 +1,68 @@
<script>
import { Combobox } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let value = ""
export let bindings = []
export let placeholder
export let label
export let disabled = false
export let options
export let appendBindingsAsOptions = true
export let error
const dispatch = createEventDispatcher()
$: readableValue = runtimeToReadableBinding(bindings, value)
$: isJS = isJSBinding(value)
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
const onChange = (value, optionPicked) => {
// Add HBS braces if picking binding
if (optionPicked && !options?.includes(value)) {
value = `{{ ${value} }}`
}
dispatch("change", readableToRuntimeBinding(bindings, value))
}
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
if (!appendBindingsAsOptions) {
return options
}
return []
.concat(options || [])
.concat(bindings?.map(binding => binding.readableBinding) || [])
}
</script>
<div class="control" class:disabled>
<Combobox
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:type={e => onChange(e.detail, false)}
on:pick={e => onChange(e.detail, true)}
on:blur={() => dispatch("blur")}
{placeholder}
options={allOptions}
{error}
/>
</div>
<style>
.control {
flex: 1;
position: relative;
}
.control:not(.disabled) :global(.spectrum-Textfield-input) {
padding-right: 40px;
}
</style>

View File

@ -18,6 +18,7 @@
export let options export let options
export let allowJS = true export let allowJS = true
export let appendBindingsAsOptions = true export let appendBindingsAsOptions = true
export let error
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
@ -59,8 +60,10 @@
value={isJS ? "(JavaScript function)" : readableValue} value={isJS ? "(JavaScript function)" : readableValue}
on:type={e => onChange(e.detail, false)} on:type={e => onChange(e.detail, false)}
on:pick={e => onChange(e.detail, true)} on:pick={e => onChange(e.detail, true)}
on:blur={() => dispatch("blur")}
{placeholder} {placeholder}
options={allOptions} options={allOptions}
{error}
/> />
{#if !disabled} {#if !disabled}
<div <div
@ -72,6 +75,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<Drawer bind:this={bindingDrawer} {title}> <Drawer bind:this={bindingDrawer} {title}>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Add the objects on the left to enrich your text. Add the objects on the left to enrich your text.

View File

@ -0,0 +1,6 @@
<script>
import dayjs from "dayjs"
export let value
</script>
{new dayjs(value).format("MMM D, YYYY HH:mm")}

View File

@ -52,7 +52,7 @@
reviewPendingDeployments(deployments, newDeployments) reviewPendingDeployments(deployments, newDeployments)
return newDeployments return newDeployments
} catch (err) { } catch (err) {
notifications.error("Error fetching deployment history") notifications.error("Error fetching deployment overview")
} }
} }

View File

@ -55,7 +55,7 @@
deployments = newDeployments deployments = newDeployments
} catch (err) { } catch (err) {
clearInterval(poll) clearInterval(poll)
notifications.error("Error fetching deployment history") notifications.error("Error fetching deployment overview")
} }
} }

View File

@ -178,7 +178,7 @@
.column { .column {
gap: var(--spacing-l); gap: var(--spacing-l);
display: grid; display: grid;
grid-template-columns: 20px 1fr 1fr auto auto; grid-template-columns: 20px 1fr 1fr 16px 16px;
align-items: center; align-items: center;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
transition: background-color ease-in-out 130ms; transition: background-color ease-in-out 130ms;

View File

@ -11,6 +11,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { lowercase } from "helpers" import { lowercase } from "helpers"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
@ -30,6 +31,7 @@
export let tooltip export let tooltip
export let menuItems export let menuItems
export let showMenu = false export let showMenu = false
export let bindings = []
let fields = Object.entries(object || {}).map(([name, value]) => ({ let fields = Object.entries(object || {}).map(([name, value]) => ({
name, name,
@ -108,6 +110,16 @@
/> />
{#if options} {#if options}
<Select bind:value={field.value} on:change={changed} {options} /> <Select bind:value={field.value} on:change={changed} {options} />
{:else if bindings && bindings.length}
<DrawerBindableInput
{bindings}
placeholder="Value"
on:change={e => (field.value = e.detail)}
disabled={readOnly}
value={field.value}
allowJS={false}
fillWidth={true}
/>
{:else} {:else}
<Input <Input
placeholder={valuePlaceholder} placeholder={valuePlaceholder}

View File

@ -57,7 +57,8 @@
placeholder="Default" placeholder="Default"
thin thin
disabled={bindable} disabled={bindable}
bind:value={binding.default} on:change={evt => onBindingChange(binding.name, evt.detail)}
value={runtimeToReadableBinding(bindings, binding.default)}
/> />
{#if bindable} {#if bindable}
<DrawerBindableInput <DrawerBindableInput

View File

@ -0,0 +1,91 @@
<script>
import { Layout, Icon, ActionButton } from "@budibase/bbui"
import StatusRenderer from "./StatusRenderer.svelte"
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
import TestDisplay from "components/automation/AutomationBuilder/TestDisplay.svelte"
import { goto } from "@roxi/routify"
import { automationStore } from "builderStore"
export let history
export let appId
export let close
$: exists = $automationStore.automations?.find(
auto => auto._id === history?.automationId
)
</script>
{#if history}
<div class="body">
<div class="top">
<div class="controls">
<StatusRenderer value={history.status} />
<ActionButton noPadding size="S" icon="Close" quiet on:click={close} />
</div>
</div>
<Layout paddingX="XL" gap="S">
<div class="icon">
<Icon name="Clock" />
<DateTimeRenderer value={history.createdAt} />
</div>
<div class="icon">
<Icon name="JourneyVoyager" />
<div>{history.automationName}</div>
</div>
<div>
{#if exists}
<ActionButton
icon="Edit"
fullWidth={false}
on:click={() =>
$goto(`../../../app/${appId}/automate/${history.automationId}`)}
>Edit automation</ActionButton
>
{/if}
</div>
</Layout>
<div class="bottom">
{#key history}
<TestDisplay testResults={history} width="100%" />
{/key}
</div>
</div>
{:else}
<div>No details found</div>
{/if}
<style>
.body {
right: 0;
background-color: var(--background);
border-left: var(--border-light);
width: 420px;
height: calc(100vh - 240px);
position: fixed;
overflow: auto;
}
.top {
padding: var(--spacing-m) 0 var(--spacing-m) 0;
border-bottom: var(--border-light);
}
.bottom {
margin-top: var(--spacing-m);
border-top: var(--border-light);
padding-top: calc(var(--spacing-xl) * 2);
padding-bottom: calc(var(--spacing-xl) * 2);
}
.icon {
display: flex;
gap: var(--spacing-m);
}
.controls {
padding: 0 var(--spacing-l) 0 var(--spacing-l);
display: grid;
grid-template-columns: 1fr auto;
gap: var(--spacing-s);
}
</style>

View File

@ -0,0 +1,219 @@
<script>
import { Layout, Table, Select, Pagination } from "@budibase/bbui"
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
import StatusRenderer from "./StatusRenderer.svelte"
import HistoryDetailsPanel from "./HistoryDetailsPanel.svelte"
import { automationStore } from "builderStore"
import { createPaginationStore } from "helpers/pagination"
import { onMount } from "svelte"
import dayjs from "dayjs"
const ERROR = "error",
SUCCESS = "success",
STOPPED = "stopped"
export let app
let pageInfo = createPaginationStore()
let runHistory = null
let showPanel = false
let selectedHistory = null
let automationOptions = []
let automationId = null
let status = null
let timeRange = null
$: page = $pageInfo.page
$: fetchLogs(automationId, status, page, timeRange)
const timeOptions = [
{ value: "1-w", label: "Past week" },
{ value: "1-d", label: "Past day" },
{ value: "1-h", label: "Past 1 hour" },
{ value: "15-m", label: "Past 15 mins" },
{ value: "5-m", label: "Past 5 mins" },
]
const statusOptions = [
{ value: SUCCESS, label: "Success" },
{ value: ERROR, label: "Error" },
{ value: STOPPED, label: "Stopped" },
]
const runHistorySchema = {
status: { displayName: "Status" },
createdAt: { displayName: "Time" },
automationName: { displayName: "Automation" },
}
const customRenderers = [
{ column: "createdAt", component: DateTimeRenderer },
{ column: "status", component: StatusRenderer },
]
async function fetchLogs(automationId, status, page, timeRange) {
let startDate = null
if (timeRange) {
const [length, units] = timeRange.split("-")
startDate = dayjs().subtract(length, units)
}
const response = await automationStore.actions.getLogs({
automationId,
status,
page,
startDate,
})
pageInfo.fetched(response.hasNextPage, response.nextPage)
runHistory = enrichHistory($automationStore.blockDefinitions, response.data)
}
function enrichHistory(definitions, runHistory) {
if (!definitions) {
return []
}
const finalHistory = []
for (let history of runHistory) {
if (!history.steps) {
continue
}
let notFound = false
for (let step of history.steps) {
const trigger = definitions.TRIGGER[step.stepId],
action = definitions.ACTION[step.stepId]
if (!trigger && !action) {
notFound = true
break
}
step.icon = trigger ? trigger.icon : action.icon
step.name = trigger ? trigger.name : action.name
}
if (!notFound) {
finalHistory.push(history)
}
}
return finalHistory
}
function viewDetails({ detail }) {
selectedHistory = detail
showPanel = true
}
onMount(async () => {
await automationStore.actions.fetch()
const params = new URLSearchParams(window.location.search)
const shouldOpen = params.get("open") === ERROR
// open with errors, open panel for latest
if (shouldOpen) {
status = ERROR
}
await automationStore.actions.fetch()
await fetchLogs(null, status)
if (shouldOpen) {
viewDetails({ detail: runHistory[0] })
}
automationOptions = []
for (let automation of $automationStore.automations) {
automationOptions.push({ value: automation._id, label: automation.name })
}
})
</script>
<div class="root" class:panelOpen={showPanel}>
<Layout paddingX="XL" gap="S" alignContent="start">
<div class="search">
<div class="select">
<Select
placeholder="All automations"
label="Automation"
bind:value={automationId}
options={automationOptions}
/>
</div>
<div class="select">
<Select
placeholder="Past 30 days"
label="Date range"
bind:value={timeRange}
options={timeOptions}
/>
</div>
<div class="select">
<Select
placeholder="All status"
label="Status"
bind:value={status}
options={statusOptions}
/>
</div>
</div>
{#if runHistory}
<Table
on:click={viewDetails}
schema={runHistorySchema}
allowSelectRows={false}
allowEditColumns={false}
allowEditRows={false}
data={runHistory}
{customRenderers}
placeholderText="No history found"
/>
{/if}
</Layout>
<div class="panel" class:panelShow={showPanel}>
<HistoryDetailsPanel
appId={app.devId}
bind:history={selectedHistory}
close={() => {
showPanel = false
}}
/>
</div>
</div>
<div class="pagination">
<Pagination
page={$pageInfo.pageNumber}
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
goToPrevPage={pageInfo.prevPage}
goToNextPage={pageInfo.nextPage}
/>
</div>
<style>
.root {
display: grid;
grid-template-columns: 1fr;
height: 100%;
}
.search {
display: flex;
gap: var(--spacing-l);
width: 100%;
align-items: flex-end;
}
.select {
flex-basis: 150px;
}
.pagination {
position: absolute;
bottom: 0;
margin-bottom: var(--spacing-xl);
margin-left: var(--spacing-l);
}
.panel {
display: none;
background-color: var(--background);
}
.panelShow {
display: block;
}
.panelOpen {
grid-template-columns: auto 420px;
}
</style>

View File

@ -0,0 +1,38 @@
<script>
import { Icon } from "@budibase/bbui"
export let value
$: isError = !value || value.toLowerCase() === "error"
$: isStopped = value?.toLowerCase() === "stopped"
$: status = getStatus(isError, isStopped)
function getStatus(error, stopped) {
if (error) {
return { color: "var(--red)", message: "Error", icon: "Alert" }
} else if (stopped) {
return { color: "var(--yellow)", message: "Stopped", icon: "StopCircle" }
} else {
return {
color: "var(--green)",
message: "Success",
icon: "CheckmarkCircle",
}
}
}
</script>
<div class="cell">
<Icon color={status.color} name={status.icon} />
<div style={`color: ${status.color};`}>
{status.message}
</div>
</div>
<style>
.cell {
display: flex;
flex-direction: row;
gap: var(--spacing-m);
align-items: center;
}
</style>

View File

@ -158,13 +158,9 @@ export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
opt => opt.type opt => opt.type
) )
export const ALLOWABLE_JSON_OPTIONS = [FIELDS.JSON, FIELDS.ARRAY]
export const ALLOWABLE_JSON_TYPES = ALLOWABLE_JSON_OPTIONS.map(opt => opt.type)
export const SWITCHABLE_TYPES = [ export const SWITCHABLE_TYPES = [
...ALLOWABLE_STRING_TYPES, ...ALLOWABLE_STRING_TYPES,
...ALLOWABLE_NUMBER_TYPES, ...ALLOWABLE_NUMBER_TYPES,
...ALLOWABLE_JSON_TYPES,
] ]
export const IntegrationTypes = { export const IntegrationTypes = {

View File

@ -5,6 +5,7 @@
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte" import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte" import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
import { onMount } from "svelte"
$: automation = $: automation =
$automationStore.selectedAutomation?.automation || $automationStore.selectedAutomation?.automation ||
@ -12,6 +13,9 @@
let modal let modal
let webhookModal let webhookModal
onMount(() => {
$automationStore.showTestPanel = false
})
</script> </script>
<!-- routify:options index=3 --> <!-- routify:options index=3 -->
@ -45,7 +49,7 @@
{/if} {/if}
</div> </div>
{#if automation?.showTestPanel} {#if $automationStore.showTestPanel}
<div class="setup"> <div class="setup">
<TestPanel {automation} /> <TestPanel {automation} />
</div> </div>

View File

@ -12,4 +12,6 @@
} }
</script> </script>
<slot /> {#key $params.selectedDatasource}
<slot />
{/key}

View File

@ -40,13 +40,39 @@
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { RawRestBodyTypes } from "constants/backend" import { RawRestBodyTypes } from "constants/backend"
import {
getRestBindings,
toBindingsArray,
runtimeToReadableBinding,
readableToRuntimeBinding,
runtimeToReadableMap,
readableToRuntimeMap,
} from "builderStore/dataBinding"
let query, datasource let query, datasource
let breakQs = {}, let breakQs = {},
bindings = {} requestBindings = {}
let saveId, url let saveId, url
let response, schema, enabledHeaders let response, schema, enabledHeaders
let authConfigId let authConfigId
let dynamicVariables, addVariableModal, varBinding let dynamicVariables, addVariableModal, varBinding
let restBindings = getRestBindings()
$: staticVariables = datasource?.config?.staticVariables || {}
$: customRequestBindings = toBindingsArray(requestBindings, "Binding")
$: dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
$: dataSourceStaticBindings = toBindingsArray(
staticVariables,
"Datasource.Static"
)
$: mergedBindings = [
...restBindings,
...customRequestBindings,
...dynamicRequestBindings,
...dataSourceStaticBindings,
]
$: datasourceType = datasource?.source $: datasourceType = datasource?.source
$: integrationInfo = $integrations[datasourceType] $: integrationInfo = $integrations[datasourceType]
@ -63,8 +89,10 @@
Object.keys(schema || {}).length !== 0 || Object.keys(schema || {}).length !== 0 ||
Object.keys(query?.schema || {}).length !== 0 Object.keys(query?.schema || {}).length !== 0
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
function getSelectedQuery() { function getSelectedQuery() {
return cloneDeep( const cloneQuery = cloneDeep(
$queries.list.find(q => q._id === $queries.selected) || { $queries.list.find(q => q._id === $queries.selected) || {
datasourceId: $params.selectedDatasource, datasourceId: $params.selectedDatasource,
parameters: [], parameters: [],
@ -76,6 +104,7 @@
queryVerb: "read", queryVerb: "read",
} }
) )
return cloneQuery
} }
function checkQueryName(inputUrl = null) { function checkQueryName(inputUrl = null) {
@ -89,7 +118,9 @@
if (!base) { if (!base) {
return base return base
} }
const qs = restUtils.buildQueryString(qsObj) const qs = restUtils.buildQueryString(
runtimeToReadableMap(mergedBindings, qsObj)
)
let newUrl = base let newUrl = base
if (base.includes("?")) { if (base.includes("?")) {
newUrl = base.split("?")[0] newUrl = base.split("?")[0]
@ -98,14 +129,21 @@
} }
function buildQuery() { function buildQuery() {
const newQuery = { ...query } const newQuery = cloneDeep(query)
const queryString = restUtils.buildQueryString(breakQs) const queryString = restUtils.buildQueryString(runtimeUrlQueries)
newQuery.parameters = restUtils.keyValueToQueryParameters(requestBindings)
newQuery.fields.requestBody =
typeof newQuery.fields.requestBody === "object"
? readableToRuntimeMap(mergedBindings, newQuery.fields.requestBody)
: readableToRuntimeBinding(mergedBindings, newQuery.fields.requestBody)
newQuery.fields.path = url.split("?")[0] newQuery.fields.path = url.split("?")[0]
newQuery.fields.queryString = queryString newQuery.fields.queryString = queryString
newQuery.fields.authConfigId = authConfigId newQuery.fields.authConfigId = authConfigId
newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders) newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
newQuery.schema = restUtils.fieldsToSchema(schema) newQuery.schema = restUtils.fieldsToSchema(schema)
newQuery.parameters = restUtils.keyValueToQueryParameters(bindings)
return newQuery return newQuery
} }
@ -120,6 +158,13 @@
datasource.config.dynamicVariables = rebuildVariables(saveId) datasource.config.dynamicVariables = rebuildVariables(saveId)
datasource = await datasources.save(datasource) datasource = await datasources.save(datasource)
} }
prettifyQueryRequestBody(
query,
requestBindings,
dynamicVariables,
staticVariables,
restBindings
)
} catch (err) { } catch (err) {
notifications.error(`Error saving query`) notifications.error(`Error saving query`)
} }
@ -127,7 +172,7 @@
async function runQuery() { async function runQuery() {
try { try {
response = await queries.preview(buildQuery(query)) response = await queries.preview(buildQuery())
if (response.rows.length === 0) { if (response.rows.length === 0) {
notifications.info("Request did not return any data") notifications.info("Request did not return any data")
} else { } else {
@ -236,6 +281,36 @@
} }
} }
const prettifyQueryRequestBody = (
query,
requestBindings,
dynamicVariables,
staticVariables,
restBindings
) => {
let customRequestBindings = toBindingsArray(requestBindings, "Binding")
let dynamicRequestBindings = toBindingsArray(dynamicVariables, "Dynamic")
let dataSourceStaticBindings = toBindingsArray(
staticVariables,
"Datasource.Static"
)
const prettyBindings = [
...restBindings,
...customRequestBindings,
...dynamicRequestBindings,
...dataSourceStaticBindings,
]
//Parse the body here as now all bindings have been updated.
if (query?.fields?.requestBody) {
query.fields.requestBody =
typeof query.fields.requestBody === "object"
? runtimeToReadableMap(prettyBindings, query.fields.requestBody)
: runtimeToReadableBinding(prettyBindings, query.fields.requestBody)
}
}
onMount(async () => { onMount(async () => {
query = getSelectedQuery() query = getSelectedQuery()
@ -250,6 +325,8 @@
const datasourceUrl = datasource?.config.url const datasourceUrl = datasource?.config.url
const qs = query?.fields.queryString const qs = query?.fields.queryString
breakQs = restUtils.breakQueryString(qs) breakQs = restUtils.breakQueryString(qs)
breakQs = runtimeToReadableMap(mergedBindings, breakQs)
const path = query.fields.path const path = query.fields.path
if ( if (
datasourceUrl && datasourceUrl &&
@ -260,7 +337,7 @@
} }
url = buildUrl(query.fields.path, breakQs) url = buildUrl(query.fields.path, breakQs)
schema = restUtils.schemaToFields(query.schema) schema = restUtils.schemaToFields(query.schema)
bindings = restUtils.queryParametersToKeyValue(query.parameters) requestBindings = restUtils.queryParametersToKeyValue(query.parameters)
authConfigId = getAuthConfigId() authConfigId = getAuthConfigId()
if (!query.fields.disabledHeaders) { if (!query.fields.disabledHeaders) {
query.fields.disabledHeaders = {} query.fields.disabledHeaders = {}
@ -291,6 +368,14 @@
query.fields.pagination = {} query.fields.pagination = {}
} }
dynamicVariables = getDynamicVariables(datasource, query._id) dynamicVariables = getDynamicVariables(datasource, query._id)
prettifyQueryRequestBody(
query,
requestBindings,
dynamicVariables,
staticVariables,
restBindings
)
}) })
</script> </script>
@ -344,16 +429,26 @@
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop> <Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
<Tab title="Bindings"> <Tab title="Bindings">
<KeyValueBuilder <KeyValueBuilder
bind:object={bindings} bind:object={requestBindings}
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query" tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
name="binding" name="binding"
headings headings
keyPlaceholder="Binding name" keyPlaceholder="Binding name"
valuePlaceholder="Default" valuePlaceholder="Default"
bindings={[
...restBindings,
...dynamicRequestBindings,
...dataSourceStaticBindings,
]}
/> />
</Tab> </Tab>
<Tab title="Params"> <Tab title="Params">
<KeyValueBuilder bind:object={breakQs} name="param" headings /> <KeyValueBuilder
bind:object={breakQs}
name="param"
headings
bindings={mergedBindings}
/>
</Tab> </Tab>
<Tab title="Headers"> <Tab title="Headers">
<KeyValueBuilder <KeyValueBuilder
@ -362,6 +457,7 @@
toggle toggle
name="header" name="header"
headings headings
bindings={mergedBindings}
/> />
</Tab> </Tab>
<Tab title="Body"> <Tab title="Body">

View File

@ -7,6 +7,7 @@
Modal, Modal,
Page, Page,
notifications, notifications,
Notification,
Body, Body,
Search, Search,
} from "@budibase/bbui" } from "@budibase/bbui"
@ -37,6 +38,7 @@
let searchTerm = "" let searchTerm = ""
let cloud = $admin.cloud let cloud = $admin.cloud
let creatingFromTemplate = false let creatingFromTemplate = false
let automationErrors
const resolveWelcomeMessage = (auth, apps) => { const resolveWelcomeMessage = (auth, apps) => {
const userWelcome = auth?.user?.firstName const userWelcome = auth?.user?.firstName
@ -59,7 +61,8 @@
) )
$: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther) $: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther)
$: unlocked = lockedApps?.length == 0 $: unlocked = lockedApps?.length === 0
$: automationErrors = getAutomationErrors(enrichedApps)
const enrichApps = (apps, user, sortBy) => { const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({ const enrichedApps = apps.map(app => ({
@ -89,6 +92,36 @@
} }
} }
const getAutomationErrors = apps => {
const automationErrors = {}
for (let app of apps) {
if (app.automationErrors) {
if (errorCount(app.automationErrors) > 0) {
automationErrors[app.devId] = app.automationErrors
}
}
}
return automationErrors
}
const goToAutomationError = appId => {
const params = new URLSearchParams({
tab: "Automation History",
open: "error",
})
$goto(`../overview/${appId}?${params.toString()}`)
}
const errorCount = errors => {
return Object.values(errors).reduce((acc, next) => acc + next.length, 0)
}
const automationErrorMessage = appId => {
const app = enrichedApps.find(app => app.devId === appId)
const errors = automationErrors[appId]
return `${app.name} - Automation error (${errorCount(errors)})`
}
const initiateAppCreation = () => { const initiateAppCreation = () => {
if ($apps?.length) { if ($apps?.length) {
$goto("/builder/portal/apps/create") $goto("/builder/portal/apps/create")
@ -208,6 +241,23 @@
<Page wide> <Page wide>
<Layout noPadding gap="M"> <Layout noPadding gap="M">
{#if loaded} {#if loaded}
{#each Object.keys(automationErrors || {}) as appId}
<Notification
wide
dismissable
action={() => goToAutomationError(appId)}
type="error"
icon="Alert"
actionMessage={errorCount(automationErrors[appId]) > 1
? "View errors"
: "View error"}
on:dismiss={async () => {
await automationStore.actions.clearLogErrors({ appId })
await apps.load()
}}
message={automationErrorMessage(appId)}
/>
{/each}
<div class="title"> <div class="title">
<div class="welcome"> <div class="welcome">
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">

View File

@ -74,7 +74,10 @@
rev: smtpConfig._rev, rev: smtpConfig._rev,
}) })
smtpConfig = { smtpConfig = {
config: {}, type: ConfigTypes.SMTP,
config: {
secure: true,
},
} }
await admin.getChecklist() await admin.getChecklist()
notifications.success(`Settings cleared`) notifications.success(`Settings cleared`)

View File

@ -27,13 +27,23 @@
} }
let pageInfo = createPaginationStore() let pageInfo = createPaginationStore()
let search = undefined let prevSearch = undefined,
search = undefined
$: page = $pageInfo.page $: page = $pageInfo.page
$: fetchUsers(page, search) $: fetchUsers(page, search)
let createUserModal let createUserModal
async function fetchUsers(page, search) { async function fetchUsers(page, search) {
if ($pageInfo.loading) {
return
}
// need to remove the page if they've started searching
if (search && !prevSearch) {
pageInfo.reset()
page = undefined
}
prevSearch = search
try { try {
pageInfo.loading() pageInfo.loading()
await users.search({ page, search }) await users.search({ page, search })

View File

@ -28,6 +28,7 @@
import EditableIcon from "components/common/EditableIcon.svelte" import EditableIcon from "components/common/EditableIcon.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import BetaTab from "components/beta/BetaTab.svelte" import BetaTab from "components/beta/BetaTab.svelte"
import HistoryTab from "components/portal/overview/automation/HistoryTab.svelte"
import { checkIncomingDeploymentStatus } from "components/deploy/utils" import { checkIncomingDeploymentStatus } from "components/deploy/utils"
import { onDestroy, onMount } from "svelte" import { onDestroy, onMount } from "svelte"
@ -188,6 +189,10 @@
}) })
onMount(async () => { onMount(async () => {
const params = new URLSearchParams(window.location.search)
if (params.get("tab")) {
selectedTab = params.get("tab")
}
try { try {
if (!apps.length) { if (!apps.length) {
await apps.load() await apps.load()
@ -212,7 +217,7 @@
<ProgressCircle size="XL" /> <ProgressCircle size="XL" />
</div> </div>
{:then _} {:then _}
<Layout paddingX="XXL" paddingY="XXL" gap="XL"> <Layout paddingX="XXL" paddingY="XL" gap="L">
<span class="page-header" class:loaded> <span class="page-header" class:loaded>
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}> <ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
Back Back
@ -300,10 +305,12 @@
on:unpublish={e => unpublishApp(e.detail)} on:unpublish={e => unpublishApp(e.detail)}
/> />
</Tab> </Tab>
{#if false} {#if isPublished}
<Tab title="Automation History"> <Tab title="Automation History">
<div class="container">Automation History contents</div> <HistoryTab app={selectedApp} />
</Tab> </Tab>
{/if}
{#if false}
<Tab title="Backups"> <Tab title="Backups">
<div class="container">Backups contents</div> <div class="container">Backups contents</div>
</Tab> </Tab>

View File

@ -197,6 +197,7 @@
.overview-tab .top { .overview-tab .top {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.overview-tab .bottom { .overview-tab .bottom {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@ -214,29 +215,35 @@
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
} }
.status-text, .status-text,
.last-edit-text { .last-edit-text {
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
} }
.updated-by { .updated-by {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
} }
.succeeded :global(.icon) { .succeeded :global(.icon) {
color: var(--spectrum-global-color-green-600); color: var(--spectrum-global-color-green-600);
} }
.failed :global(.icon) { .failed :global(.icon) {
color: var( color: var(
--spectrum-semantic-negative-color-default, --spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500) var(--spectrum-global-color-red-500)
); );
} }
.metric-info { .metric-info {
display: flex; display: flex;
gap: var(--spacing-l); gap: var(--spacing-l);
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
} }
.version-status, .version-status,
.last-edit-text, .last-edit-text,
.status-text { .status-text {

View File

@ -64,6 +64,8 @@ export function createAuthStore() {
name: user.account?.name, name: user.account?.name,
user_id: user._id, user_id: user._id,
tenant: user.tenantId, tenant: user.tenantId,
admin: user?.admin?.global,
builder: user?.builder?.global,
"Company size": user.account?.size, "Company size": user.account?.size,
"Job role": user.account?.profession, "Job role": user.account?.profession,
}) })

View File

@ -2467,6 +2467,11 @@ dayjs@^1.10.4:
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
dayjs@^1.11.2:
version "1.11.2"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5"
integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==
debug@4, debug@4.3.2, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: debug@4, debug@4.3.2, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
version "4.3.2" version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
@ -2568,9 +2573,9 @@ diff@^4.0.1:
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diff@^5.0.0: diff@^5.0.0:
version "5.0.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
dir-glob@^3.0.1: dir-glob@^3.0.1:
version "3.0.1" version "3.0.1"
@ -3246,9 +3251,9 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
glob@^7.1.6: glob@^7.1.6:
version "7.2.2" version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.2.tgz#29deb38e1ef90f132d5958abe9c3ee8e87f3c318" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ== integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies: dependencies:
fs.realpath "^1.0.0" fs.realpath "^1.0.0"
inflight "^1.0.4" inflight "^1.0.4"
@ -6282,9 +6287,9 @@ yargs@^15.3.1, yargs@^15.4.1:
yargs-parser "^18.1.2" yargs-parser "^18.1.2"
yargs@^17.2.1: yargs@^17.2.1:
version "17.5.0" version "17.5.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.0.tgz#2706c5431f8c119002a2b106fc9f58b9bb9097a3" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e"
integrity sha512-3sLxVhbAB5OC8qvVRebCLWuouhwh/rswsiDYx3WGxajUk/l4G20SKfrKKFeNIHboUFt2JFgv2yfn+5cgOr/t5A== integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==
dependencies: dependencies:
cliui "^7.0.2" cliui "^7.0.2"
escalade "^3.1.1" escalade "^3.1.1"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.212-alpha.8", "version": "1.0.219",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {
@ -33,4 +33,4 @@
"devDependencies": { "devDependencies": {
"eslint": "^7.20.0" "eslint": "^7.20.0"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.212-alpha.8", "version": "1.0.219",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.212-alpha.8", "@budibase/bbui": "^1.0.219",
"@budibase/frontend-core": "^1.0.212-alpha.8", "@budibase/frontend-core": "^1.0.219",
"@budibase/string-templates": "^1.0.212-alpha.8", "@budibase/string-templates": "^1.0.219",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",
@ -58,4 +58,4 @@
"rollup-plugin-visualizer": "^5.5.4" "rollup-plugin-visualizer": "^5.5.4"
}, },
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
} }

View File

@ -61,7 +61,9 @@
clonedSchema = schema clonedSchema = schema
} else { } else {
allowedFields?.forEach(field => { allowedFields?.forEach(field => {
clonedSchema[field] = schema[field] if (schema[field]) {
clonedSchema[field] = schema[field]
}
}) })
} }
return Object.values(clonedSchema || {}) return Object.values(clonedSchema || {})

View File

@ -278,6 +278,9 @@ const notEqualHandler = (value, rule) => {
// Evaluates a regex constraint // Evaluates a regex constraint
const regexHandler = (value, rule) => { const regexHandler = (value, rule) => {
const regex = parseType(rule.value, "string") const regex = parseType(rule.value, "string")
if (!value) {
value = ""
}
return new RegExp(regex).test(value) return new RegExp(regex).test(value)
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.212-alpha.8", "version": "1.0.219",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.212-alpha.8", "@budibase/bbui": "^1.0.219",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }
} }

View File

@ -73,4 +73,39 @@ export const buildAutomationEndpoints = API => ({
url: `/api/automations/${automationId}/${automationRev}`, url: `/api/automations/${automationId}/${automationRev}`,
}) })
}, },
/**
* Get the logs for the app, or by automation ID.
* @param automationId The ID of the automation to get logs for.
* @param startDate An ISO date string to state the start of the date range.
* @param status The status, error or success.
* @param page The page to retrieve.
*/
getAutomationLogs: async ({ automationId, startDate, status, page }) => {
return await API.post({
url: "/api/automations/logs/search",
body: {
automationId,
startDate,
status,
page,
},
})
},
/**
* Clears automation log errors (which are creating notification) for
* automation or the app.
* @param automationId optional - the ID of the automation to clear errors for.
* @param appId The app ID to clear errors for.
*/
clearAutomationLogErrors: async ({ automationId, appId }) => {
return await API.delete({
url: "/api/automations/logs",
body: {
appId,
automationId,
},
})
},
}) })

View File

@ -15,6 +15,15 @@ module FetchMock {
}, },
}, },
json: async () => { json: async () => {
//x-www-form-encoded body is a URLSearchParams
//The call to stringify it leaves it blank
if (body?.opts?.body instanceof URLSearchParams) {
const paramArray = Array.from(body.opts.body.entries())
body.opts.body = paramArray.reduce((acc: any, pair: any) => {
acc[pair[0]] = pair[1]
return acc
}, {})
}
return body return body
}, },
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.212-alpha.8", "version": "1.0.219",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -77,11 +77,10 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "10.0.3", "@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "^1.0.212-alpha.8", "@budibase/backend-core": "^1.0.219",
"@budibase/client": "^1.0.212-alpha.8", "@budibase/client": "^1.0.219",
"@budibase/pro": "1.0.212-alpha.8", "@budibase/pro": "1.0.219",
"@budibase/string-templates": "^1.0.212-alpha.8", "@budibase/string-templates": "^1.0.219",
"@budibase/types": "^1.0.212-alpha.8",
"@bull-board/api": "3.7.0", "@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4", "@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",
@ -152,6 +151,7 @@
"@babel/core": "7.17.4", "@babel/core": "7.17.4",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/types": "^1.0.219",
"@jest/test-sequencer": "24.9.0", "@jest/test-sequencer": "24.9.0",
"@types/apidoc": "0.50.0", "@types/apidoc": "0.50.0",
"@types/bson": "4.2.0", "@types/bson": "4.2.0",
@ -196,4 +196,4 @@
"oracledb": "5.3.0" "oracledb": "5.3.0"
}, },
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
} }

View File

@ -56,6 +56,8 @@ async function init() {
DISABLE_THREADING: 1, DISABLE_THREADING: 1,
SERVICE: "app-service", SERVICE: "app-service",
DEPLOYMENT_ENVIRONMENT: "development", DEPLOYMENT_ENVIRONMENT: "development",
BB_ADMIN_USER_EMAIL: "",
BB_ADMIN_USER_PASSWORD: "",
} }
let envFile = "" let envFile = ""
Object.keys(envFileJson).forEach(key => { Object.keys(envFileJson).forEach(key => {

View File

@ -2137,6 +2137,10 @@
"query": { "query": {
"type": "object", "type": "object",
"properties": { "properties": {
"allOr": {
"type": "boolean",
"description": "Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used."
},
"string": { "string": {
"type": "object", "type": "object",
"example": { "example": {
@ -2155,12 +2159,12 @@
}, },
"range": { "range": {
"type": "object", "type": "object",
"description": "Searches within a range, the format of this must be columnName -> [low, high].", "description": "Searches within a range, the format of this must be in the format of an object with a \"low\" and \"high\" property.",
"example": { "example": {
"columnName1": [ "columnName1": {
10, "low": 10,
20 "high": 20
] }
} }
}, },
"equal": { "equal": {

View File

@ -1551,6 +1551,12 @@ paths:
query: query:
type: object type: object
properties: properties:
allOr:
type: boolean
description: Specifies that a row should be returned if it satisfies any of the
specified options, rather than requiring it to fulfill
all the search parameters. This defaults to false,
meaning AND logic will be used.
string: string:
type: object type: object
example: example:
@ -1566,12 +1572,12 @@ paths:
description: A fuzzy search, only supported by internal tables. description: A fuzzy search, only supported by internal tables.
range: range:
type: object type: object
description: Searches within a range, the format of this must be columnName -> description: Searches within a range, the format of this must be in the format
[low, high]. of an object with a "low" and "high" property.
example: example:
columnName1: columnName1:
- 10 low: 10
- 20 high: 20
equal: equal:
type: object type: object
description: Searches for rows that have a column value that is exactly the description: Searches for rows that have a column value that is exactly the

View File

@ -45,10 +45,12 @@ const { getTenantId, isMultiTenant } = require("@budibase/backend-core/tenancy")
import { syncGlobalUsers } from "./user" import { syncGlobalUsers } from "./user"
const { app: appCache } = require("@budibase/backend-core/cache") const { app: appCache } = require("@budibase/backend-core/cache")
import { cleanupAutomations } from "../../automations/utils" import { cleanupAutomations } from "../../automations/utils"
import { checkAppMetadata } from "../../automations/logging"
const { const {
getAppDB, getAppDB,
getProdAppDB, getProdAppDB,
updateAppId, updateAppId,
doInAppContext,
} = require("@budibase/backend-core/context") } = require("@budibase/backend-core/context")
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
@ -192,7 +194,7 @@ export const fetch = async (ctx: any) => {
} }
} }
ctx.body = apps ctx.body = await checkAppMetadata(apps)
} }
export const fetchAppDefinition = async (ctx: any) => { export const fetchAppDefinition = async (ctx: any) => {
@ -558,20 +560,22 @@ export const sync = async (ctx: any, next: any) => {
} }
const updateAppPackage = async (appPackage: any, appId: any) => { const updateAppPackage = async (appPackage: any, appId: any) => {
const db = getAppDB() return doInAppContext(appId, async () => {
const application = await db.get(DocumentTypes.APP_METADATA) const db = getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA)
const newAppPackage = { ...application, ...appPackage } const newAppPackage = { ...application, ...appPackage }
if (appPackage._rev !== application._rev) { if (appPackage._rev !== application._rev) {
newAppPackage._rev = application._rev newAppPackage._rev = application._rev
} }
// the locked by property is attached by server but generated from // the locked by property is attached by server but generated from
// Redis, shouldn't ever store it // Redis, shouldn't ever store it
delete newAppPackage.lockedBy delete newAppPackage.lockedBy
await db.put(newAppPackage) await db.put(newAppPackage)
// remove any cached metadata, so that it will be updated // remove any cached metadata, so that it will be updated
await appCache.invalidateAppMetadata(appId) await appCache.invalidateAppMetadata(appId)
return newAppPackage return newAppPackage
})
} }

View File

@ -1,6 +1,10 @@
const actions = require("../../automations/actions") const actions = require("../../automations/actions")
const triggers = require("../../automations/triggers") const triggers = require("../../automations/triggers")
const { getAutomationParams, generateAutomationID } = require("../../db/utils") const {
getAutomationParams,
generateAutomationID,
DocumentTypes,
} = require("../../db/utils")
const { const {
checkForWebhooks, checkForWebhooks,
updateTestHistory, updateTestHistory,
@ -9,8 +13,14 @@ const {
const { deleteEntityMetadata } = require("../../utilities") const { deleteEntityMetadata } = require("../../utilities")
const { MetadataTypes } = require("../../constants") const { MetadataTypes } = require("../../constants")
const { setTestFlag, clearTestFlag } = require("../../utilities/redis") const { setTestFlag, clearTestFlag } = require("../../utilities/redis")
const { getAppDB } = require("@budibase/backend-core/context") const {
getAppDB,
getProdAppDB,
doInAppContext,
} = require("@budibase/backend-core/context")
const { events } = require("@budibase/backend-core") const { events } = require("@budibase/backend-core")
const { app } = require("@budibase/backend-core/cache")
const { automations } = require("@budibase/pro")
const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS) const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS)
const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS) const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS)
@ -183,6 +193,29 @@ exports.destroy = async function (ctx) {
await events.automation.deleted(oldAutomation) await events.automation.deleted(oldAutomation)
} }
exports.logSearch = async function (ctx) {
ctx.body = await automations.logs.logSearch(ctx.request.body)
}
exports.clearLogError = async function (ctx) {
const { automationId, appId } = ctx.request.body
await doInAppContext(appId, async () => {
const db = getProdAppDB()
const metadata = await db.get(DocumentTypes.APP_METADATA)
if (!automationId) {
delete metadata.automationErrors
} else if (
metadata.automationErrors &&
metadata.automationErrors[automationId]
) {
delete metadata.automationErrors[automationId]
}
await db.put(metadata)
await app.invalidateAppMetadata(metadata.appId, metadata)
ctx.body = { message: `Error logs cleared.` }
})
}
exports.getActionList = async function (ctx) { exports.getActionList = async function (ctx) {
ctx.body = ACTION_DEFS ctx.body = ACTION_DEFS
} }

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