Merge branch 'develop' of github.com:Budibase/budibase into labday/backups

This commit is contained in:
mike12345567 2022-07-05 17:47:22 +01:00
commit 228b2506cc
113 changed files with 2293 additions and 875 deletions

View File

@ -71,7 +71,7 @@ jobs:
- name: Upload to S3 - name: Upload to S3
if: github.ref == 'refs/heads/new-design-ui' if: github.ref == 'refs/heads/new-design-ui'
run: | run: |
tar -czvf new_ui.tar.gz packages/server/assets packages/server/index.html tar -czvf new_ui.tar.gz packages/server/builder/assets packages/server/builder/index.html
aws s3 cp new_ui.tar.gz s3://prod-budi-app-assets/beta:design_ui/ aws s3 cp new_ui.tar.gz s3://prod-budi-app-assets/beta:design_ui/
aws s3 cp packages/client/dist/budibase-client.js s3://prod-budi-app-assets/beta:design_ui/budibase-client.js aws s3 cp packages/client/dist/budibase-client.js s3://prod-budi-app-assets/beta:design_ui/budibase-client.js
aws cloudfront create-invalidation --distribution-id E3ELKP4RCEHVLW --paths "/beta:design_ui/*" aws cloudfront create-invalidation --distribution-id E3ELKP4RCEHVLW --paths "/beta:design_ui/*"

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

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.212-alpha.7", "version": "1.0.218",
"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.7", "@budibase/types": "^1.0.218",
"@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",

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,6 +15,7 @@ const {
tenancy, tenancy,
appTenancy, appTenancy,
authError, authError,
ssoCallbackUrl,
csrf, csrf,
internalApi, internalApi,
} = require("./middleware") } = require("./middleware")
@ -34,6 +38,122 @@ 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)
} 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 +166,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,41 +0,0 @@
exports.SEPARATOR = "_"
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 } 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,14 +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"
const UNICODE_MAX = "\ufff0"
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"
@ -63,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.
@ -93,13 +92,17 @@ export function generateGlobalUserID(id?: any) {
/** /**
* Gets parameters for retrieving users. * Gets parameters for retrieving users.
*/ */
export function getGlobalUserParams(globalId: any, otherProps = {}) { export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
if (!globalId) { if (!globalId) {
globalId = "" globalId = ""
} }
const startkey = otherProps?.startkey
return { return {
...otherProps, ...otherProps,
startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`, // need to include this incase pagination
startkey: startkey
? startkey
: `${DocumentTypes.USER}${SEPARATOR}${globalId}`,
endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
} }
} }
@ -385,7 +388,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 {
@ -394,7 +399,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(),
}, },
}, },
@ -435,6 +440,26 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => {
return platformUrl return platformUrl
} }
export function pagination(
data: any[],
pageSize: number,
{ paginate, property } = { paginate: true, property: "_id" }
) {
if (!paginate) {
return { data, hasNextPage: false }
}
const hasNextPage = data.length > pageSize
let nextPage = undefined
if (hasNextPage) {
nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id
}
return {
data: data.slice(0, pageSize),
hasNextPage,
nextPage,
}
}
export async function getScopedConfig(db: any, params: any) { export async function getScopedConfig(db: any, params: any) {
const configDoc = await getScopedFullConfig(db, params) const configDoc = await getScopedFullConfig(db, params)
return configDoc && configDoc.config ? configDoc.config : configDoc return configDoc && configDoc.config ? configDoc.config : configDoc

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

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

@ -1,5 +1,6 @@
const { ViewNames } = require("./db/utils") const { ViewNames } = require("./db/utils")
const { queryGlobalView } = require("./db/views") const { queryGlobalView } = require("./db/views")
const { UNICODE_MAX } = require("./db/constants")
/** /**
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
@ -19,3 +20,24 @@ exports.getGlobalUserByEmail = async email => {
return response return response
} }
/**
* Performs a starts with search on the global email view.
*/
exports.searchGlobalUsersByEmail = async (email, opts) => {
if (typeof email !== "string") {
throw new Error("Must provide a string to search by")
}
const lcEmail = email.toLowerCase()
// handle if passing up startkey for pagination
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
let response = await queryGlobalView(ViewNames.USER_BY_EMAIL, {
...opts,
startkey,
endkey: `${lcEmail}${UNICODE_MAX}`,
})
if (!response) {
response = []
}
return Array.isArray(response) ? response : [response]
}

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.7", "version": "1.0.218",
"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.7", "@budibase/string-templates": "^1.0.218",
"@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",

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
@ -80,4 +82,8 @@
.active svg { .active svg {
color: var(--spectrum-global-color-blue-600); color: var(--spectrum-global-color-blue-600);
} }
.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

@ -85,12 +85,12 @@ 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.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true })
cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true })
cy.get(interact.UNPUBLISH_MODAL) cy.get("[data-cy='publish-popover-menu']")
.within(() => { .within(() => {
cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true }) 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(() => {

View File

@ -52,13 +52,6 @@ filterTests(['smoke', 'all'], () => {
// 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.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true })
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.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(() => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.212-alpha.7", "version": "1.0.218",
"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.7", "@budibase/bbui": "^1.0.218",
"@budibase/client": "^1.0.212-alpha.7", "@budibase/client": "^1.0.218",
"@budibase/frontend-core": "^1.0.212-alpha.7", "@budibase/frontend-core": "^1.0.218",
"@budibase/string-templates": "^1.0.212-alpha.7", "@budibase/string-templates": "^1.0.218",
"@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",

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

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

@ -0,0 +1,66 @@
import { writable } from "svelte/store"
function defaultValue() {
return {
nextPage: null,
page: undefined,
hasPrevPage: false,
hasNextPage: false,
loading: false,
pageNumber: 1,
pages: [],
}
}
export function createPaginationStore() {
const { subscribe, set, update } = writable(defaultValue())
function prevPage() {
update(state => {
state.pageNumber--
state.nextPage = state.pages.pop()
state.page = state.pages[state.pages.length - 1]
state.hasPrevPage = state.pageNumber > 1
return state
})
}
function nextPage() {
update(state => {
state.pageNumber++
state.page = state.nextPage
state.pages.push(state.page)
state.hasPrevPage = state.pageNumber > 1
return state
})
}
function fetched(hasNextPage, nextPage) {
update(state => {
state.hasNextPage = hasNextPage
state.nextPage = nextPage
state.loading = false
return state
})
}
function loading(loading = true) {
update(state => {
state.loading = loading
return state
})
}
function reset() {
set(defaultValue())
}
return {
subscribe,
prevPage,
nextPage,
fetched,
loading,
reset,
}
}

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

@ -10,7 +10,9 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation" import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal" import { users } from "stores/portal"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
const password = Math.random().toString(36).substring(2, 22) const password = Math.random().toString(36).substring(2, 22)
const options = ["Email onboarding", "Basic onboarding"] const options = ["Email onboarding", "Basic onboarding"]
const [email, error, touched] = createValidationStore("", emailValidator) const [email, error, touched] = createValidationStore("", emailValidator)
@ -39,6 +41,7 @@
forceResetPassword: true, forceResetPassword: true,
}) })
notifications.success("Successfully created user") notifications.success("Successfully created user")
dispatch("created")
} catch (error) { } catch (error) {
notifications.error("Error creating user") notifications.error("Error creating user")
} }

View File

@ -12,41 +12,46 @@
Layout, Layout,
Modal, Modal,
notifications, notifications,
Pagination,
} from "@budibase/bbui" } from "@budibase/bbui"
import TagsRenderer from "./_components/TagsTableRenderer.svelte" import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import AddUserModal from "./_components/AddUserModal.svelte" import AddUserModal from "./_components/AddUserModal.svelte"
import { users } from "stores/portal" import { users } from "stores/portal"
import { onMount } from "svelte" import { createPaginationStore } from "helpers/pagination"
const schema = { const schema = {
email: {}, email: {},
developmentAccess: { displayName: "Development Access", type: "boolean" }, developmentAccess: { displayName: "Development Access", type: "boolean" },
adminAccess: { displayName: "Admin Access", type: "boolean" }, adminAccess: { displayName: "Admin Access", type: "boolean" },
// role: { type: "options" },
group: {}, group: {},
// access: {},
// group: {}
} }
let search let pageInfo = createPaginationStore()
$: filteredUsers = $users let prevSearch = undefined,
.filter(user => user.email.includes(search || "")) search = undefined
.map(user => ({ $: page = $pageInfo.page
...user, $: fetchUsers(page, search)
group: ["All users"],
developmentAccess: !!user.builder?.global,
adminAccess: !!user.admin?.global,
}))
let createUserModal let createUserModal
onMount(async () => { 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 {
await users.init() pageInfo.loading()
await users.search({ page, search })
pageInfo.fetched($users.hasNextPage, $users.nextPage)
} catch (error) { } catch (error) {
notifications.error("Error getting user list") notifications.error("Error getting user list")
} }
}) }
</script> </script>
<Layout noPadding> <Layout noPadding>
@ -75,17 +80,31 @@
<Table <Table
on:click={({ detail }) => $goto(`./${detail._id}`)} on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema} {schema}
data={filteredUsers || $users} data={$users.data}
allowEditColumns={false} allowEditColumns={false}
allowEditRows={false} allowEditRows={false}
allowSelectRows={false} allowSelectRows={false}
customRenderers={[{ column: "group", component: TagsRenderer }]} customRenderers={[{ column: "group", component: TagsRenderer }]}
/> />
<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>
</Layout> </Layout>
</Layout> </Layout>
<Modal bind:this={createUserModal}> <Modal bind:this={createUserModal}>
<AddUserModal /> <AddUserModal
on:created={async () => {
pageInfo.reset()
await fetchUsers()
}}
/>
</Modal> </Modal>
<style> <style>

View File

@ -27,6 +27,7 @@
import AppLockModal from "components/common/AppLockModal.svelte" import AppLockModal from "components/common/AppLockModal.svelte"
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 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"
@ -187,6 +188,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()
@ -211,7 +216,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
@ -299,10 +304,10 @@
on:unpublish={e => unpublishApp(e.detail)} on:unpublish={e => unpublishApp(e.detail)}
/> />
</Tab> </Tab>
<Tab title="Automation History">
<HistoryTab app={selectedApp} />
</Tab>
{#if false} {#if false}
<Tab title="Automation History">
<div class="container">Automation History contents</div>
</Tab>
<Tab title="Backups"> <Tab title="Backups">
<div class="container">Backups contents</div> <div class="container">Backups contents</div>
</Tab> </Tab>

View File

@ -1,14 +1,7 @@
<script> <script>
import DashCard from "components/common/DashCard.svelte" import DashCard from "components/common/DashCard.svelte"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import { import { Icon, Heading, Link, Avatar, Layout } from "@budibase/bbui"
Icon,
Heading,
Link,
Avatar,
notifications,
Layout,
} from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import clientPackage from "@budibase/client/package.json" import clientPackage from "@budibase/client/package.json"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
@ -20,29 +13,22 @@
export let navigateTab export let navigateTab
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const userInit = async () => {
try {
await users.init()
} catch (error) {
notifications.error("Error getting user list")
}
}
const unpublishApp = () => { const unpublishApp = () => {
dispatch("unpublish", app) dispatch("unpublish", app)
} }
let userPromise = userInit() let appEditor, appEditorPromise
$: updateAvailable = clientPackage.version !== $store.version $: updateAvailable = clientPackage.version !== $store.version
$: isPublished = app && app?.status === AppStatus.DEPLOYED $: isPublished = app && app?.status === AppStatus.DEPLOYED
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy $: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
$: appEditorText = appEditor?.firstName || appEditor?.email $: appEditorText = appEditor?.firstName || appEditor?.email
$: filteredUsers = !appEditorId $: fetchAppEditor(appEditorId)
? []
: $users.filter(user => user._id === appEditorId)
$: appEditor = filteredUsers.length ? filteredUsers[0] : null async function fetchAppEditor(editorId) {
appEditorPromise = users.get(editorId)
appEditor = await appEditorPromise
}
const getInitials = user => { const getInitials = user => {
let initials = "" let initials = ""
@ -90,7 +76,7 @@
</DashCard> </DashCard>
<DashCard title={"Last Edited"} dataCy={"edited-by"}> <DashCard title={"Last Edited"} dataCy={"edited-by"}>
<div class="last-edited-content"> <div class="last-edited-content">
{#await userPromise} {#await appEditorPromise}
<Avatar size="M" initials={"-"} /> <Avatar size="M" initials={"-"} />
{:then _} {:then _}
<div class="updated-by"> <div class="updated-by">
@ -211,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;
} }
@ -228,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

@ -3,11 +3,24 @@ import { API } from "api"
import { update } from "lodash" import { update } from "lodash"
export function createUsersStore() { export function createUsersStore() {
const { subscribe, set } = writable([]) const { subscribe, set } = writable({})
async function init() { // opts can contain page and search params
const users = await API.getUsers() async function search(opts = {}) {
set(users) const paged = await API.searchUsers(opts)
set({
...paged,
...opts,
})
return paged
}
async function get(userId) {
try {
return await API.getUser(userId)
} catch (err) {
return null
}
} }
async function invite({ email, builder, admin }) { async function invite({ email, builder, admin }) {
@ -47,7 +60,8 @@ export function createUsersStore() {
body.admin = { global: true } body.admin = { global: true }
} }
await API.saveUser(body) await API.saveUser(body)
await init() // re-search from first page
await search()
} }
async function del(id) { async function del(id) {
@ -61,7 +75,8 @@ export function createUsersStore() {
return { return {
subscribe, subscribe,
init, search,
get,
invite, invite,
acceptInvite, acceptInvite,
create, create,

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.7", "version": "1.0.218",
"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": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.212-alpha.7", "version": "1.0.218",
"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.7", "@budibase/bbui": "^1.0.218",
"@budibase/frontend-core": "^1.0.212-alpha.7", "@budibase/frontend-core": "^1.0.218",
"@budibase/string-templates": "^1.0.212-alpha.7", "@budibase/string-templates": "^1.0.218",
"@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",

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

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.212-alpha.7", "version": "1.0.218",
"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.7", "@budibase/bbui": "^1.0.218",
"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

@ -8,6 +8,34 @@ export const buildUserEndpoints = API => ({
}) })
}, },
/**
* Gets a list of users in the current tenant.
* @param {string} page The page to retrieve
* @param {string} search The starts with string to search username/email by.
*/
searchUsers: async ({ page, search } = {}) => {
const opts = {}
if (page) {
opts.page = page
}
if (search) {
opts.search = search
}
return await API.post({
url: `/api/global/users/search`,
body: opts,
})
},
/**
* Get a single user by ID.
*/
getUser: async userId => {
return await API.get({
url: `/api/global/users/${userId}`,
})
},
/** /**
* Creates a user for an app. * Creates a user for an app.
* @param user the user to create * @param user the user to create

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.7", "version": "1.0.218",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -77,11 +77,11 @@
"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.7", "@budibase/backend-core": "^1.0.218",
"@budibase/client": "^1.0.212-alpha.7", "@budibase/client": "^1.0.218",
"@budibase/pro": "1.0.212-alpha.7", "@budibase/pro": "1.0.218",
"@budibase/string-templates": "^1.0.212-alpha.7", "@budibase/string-templates": "^1.0.218",
"@budibase/types": "^1.0.212-alpha.7", "@budibase/types": "^1.0.218",
"@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",

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) => {
@ -548,22 +550,24 @@ 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
})
} }
const createEmptyAppPackage = async (ctx: any, app: any) => { const createEmptyAppPackage = async (ctx: any, app: any) => {

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

View File

@ -8,6 +8,8 @@ import { QUERY_THREAD_TIMEOUT } from "../../../environment"
import { getAppDB } from "@budibase/backend-core/context" import { getAppDB } from "@budibase/backend-core/context"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
import { getCookie } from "@budibase/backend-core/utils"
import { Cookies, Configs } from "@budibase/backend-core/constants"
const Runner = new Thread(ThreadType.QUERY, { const Runner = new Thread(ThreadType.QUERY, {
timeoutMs: QUERY_THREAD_TIMEOUT || 10000, timeoutMs: QUERY_THREAD_TIMEOUT || 10000,
@ -110,6 +112,21 @@ export async function find(ctx: any) {
ctx.body = query ctx.body = query
} }
//Required to discern between OIDC OAuth config entries
function getOAuthConfigCookieId(ctx: any) {
if (ctx.user.providerType === Configs.OIDC) {
return getCookie(ctx, Cookies.OIDC_CONFIG)
}
}
function getAuthConfig(ctx: any) {
const authCookie = getCookie(ctx, Cookies.Auth)
let authConfigCtx: any = {}
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
authConfigCtx["sessionId"] = authCookie ? authCookie.sessionId : null
return authConfigCtx
}
export async function preview(ctx: any) { export async function preview(ctx: any) {
const db = getAppDB() const db = getAppDB()
@ -119,6 +136,8 @@ export async function preview(ctx: any) {
// this stops dynamic variables from calling the same query // this stops dynamic variables from calling the same query
const { fields, parameters, queryVerb, transformer, queryId } = query const { fields, parameters, queryVerb, transformer, queryId } = query
const authConfigCtx: any = getAuthConfig(ctx)
try { try {
const runFn = () => const runFn = () =>
Runner.run({ Runner.run({
@ -129,8 +148,11 @@ export async function preview(ctx: any) {
parameters, parameters,
transformer, transformer,
queryId, queryId,
ctx: {
user: ctx.user,
auth: { ...authConfigCtx },
},
}) })
const { rows, keys, info, extra } = await quotas.addQuery(runFn) const { rows, keys, info, extra } = await quotas.addQuery(runFn)
await events.query.previewed(datasource, query) await events.query.previewed(datasource, query)
ctx.body = { ctx.body = {
@ -150,6 +172,8 @@ async function execute(ctx: any, opts = { rowsOnly: false }) {
const query = await db.get(ctx.params.queryId) const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId) const datasource = await db.get(query.datasourceId)
const authConfigCtx: any = getAuthConfig(ctx)
const enrichedParameters = ctx.request.body.parameters || {} const enrichedParameters = ctx.request.body.parameters || {}
// make sure parameters are fully enriched with defaults // make sure parameters are fully enriched with defaults
if (query && query.parameters) { if (query && query.parameters) {
@ -172,6 +196,10 @@ async function execute(ctx: any, opts = { rowsOnly: false }) {
parameters: enrichedParameters, parameters: enrichedParameters,
transformer: query.transformer, transformer: query.transformer,
queryId: ctx.params.queryId, queryId: ctx.params.queryId,
ctx: {
user: ctx.user,
auth: { ...authConfigCtx },
},
}) })
const { rows, pagination, extra } = await quotas.addQuery(runFn) const { rows, pagination, extra } = await quotas.addQuery(runFn)

View File

@ -72,7 +72,10 @@ router.use(async (ctx, next) => {
error, error,
} }
ctx.log.error(err) ctx.log.error(err)
console.trace(err) // unauthorised errors don't provide a useful trace
if (!env.isTest()) {
console.trace(err)
}
} }
}) })

View File

@ -51,6 +51,16 @@ router
automationValidator(false), automationValidator(false),
controller.create controller.create
) )
.post(
"/api/automations/logs/search",
authorized(BUILDER),
controller.logSearch
)
.delete(
"/api/automations/logs",
authorized(BUILDER),
controller.clearLogError
)
.delete( .delete(
"/api/automations/:id/:rev", "/api/automations/:id/:rev",
paramResource("id"), paramResource("id"),

View File

@ -346,4 +346,170 @@ describe("/queries", () => {
expect(contents).toBe(null) expect(contents).toBe(null)
}) })
}) })
describe("Current User Request Mapping", () => {
async function previewGet(datasource, fields, params) {
return config.previewQuery(request, config, datasource, fields, params)
}
async function previewPost(datasource, fields, params) {
return config.previewQuery(request, config, datasource, fields, params, "create")
}
it("should parse global and query level header mappings", async () => {
const userDetails = config.getUserDetails()
const datasource = await config.restDatasource({
defaultHeaders: {
"test": "headerVal",
"emailHdr": "{{[user].[email]}}"
}
})
const res = await previewGet(datasource, {
path: "www.google.com",
queryString: "email={{[user].[email]}}",
headers: {
queryHdr : "{{[user].[firstName]}}",
secondHdr : "1234"
}
})
const parsedRequest = JSON.parse(res.body.extra.raw)
expect(parsedRequest.opts.headers).toEqual({
"test": "headerVal",
"emailHdr": userDetails.email,
"queryHdr": userDetails.firstName,
"secondHdr" : "1234"
})
expect(res.body.rows[0].url).toEqual("http://www.google.com?email=" + userDetails.email)
})
it("should bind the current user to query parameters", async () => {
const userDetails = config.getUserDetails()
const datasource = await config.restDatasource()
const res = await previewGet(datasource, {
path: "www.google.com",
queryString: "test={{myEmail}}&testName={{myName}}&testParam={{testParam}}",
}, {
"myEmail" : "{{[user].[email]}}",
"myName" : "{{[user].[firstName]}}",
"testParam" : "1234"
})
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=" + userDetails.email +
"&testName=" + userDetails.firstName + "&testParam=1234")
})
it("should bind the current user the request body - plain text", async () => {
const userDetails = config.getUserDetails()
const datasource = await config.restDatasource()
const res = await previewPost(datasource, {
path: "www.google.com",
queryString: "testParam={{testParam}}",
requestBody: "This is plain text and this is my email: {{[user].[email]}}. This is a test param: {{testParam}}",
bodyType: "text"
}, {
"testParam" : "1234"
})
const parsedRequest = JSON.parse(res.body.extra.raw)
expect(parsedRequest.opts.body).toEqual(`This is plain text and this is my email: ${userDetails.email}. This is a test param: 1234`)
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
})
it("should bind the current user the request body - json", async () => {
const userDetails = config.getUserDetails()
const datasource = await config.restDatasource()
const res = await previewPost(datasource, {
path: "www.google.com",
queryString: "testParam={{testParam}}",
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
bodyType: "json"
}, {
"testParam" : "1234",
"userRef" : "{{[user].[firstName]}}"
})
const parsedRequest = JSON.parse(res.body.extra.raw)
const test = `{"email":"${userDetails.email}","queryCode":1234,"userRef":"${userDetails.firstName}"}`
expect(parsedRequest.opts.body).toEqual(test)
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
})
it("should bind the current user the request body - xml", async () => {
const userDetails = config.getUserDetails()
const datasource = await config.restDatasource()
const res = await previewPost(datasource, {
path: "www.google.com",
queryString: "testParam={{testParam}}",
requestBody: "<note> <email>{{[user].[email]}}</email> <code>{{testParam}}</code> " +
"<ref>{{userId}}</ref> <somestring>testing</somestring> </note>",
bodyType: "xml"
}, {
"testParam" : "1234",
"userId" : "{{[user].[firstName]}}"
})
const parsedRequest = JSON.parse(res.body.extra.raw)
const test = `<note> <email>${userDetails.email}</email> <code>1234</code> <ref>${userDetails.firstName}</ref> <somestring>testing</somestring> </note>`
expect(parsedRequest.opts.body).toEqual(test)
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
})
it("should bind the current user the request body - form-data", async () => {
const userDetails = config.getUserDetails()
const datasource = await config.restDatasource()
const res = await previewPost(datasource, {
path: "www.google.com",
queryString: "testParam={{testParam}}",
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
bodyType: "form"
}, {
"testParam" : "1234",
"userRef" : "{{[user].[firstName]}}"
})
const parsedRequest = JSON.parse(res.body.extra.raw)
const emailData = parsedRequest.opts.body._streams[1]
expect(emailData).toEqual(userDetails.email)
const queryCodeData = parsedRequest.opts.body._streams[4]
expect(queryCodeData).toEqual("1234")
const userRef = parsedRequest.opts.body._streams[7]
expect(userRef).toEqual(userDetails.firstName)
expect(res.body.rows[0].url).toEqual("http://www.google.com?testParam=1234")
})
it("should bind the current user the request body - encoded", async () => {
const userDetails = config.getUserDetails()
const datasource = await config.restDatasource()
const res = await previewPost(datasource, {
path: "www.google.com",
queryString: "testParam={{testParam}}",
requestBody: "{\"email\":\"{{[user].[email]}}\",\"queryCode\":{{testParam}},\"userRef\":\"{{userRef}}\"}",
bodyType: "encoded"
}, {
"testParam" : "1234",
"userRef" : "{{[user].[firstName]}}"
})
const parsedRequest = JSON.parse(res.body.extra.raw)
expect(parsedRequest.opts.body.email).toEqual(userDetails.email)
expect(parsedRequest.opts.body.queryCode).toEqual("1234")
expect(parsedRequest.opts.body.userRef).toEqual(userDetails.firstName)
})
});
}) })

View File

@ -0,0 +1,35 @@
import * as env from "../../environment"
import { AutomationResults, Automation, App } from "@budibase/types"
import { automations } from "@budibase/pro"
import { db as dbUtils } from "@budibase/backend-core"
export async function storeLog(
automation: Automation,
results: AutomationResults
) {
// can disable this if un-needed in self-host, also only do this for prod apps
if (env.DISABLE_AUTOMATION_LOGS) {
return
}
await automations.logs.storeLog(automation, results)
}
export async function checkAppMetadata(apps: App[]) {
const maxStartDate = await automations.logs.oldestLogDate()
for (let metadata of apps) {
if (!metadata.automationErrors) {
continue
}
for (let [key, errors] of Object.entries(metadata.automationErrors)) {
const updated = []
for (let error of errors) {
const startDate = error.split(dbUtils.SEPARATOR)[2]
if (startDate > maxStartDate) {
updated.push(error)
}
}
metadata.automationErrors[key] = updated
}
}
return apps
}

View File

@ -50,43 +50,51 @@ exports.definition = {
outputs: { outputs: {
properties: { properties: {
success: { success: {
type: "boolean",
description: "Whether the action was successful",
},
result: {
type: "boolean", type: "boolean",
description: "Whether the logic block passed", description: "Whether the logic block passed",
}, },
}, },
required: ["success"], required: ["success", "result"],
}, },
}, },
} }
exports.run = async function filter({ inputs }) { exports.run = async function filter({ inputs }) {
let { field, condition, value } = inputs try {
// coerce types so that we can use them let { field, condition, value } = inputs
if (!isNaN(value) && !isNaN(field)) { // coerce types so that we can use them
value = parseFloat(value) if (!isNaN(value) && !isNaN(field)) {
field = parseFloat(field) value = parseFloat(value)
} else if (!isNaN(Date.parse(value)) && !isNaN(Date.parse(field))) { field = parseFloat(field)
value = Date.parse(value) } else if (!isNaN(Date.parse(value)) && !isNaN(Date.parse(field))) {
field = Date.parse(field) value = Date.parse(value)
} field = Date.parse(field)
let success = false
if (typeof field !== "object" && typeof value !== "object") {
switch (condition) {
case FilterConditions.EQUAL:
success = field === value
break
case FilterConditions.NOT_EQUAL:
success = field !== value
break
case FilterConditions.GREATER_THAN:
success = field > value
break
case FilterConditions.LESS_THAN:
success = field < value
break
} }
} else { let result = false
success = false if (typeof field !== "object" && typeof value !== "object") {
switch (condition) {
case FilterConditions.EQUAL:
result = field === value
break
case FilterConditions.NOT_EQUAL:
result = field !== value
break
case FilterConditions.GREATER_THAN:
result = field > value
break
case FilterConditions.LESS_THAN:
result = field < value
break
}
} else {
result = false
}
return { success: true, result }
} catch (err) {
return { success: false, result: false }
} }
return { success }
} }

View File

@ -6,7 +6,8 @@ describe("test the filter logic", () => {
let res = await setup.runStep(setup.actions.FILTER.stepId, let res = await setup.runStep(setup.actions.FILTER.stepId,
{ field, condition, value } { field, condition, value }
) )
expect(res.success).toEqual(pass) expect(res.result).toEqual(pass)
expect(res.success).toEqual(true)
} }
it("should be able test equality", async () => { it("should be able test equality", async () => {

View File

@ -65,6 +65,7 @@ async function getLinksForRows(rows) {
// return duplicates, could be querying for both tables in a relation // return duplicates, could be querying for both tables in a relation
return getUniqueByProp( return getUniqueByProp(
responses responses
.filter(el => el != null)
// create a unique ID which we can use for getting only unique ones // create a unique ID which we can use for getting only unique ones
.map(el => ({ ...el, unique: el.id + el.thisId + el.fieldName })), .map(el => ({ ...el, unique: el.id + el.thisId + el.fieldName })),
"unique" "unique"

View File

@ -11,6 +11,8 @@ const {
isProdAppID, isProdAppID,
getDevelopmentAppID, getDevelopmentAppID,
generateAppID, generateAppID,
getQueryIndex,
ViewNames,
} = require("@budibase/backend-core/db") } = require("@budibase/backend-core/db")
const UNICODE_MAX = "\ufff0" const UNICODE_MAX = "\ufff0"
@ -22,11 +24,7 @@ const AppStatus = {
} }
const DocumentTypes = { const DocumentTypes = {
APP: CoreDocTypes.APP, ...CoreDocTypes,
DEV: CoreDocTypes.DEV,
APP_DEV: CoreDocTypes.APP_DEV,
APP_METADATA: CoreDocTypes.APP_METADATA,
ROLE: CoreDocTypes.ROLE,
TABLE: "ta", TABLE: "ta",
ROW: "ro", ROW: "ro",
USER: "us", USER: "us",
@ -45,11 +43,6 @@ const DocumentTypes = {
USER_FLAG: "flag", USER_FLAG: "flag",
} }
const ViewNames = {
LINK: "by_link",
ROUTING: "screen_routes",
}
const InternalTables = { const InternalTables = {
USER_METADATA: "ta_users", USER_METADATA: "ta_users",
} }
@ -89,9 +82,7 @@ exports.generateDevAppID = getDevelopmentAppID
exports.generateRoleID = generateRoleID exports.generateRoleID = generateRoleID
exports.getRoleParams = getRoleParams exports.getRoleParams = getRoleParams
exports.getQueryIndex = viewName => { exports.getQueryIndex = getQueryIndex
return `database/${viewName}`
}
/** /**
* If creating DB allDocs/query params with only a single top level ID this can be used, this * If creating DB allDocs/query params with only a single top level ID this can be used, this

View File

@ -78,6 +78,7 @@ module.exports = {
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS, ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
DISABLE_THREADING: process.env.DISABLE_THREADING, DISABLE_THREADING: process.env.DISABLE_THREADING,
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DISABLE_AUTOMATION_LOGS: process.env.DISABLE_AUTOMATION_LOGS,
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
SELF_HOSTED: process.env.SELF_HOSTED, SELF_HOSTED: process.env.SELF_HOSTED,

View File

@ -9,7 +9,9 @@ export function enrichQueryFields(
parameters = {} parameters = {}
) { ) {
const enrichedQuery: { [key: string]: any } = Array.isArray(fields) ? [] : {} const enrichedQuery: { [key: string]: any } = Array.isArray(fields) ? [] : {}
if (!fields || !parameters) {
return enrichedQuery
}
// enrich the fields with dynamic parameters // enrich the fields with dynamic parameters
for (let key of Object.keys(fields)) { for (let key of Object.keys(fields)) {
if (fields[key] == null) { if (fields[key] == null) {

View File

@ -287,7 +287,7 @@ module RestModule {
input.body = form input.body = form
break break
case BodyTypes.XML: case BodyTypes.XML:
if (object != null) { if (object != null && Object.keys(object).length) {
string = new XmlBuilder().buildObject(object) string = new XmlBuilder().buildObject(object)
} }
input.body = string input.body = string

View File

@ -155,12 +155,27 @@ describe("REST Integration", () => {
expect(output.headers["Content-Type"]).toEqual("application/json") expect(output.headers["Content-Type"]).toEqual("application/json")
}) })
it("should allow XML", () => { it("should allow raw XML", () => {
const output = config.integration.addBody("xml", "<a>1</a><b>2</b>", {})
expect(output.body.includes("<a>1</a>")).toEqual(true)
expect(output.body.includes("<b>2</b>")).toEqual(true)
expect(output.headers["Content-Type"]).toEqual("application/xml")
})
it("should allow a valid js object and parse the contents to xml", () => {
const output = config.integration.addBody("xml", input, {}) const output = config.integration.addBody("xml", input, {})
expect(output.body.includes("<a>1</a>")).toEqual(true) expect(output.body.includes("<a>1</a>")).toEqual(true)
expect(output.body.includes("<b>2</b>")).toEqual(true) expect(output.body.includes("<b>2</b>")).toEqual(true)
expect(output.headers["Content-Type"]).toEqual("application/xml") expect(output.headers["Content-Type"]).toEqual("application/xml")
}) })
it("should allow a valid json string and parse the contents to xml", () => {
const output = config.integration.addBody("xml", JSON.stringify(input), {})
expect(output.body.includes("<a>1</a>")).toEqual(true)
expect(output.body.includes("<b>2</b>")).toEqual(true)
expect(output.headers["Content-Type"]).toEqual("application/xml")
})
}) })
describe("response", () => { describe("response", () => {

View File

@ -93,8 +93,24 @@ describe("migrations", () => {
await clearMigrations() await clearMigrations()
const appId = config.prodAppId const appId = config.prodAppId
const roles = { [appId]: "role_12345" } const roles = { [appId]: "role_12345" }
await config.createUser(undefined, undefined, false, true, roles) // admin only await config.createUser(
await config.createUser(undefined, undefined, false, false, roles) // non admin non builder undefined,
undefined,
undefined,
undefined,
false,
true,
roles
) // admin only
await config.createUser(
undefined,
undefined,
undefined,
undefined,
false,
false,
roles
) // non admin non builder
await config.createTable() await config.createTable()
await config.createRow() await config.createRow()
await config.createRow() await config.createRow()

View File

@ -8,3 +8,5 @@ declare module "@budibase/backend-core/constants"
declare module "@budibase/backend-core/auth" declare module "@budibase/backend-core/auth"
declare module "@budibase/backend-core/sessions" declare module "@budibase/backend-core/sessions"
declare module "@budibase/backend-core/encryption" declare module "@budibase/backend-core/encryption"
declare module "@budibase/backend-core/utils"
declare module "@budibase/backend-core/redis"

View File

@ -28,6 +28,8 @@ const { encrypt } = require("@budibase/backend-core/encryption")
const GLOBAL_USER_ID = "us_uuid1" const GLOBAL_USER_ID = "us_uuid1"
const EMAIL = "babs@babs.com" const EMAIL = "babs@babs.com"
const FIRSTNAME = "Barbara"
const LASTNAME = "Barbington"
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
class TestConfiguration { class TestConfiguration {
@ -59,6 +61,15 @@ class TestConfiguration {
return this.prodAppId return this.prodAppId
} }
getUserDetails() {
return {
globalId: GLOBAL_USER_ID,
email: EMAIL,
firstName: FIRSTNAME,
lastName: LASTNAME,
}
}
async doInContext(appId, task) { async doInContext(appId, task) {
if (!appId) { if (!appId) {
appId = this.appId appId = this.appId
@ -118,6 +129,8 @@ class TestConfiguration {
// USER / AUTH // USER / AUTH
async globalUser({ async globalUser({
id = GLOBAL_USER_ID, id = GLOBAL_USER_ID,
firstName = FIRSTNAME,
lastName = LASTNAME,
builder = true, builder = true,
admin = false, admin = false,
email = EMAIL, email = EMAIL,
@ -135,6 +148,8 @@ class TestConfiguration {
...existing, ...existing,
roles: roles || {}, roles: roles || {},
tenantId: TENANT_ID, tenantId: TENANT_ID,
firstName,
lastName,
} }
await createASession(id, { await createASession(id, {
sessionId: "sessionid", sessionId: "sessionid",
@ -161,6 +176,8 @@ class TestConfiguration {
async createUser( async createUser(
id = null, id = null,
firstName = FIRSTNAME,
lastName = LASTNAME,
email = EMAIL, email = EMAIL,
builder = true, builder = true,
admin = false, admin = false,
@ -169,6 +186,8 @@ class TestConfiguration {
const globalId = !id ? `us_${Math.random()}` : `us_${id}` const globalId = !id ? `us_${Math.random()}` : `us_${id}`
const resp = await this.globalUser({ const resp = await this.globalUser({
id: globalId, id: globalId,
firstName,
lastName,
email, email,
builder, builder,
admin, admin,
@ -520,14 +539,14 @@ class TestConfiguration {
// QUERY // QUERY
async previewQuery(request, config, datasource, fields) { async previewQuery(request, config, datasource, fields, params, verb) {
return request return request
.post(`/api/queries/preview`) .post(`/api/queries/preview`)
.send({ .send({
datasourceId: datasource._id, datasourceId: datasource._id,
parameters: {}, parameters: params || {},
fields, fields,
queryVerb: "read", queryVerb: verb || "read",
name: datasource.name, name: datasource.name,
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())

View File

@ -9,11 +9,12 @@ const { doInTenant } = require("@budibase/backend-core/tenancy")
const { definitions: triggerDefs } = require("../automations/triggerInfo") const { definitions: triggerDefs } = require("../automations/triggerInfo")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const { AutomationErrors, LoopStepTypes } = require("../constants") const { AutomationErrors, LoopStepTypes } = require("../constants")
const { storeLog } = require("../automations/logging")
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
const CRON_STEP_ID = triggerDefs.CRON.stepId const CRON_STEP_ID = triggerDefs.CRON.stepId
const STOPPED_STATUS = { success: false, status: "STOPPED" } const STOPPED_STATUS = { success: true, status: "STOPPED" }
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
const env = require("../environment") const env = require("../environment")
@ -275,7 +276,7 @@ class Orchestrator {
this._context.steps[stepCount] = outputs this._context.steps[stepCount] = outputs
// if filter causes us to stop execution don't break the loop, set a var // if filter causes us to stop execution don't break the loop, set a var
// so that we can finish iterating through the steps and record that it stopped // so that we can finish iterating through the steps and record that it stopped
if (step.stepId === FILTER_STEP_ID && !outputs.success) { if (step.stepId === FILTER_STEP_ID && !outputs.result) {
stopped = true stopped = true
this.updateExecutionOutput(step.id, step.stepId, step.inputs, { this.updateExecutionOutput(step.id, step.stepId, step.inputs, {
...outputs, ...outputs,
@ -325,6 +326,8 @@ class Orchestrator {
} }
} }
// store the logs for the automation run
await storeLog(this._automation, this.executionOutput)
return this.executionOutput return this.executionOutput
} }
} }

View File

@ -77,7 +77,7 @@ export class Thread {
}) })
} }
static shutdown() { static stopThreads() {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
if (Thread.workerRefs.length === 0) { if (Thread.workerRefs.length === 0) {
resolve() resolve()
@ -95,4 +95,8 @@ export class Thread {
Thread.workerRefs = [] Thread.workerRefs = []
}) })
} }
static async shutdown() {
await Thread.stopThreads()
}
} }

View File

@ -4,6 +4,12 @@ const ScriptRunner = require("../utilities/scriptRunner")
const { integrations } = require("../integrations") const { integrations } = require("../integrations")
const { processStringSync } = require("@budibase/string-templates") const { processStringSync } = require("@budibase/string-templates")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const {
refreshOAuthToken,
updateUserOAuth,
} = require("@budibase/backend-core/auth")
const { getGlobalIDFromUserMetadataID } = require("../db/utils")
const { isSQL } = require("../integrations/utils") const { isSQL } = require("../integrations/utils")
const { const {
enrichQueryFields, enrichQueryFields,
@ -21,29 +27,56 @@ class QueryRunner {
this.queryId = input.queryId this.queryId = input.queryId
this.noRecursiveQuery = flags.noRecursiveQuery this.noRecursiveQuery = flags.noRecursiveQuery
this.cachedVariables = [] this.cachedVariables = []
// Additional context items for enrichment
this.ctx = input.ctx
// allows the response from a query to be stored throughout this // allows the response from a query to be stored throughout this
// execution so that if it needs to be re-used for another variable // execution so that if it needs to be re-used for another variable
// it can be // it can be
this.queryResponse = {} this.queryResponse = {}
this.hasRerun = false this.hasRerun = false
this.hasRefreshedOAuth = false
} }
async execute() { async execute() {
let { datasource, fields, queryVerb, transformer } = this let { datasource, fields, queryVerb, transformer } = this
const Integration = integrations[datasource.source] const Integration = integrations[datasource.source]
if (!Integration) { if (!Integration) {
throw "Integration type does not exist." throw "Integration type does not exist."
} }
if (datasource.config.authConfigs) {
datasource.config.authConfigs = datasource.config.authConfigs.map(
config => {
return enrichQueryFields(config, this.ctx)
}
)
}
const integration = new Integration(datasource.config) const integration = new Integration(datasource.config)
// pre-query, make sure datasource variables are added to parameters // pre-query, make sure datasource variables are added to parameters
const parameters = await this.addDatasourceVariables() const parameters = await this.addDatasourceVariables()
// Enrich the parameters with the addition context items.
// 'user' is now a reserved variable key in mapping parameters
const enrichedParameters = enrichQueryFields(parameters, this.ctx)
const enrichedContext = { ...enrichedParameters, ...this.ctx }
// Parse global headers
if (datasource.config.defaultHeaders) {
datasource.config.defaultHeaders = enrichQueryFields(
datasource.config.defaultHeaders,
enrichedContext
)
}
let query let query
// handle SQL injections by interpolating the variables // handle SQL injections by interpolating the variables
if (isSQL(datasource)) { if (isSQL(datasource)) {
query = interpolateSQL(fields, parameters, integration) query = interpolateSQL(fields, enrichedParameters, integration)
} else { } else {
query = enrichQueryFields(fields, parameters) query = enrichQueryFields(fields, enrichedContext)
} }
// Add pagination values for REST queries // Add pagination values for REST queries
@ -67,20 +100,30 @@ class QueryRunner {
if (transformer) { if (transformer) {
const runner = new ScriptRunner(transformer, { const runner = new ScriptRunner(transformer, {
data: rows, data: rows,
params: parameters, params: enrichedParameters,
}) })
rows = runner.execute() rows = runner.execute()
} }
// if the request fails we retry once, invalidating the cached value // if the request fails we retry once, invalidating the cached value
if ( if (info && info.code >= 400 && !this.hasRerun) {
info && if (
info.code >= 400 && this.ctx.user?.provider &&
this.cachedVariables.length > 0 && info.code === 401 &&
!this.hasRerun !this.hasRefreshedOAuth
) { ) {
// Attempt to refresh the access token from the provider
this.hasRefreshedOAuth = true
const authResponse = await this.refreshOAuth2(this.ctx)
if (!authResponse || authResponse.err) {
// In this event the user may have oAuth issues that
// could require re-authenticating with their provider.
throw new Error("OAuth2 access token could not be refreshed")
}
}
this.hasRerun = true this.hasRerun = true
// invalidate the cache value
await threadUtils.invalidateDynamicVariables(this.cachedVariables) await threadUtils.invalidateDynamicVariables(this.cachedVariables)
return this.execute() return this.execute()
} }
@ -126,6 +169,31 @@ class QueryRunner {
).execute() ).execute()
} }
async refreshOAuth2(ctx) {
const { oauth2, providerType, _id } = ctx.user
const { configId } = ctx.auth
if (!providerType || !oauth2?.refreshToken) {
console.error("No refresh token found for authenticated user")
return
}
const resp = await refreshOAuthToken(
oauth2.refreshToken,
providerType,
configId
)
// Refresh session flow. Should be in same location as refreshOAuthToken
// There are several other properties available in 'resp'
if (!resp.error) {
const globalUserId = getGlobalIDFromUserMetadataID(_id)
await updateUserOAuth(globalUserId, resp)
}
return resp
}
async getDynamicVariable(variable) { async getDynamicVariable(variable) {
let { parameters } = this let { parameters } = this
const queryId = variable.queryId, const queryId = variable.queryId,

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.212-alpha.7", "version": "1.0.218",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

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