Merge branch 'develop' into worker_typescript
This commit is contained in:
commit
995c36e8c7
|
@ -201,9 +201,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||||
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
<td align="center"><a href="https://github.com/seoulaja"><img src="https://avatars.githubusercontent.com/u/15101654?v=4?s=100" width="100px;" alt=""/><br /><sub><b>seoulaja</b></sub></a><br /><a href="#translation-seoulaja" title="Translation">🌍</a></td>
|
||||||
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/mslourens"><img src="https://avatars.githubusercontent.com/u/1907152?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maurits Lourens</b></sub></a><br /><a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=mslourens" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td align="center"><a href="https://github.com/Rory-Powell"><img src="https://avatars.githubusercontent.com/u/8755148?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rory Powell</b></sub></a><br /><a href="#infra-Rory-Powell" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Tests">⚠️</a> <a href="https://github.com/Budibase/budibase/commits?author=Rory-Powell" title="Code">💻</a></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- markdownlint-restore -->
|
<!-- markdownlint-restore -->
|
||||||
|
|
|
@ -99,13 +99,17 @@ spec:
|
||||||
- name: PLATFORM_URL
|
- name: PLATFORM_URL
|
||||||
value: {{ .Values.globals.platformUrl | quote }}
|
value: {{ .Values.globals.platformUrl | quote }}
|
||||||
- name: USE_QUOTAS
|
- name: USE_QUOTAS
|
||||||
value: "1"
|
value: {{ .Values.globals.useQuotas | quote }}
|
||||||
|
- name: EXCLUDE_QUOTAS_TENANTS
|
||||||
|
value: {{ .Values.globals.excludeQuotasTenants | quote }}
|
||||||
- name: ACCOUNT_PORTAL_URL
|
- name: ACCOUNT_PORTAL_URL
|
||||||
value: {{ .Values.globals.accountPortalUrl | quote }}
|
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||||
- name: ACCOUNT_PORTAL_API_KEY
|
- name: ACCOUNT_PORTAL_API_KEY
|
||||||
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
||||||
- name: COOKIE_DOMAIN
|
- name: COOKIE_DOMAIN
|
||||||
value: {{ .Values.globals.cookieDomain | quote }}
|
value: {{ .Values.globals.cookieDomain | quote }}
|
||||||
|
- name: HTTP_MIGRATIONS
|
||||||
|
value: {{ .Values.globals.httpMigrations | quote }}
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: bbapps
|
name: bbapps
|
||||||
|
|
|
@ -93,10 +93,13 @@ globals:
|
||||||
logLevel: info
|
logLevel: info
|
||||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||||
|
useQuotas: "0"
|
||||||
|
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
|
||||||
accountPortalUrl: ""
|
accountPortalUrl: ""
|
||||||
accountPortalApiKey: ""
|
accountPortalApiKey: ""
|
||||||
cookieDomain: ""
|
cookieDomain: ""
|
||||||
platformUrl: ""
|
platformUrl: ""
|
||||||
|
httpMigrations: "0"
|
||||||
|
|
||||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||||
|
|
||||||
|
@ -239,7 +242,8 @@ couchdb:
|
||||||
hosts:
|
hosts:
|
||||||
- chart-example.local
|
- chart-example.local
|
||||||
path: /
|
path: /
|
||||||
annotations: []
|
annotations:
|
||||||
|
[]
|
||||||
# kubernetes.io/ingress.class: nginx
|
# kubernetes.io/ingress.class: nginx
|
||||||
# kubernetes.io/tls-acme: "true"
|
# kubernetes.io/tls-acme: "true"
|
||||||
tls:
|
tls:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.44-alpha.9",
|
"version": "1.0.49-alpha.4",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...require("./src/db/utils"),
|
...require("./src/db/utils"),
|
||||||
...require("./src/db/constants"),
|
...require("./src/db/constants"),
|
||||||
|
...require("./src/db/views"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/migrations")
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.44-alpha.9",
|
"version": "1.0.49-alpha.4",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -12,6 +12,8 @@ const {
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
appTenancy,
|
||||||
authError,
|
authError,
|
||||||
|
csrf,
|
||||||
|
internalApi,
|
||||||
} = require("./middleware")
|
} = require("./middleware")
|
||||||
|
|
||||||
// Strategies
|
// Strategies
|
||||||
|
@ -42,4 +44,6 @@ module.exports = {
|
||||||
buildAppTenancyMiddleware: appTenancy,
|
buildAppTenancyMiddleware: appTenancy,
|
||||||
auditLog,
|
auditLog,
|
||||||
authError,
|
authError,
|
||||||
|
buildCsrfMiddleware: csrf,
|
||||||
|
internalApi,
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ exports.Cookies = {
|
||||||
CurrentApp: "budibase:currentapp",
|
CurrentApp: "budibase:currentapp",
|
||||||
Auth: "budibase:auth",
|
Auth: "budibase:auth",
|
||||||
Init: "budibase:init",
|
Init: "budibase:init",
|
||||||
|
DatasourceAuth: "budibase:datasourceauth",
|
||||||
OIDC_CONFIG: "budibase:oidc:config",
|
OIDC_CONFIG: "budibase:oidc:config",
|
||||||
RETURN_URL: "budibase:returnurl",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.Headers = {
|
exports.Headers = {
|
||||||
|
@ -18,6 +18,7 @@ exports.Headers = {
|
||||||
TYPE: "x-budibase-type",
|
TYPE: "x-budibase-type",
|
||||||
TENANT_ID: "x-budibase-tenant-id",
|
TENANT_ID: "x-budibase-tenant-id",
|
||||||
TOKEN: "x-budibase-token",
|
TOKEN: "x-budibase-token",
|
||||||
|
CSRF_TOKEN: "x-csrf-token",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.GlobalRoles = {
|
exports.GlobalRoles = {
|
||||||
|
|
|
@ -21,6 +21,7 @@ exports.StaticDatabases = {
|
||||||
name: "global-db",
|
name: "global-db",
|
||||||
docs: {
|
docs: {
|
||||||
apiKeys: "apikeys",
|
apiKeys: "apikeys",
|
||||||
|
usageQuota: "usage_quota",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// contains information about tenancy and so on
|
// contains information about tenancy and so on
|
||||||
|
@ -28,7 +29,6 @@ exports.StaticDatabases = {
|
||||||
name: "global-info",
|
name: "global-info",
|
||||||
docs: {
|
docs: {
|
||||||
tenants: "tenants",
|
tenants: "tenants",
|
||||||
usageQuota: "usage_quota",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -450,7 +450,7 @@ async function getScopedConfig(db, params) {
|
||||||
|
|
||||||
function generateNewUsageQuotaDoc() {
|
function generateNewUsageQuotaDoc() {
|
||||||
return {
|
return {
|
||||||
_id: StaticDatabases.PLATFORM_INFO.docs.usageQuota,
|
_id: StaticDatabases.GLOBAL.docs.usageQuota,
|
||||||
quotaReset: Date.now() + 2592000000,
|
quotaReset: Date.now() + 2592000000,
|
||||||
usageQuota: {
|
usageQuota: {
|
||||||
automationRuns: 0,
|
automationRuns: 0,
|
||||||
|
|
|
@ -14,4 +14,5 @@ module.exports = {
|
||||||
cache: require("../cache"),
|
cache: require("../cache"),
|
||||||
auth: require("../auth"),
|
auth: require("../auth"),
|
||||||
constants: require("../constants"),
|
constants: require("../constants"),
|
||||||
|
migrations: require("../migrations"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ module.exports = (
|
||||||
} else {
|
} else {
|
||||||
user = await getUser(userId, session.tenantId)
|
user = await getUser(userId, session.tenantId)
|
||||||
}
|
}
|
||||||
|
user.csrfToken = session.csrfToken
|
||||||
delete user.password
|
delete user.password
|
||||||
authenticated = true
|
authenticated = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
const { Headers } = require("../constants")
|
||||||
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET, HEAD and OPTIONS methods are considered safe operations
|
||||||
|
*
|
||||||
|
* POST, PUT, PATCH, and DELETE methods, being state changing verbs,
|
||||||
|
* should have a CSRF token attached to the request
|
||||||
|
*/
|
||||||
|
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are only three content type values that can be used in cross domain requests.
|
||||||
|
* If any other value is used, e.g. application/json, the browser will first make a OPTIONS
|
||||||
|
* request which will be protected by CORS.
|
||||||
|
*/
|
||||||
|
const INCLUDED_CONTENT_TYPES = [
|
||||||
|
"application/x-www-form-urlencoded",
|
||||||
|
"multipart/form-data",
|
||||||
|
"text/plain",
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the CSRF token generated aganst the user session.
|
||||||
|
* Compare the token with the x-csrf-token header.
|
||||||
|
*
|
||||||
|
* If the token is not found within the request or the value provided
|
||||||
|
* does not match the value within the user session, the request is rejected.
|
||||||
|
*
|
||||||
|
* CSRF protection provided using the 'Synchronizer Token Pattern'
|
||||||
|
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
module.exports = (opts = { noCsrfPatterns: [] }) => {
|
||||||
|
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
|
||||||
|
return async (ctx, next) => {
|
||||||
|
// don't apply for excluded paths
|
||||||
|
const found = matches(ctx, noCsrfOptions)
|
||||||
|
if (found) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't apply for the excluded http methods
|
||||||
|
if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't apply when the content type isn't supported
|
||||||
|
let contentType = ctx.get("content-type")
|
||||||
|
? ctx.get("content-type").toLowerCase()
|
||||||
|
: ""
|
||||||
|
if (
|
||||||
|
!INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length
|
||||||
|
) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't apply csrf when the internal api key has been used
|
||||||
|
if (ctx.internal) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply csrf when there is a token in the session (new logins)
|
||||||
|
// in future there should be a hard requirement that the token is present
|
||||||
|
const userToken = ctx.user.csrfToken
|
||||||
|
if (!userToken) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reject if no token in request or mismatch
|
||||||
|
const requestToken = ctx.get(Headers.CSRF_TOKEN)
|
||||||
|
if (!requestToken || requestToken !== userToken) {
|
||||||
|
ctx.throw(403, "Invalid CSRF token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,9 @@ const authenticated = require("./authenticated")
|
||||||
const auditLog = require("./auditLog")
|
const auditLog = require("./auditLog")
|
||||||
const tenancy = require("./tenancy")
|
const tenancy = require("./tenancy")
|
||||||
const appTenancy = require("./appTenancy")
|
const appTenancy = require("./appTenancy")
|
||||||
|
const internalApi = require("./internalApi")
|
||||||
|
const datasourceGoogle = require("./passport/datasource/google")
|
||||||
|
const csrf = require("./csrf")
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
google,
|
google,
|
||||||
|
@ -18,4 +21,9 @@ module.exports = {
|
||||||
tenancy,
|
tenancy,
|
||||||
appTenancy,
|
appTenancy,
|
||||||
authError,
|
authError,
|
||||||
|
internalApi,
|
||||||
|
datasource: {
|
||||||
|
google: datasourceGoogle,
|
||||||
|
},
|
||||||
|
csrf,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
const { Headers } = require("../constants")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key only endpoint.
|
||||||
|
*/
|
||||||
|
module.exports = async (ctx, next) => {
|
||||||
|
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||||
|
if (apiKey !== env.INTERNAL_API_KEY) {
|
||||||
|
ctx.throw(403, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
const { getScopedConfig } = require("../../../db/utils")
|
||||||
|
const { getGlobalDB } = require("../../../tenancy")
|
||||||
|
const google = require("../google")
|
||||||
|
const { Configs, Cookies } = require("../../../constants")
|
||||||
|
const { clearCookie, getCookie } = require("../../../utils")
|
||||||
|
const { getDB } = require("../../../db")
|
||||||
|
|
||||||
|
async function preAuth(passport, ctx, next) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
// get the relevant config
|
||||||
|
const config = await getScopedConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
workspace: ctx.query.workspace,
|
||||||
|
})
|
||||||
|
const publicConfig = await getScopedConfig(db, {
|
||||||
|
type: Configs.SETTINGS,
|
||||||
|
})
|
||||||
|
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
|
const strategy = await google.strategyFactory(config, callbackUrl)
|
||||||
|
|
||||||
|
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
||||||
|
ctx.throw(400, "appId and datasourceId query params not present.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return passport.authenticate(strategy, {
|
||||||
|
scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"],
|
||||||
|
accessType: "offline",
|
||||||
|
prompt: "consent",
|
||||||
|
})(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postAuth(passport, ctx, next) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
|
||||||
|
const config = await getScopedConfig(db, {
|
||||||
|
type: Configs.GOOGLE,
|
||||||
|
workspace: ctx.query.workspace,
|
||||||
|
})
|
||||||
|
|
||||||
|
const publicConfig = await getScopedConfig(db, {
|
||||||
|
type: Configs.SETTINGS,
|
||||||
|
})
|
||||||
|
|
||||||
|
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
|
||||||
|
const strategy = await google.strategyFactory(
|
||||||
|
config,
|
||||||
|
callbackUrl,
|
||||||
|
(accessToken, refreshToken, profile, done) => {
|
||||||
|
clearCookie(ctx, Cookies.DatasourceAuth)
|
||||||
|
done(null, { accessToken, refreshToken })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth)
|
||||||
|
|
||||||
|
return passport.authenticate(
|
||||||
|
strategy,
|
||||||
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
|
async (err, tokens) => {
|
||||||
|
// update the DB for the datasource with all the user info
|
||||||
|
const db = getDB(authStateCookie.appId)
|
||||||
|
const datasource = await db.get(authStateCookie.datasourceId)
|
||||||
|
if (!datasource.config) {
|
||||||
|
datasource.config = {}
|
||||||
|
}
|
||||||
|
datasource.config.auth = { type: "google", ...tokens }
|
||||||
|
await db.put(datasource)
|
||||||
|
ctx.redirect(
|
||||||
|
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)(ctx, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.preAuth = preAuth
|
||||||
|
exports.postAuth = postAuth
|
|
@ -1,18 +1,17 @@
|
||||||
|
const { DEFAULT_TENANT_ID } = require("../constants")
|
||||||
const { DocumentTypes } = require("../db/constants")
|
const { DocumentTypes } = require("../db/constants")
|
||||||
const { getGlobalDB } = require("../tenancy")
|
const { getAllApps } = require("../db/utils")
|
||||||
|
const environment = require("../environment")
|
||||||
|
const {
|
||||||
|
doInTenant,
|
||||||
|
getTenantIds,
|
||||||
|
getGlobalDBName,
|
||||||
|
getTenantId,
|
||||||
|
} = require("../tenancy")
|
||||||
|
|
||||||
exports.MIGRATION_DBS = {
|
exports.MIGRATION_TYPES = {
|
||||||
GLOBAL_DB: "GLOBAL_DB",
|
GLOBAL: "global", // run once, recorded in global db, global db is provided as an argument
|
||||||
}
|
APP: "app", // run per app, recorded in each app db, app db is provided as an argument
|
||||||
|
|
||||||
exports.MIGRATIONS = {
|
|
||||||
USER_EMAIL_VIEW_CASING: "user_email_view_casing",
|
|
||||||
}
|
|
||||||
|
|
||||||
const DB_LOOKUP = {
|
|
||||||
[exports.MIGRATION_DBS.GLOBAL_DB]: [
|
|
||||||
exports.MIGRATIONS.USER_EMAIL_VIEW_CASING,
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getMigrationsDoc = async db => {
|
exports.getMigrationsDoc = async db => {
|
||||||
|
@ -26,36 +25,90 @@ exports.getMigrationsDoc = async db => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => {
|
const runMigration = async (CouchDB, migration, options = {}) => {
|
||||||
try {
|
const tenantId = getTenantId()
|
||||||
let db
|
const migrationType = migration.type
|
||||||
if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) {
|
const migrationName = migration.name
|
||||||
db = getGlobalDB()
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unrecognised migration db [${migrationDb}]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!DB_LOOKUP[migrationDb].includes(migrationName)) {
|
// get the db to store the migration in
|
||||||
|
let dbNames
|
||||||
|
if (migrationType === exports.MIGRATION_TYPES.GLOBAL) {
|
||||||
|
dbNames = [getGlobalDBName()]
|
||||||
|
} else if (migrationType === exports.MIGRATION_TYPES.APP) {
|
||||||
|
const apps = await getAllApps(CouchDB, migration.opts)
|
||||||
|
dbNames = apps.map(app => app.appId)
|
||||||
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unrecognised migration name [${migrationName}] for db [${migrationDb}]`
|
`[Tenant: ${tenantId}] Unrecognised migration type [${migrationType}]`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run the migration against each db
|
||||||
|
for (const dbName of dbNames) {
|
||||||
|
const db = new CouchDB(dbName)
|
||||||
|
try {
|
||||||
const doc = await exports.getMigrationsDoc(db)
|
const doc = await exports.getMigrationsDoc(db)
|
||||||
// exit if the migration has been performed
|
|
||||||
|
// exit if the migration has been performed already
|
||||||
if (doc[migrationName]) {
|
if (doc[migrationName]) {
|
||||||
return
|
if (
|
||||||
|
options.force &&
|
||||||
|
options.force[migrationType] &&
|
||||||
|
options.force[migrationType].includes(migrationName)
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// the migration has already been performed
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Performing migration: ${migrationName}`)
|
console.log(
|
||||||
await migrateFn()
|
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running`
|
||||||
console.log(`Migration complete: ${migrationName}`)
|
)
|
||||||
|
// run the migration with tenant context
|
||||||
|
await migration.fn(db)
|
||||||
|
console.log(
|
||||||
|
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete`
|
||||||
|
)
|
||||||
|
|
||||||
// mark as complete
|
// mark as complete
|
||||||
doc[migrationName] = Date.now()
|
doc[migrationName] = Date.now()
|
||||||
await db.put(doc)
|
await db.put(doc)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error performing migration: ${migrationName}: `, err)
|
console.error(
|
||||||
|
`[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `,
|
||||||
|
err
|
||||||
|
)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.runMigrations = async (CouchDB, migrations, options = {}) => {
|
||||||
|
console.log("Running migrations")
|
||||||
|
let tenantIds
|
||||||
|
if (environment.MULTI_TENANCY) {
|
||||||
|
if (!options.tenantIds || !options.tenantIds.length) {
|
||||||
|
// run for all tenants
|
||||||
|
tenantIds = await getTenantIds()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// single tenancy
|
||||||
|
tenantIds = [DEFAULT_TENANT_ID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// for all tenants
|
||||||
|
for (const tenantId of tenantIds) {
|
||||||
|
// for all migrations
|
||||||
|
for (const migration of migrations) {
|
||||||
|
// run the migration
|
||||||
|
await doInTenant(tenantId, () =>
|
||||||
|
runMigration(CouchDB, migration, options)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Migrations complete")
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
exports[`migrations should match snapshot 1`] = `
|
exports[`migrations should match snapshot 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"_id": "migrations",
|
"_id": "migrations",
|
||||||
"_rev": "1-af6c272fe081efafecd2ea49a8fcbb40",
|
"_rev": "1-6277abc4e3db950221768e5a2618a059",
|
||||||
"user_email_view_casing": 1487076708000,
|
"test": 1487076708000,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require("../../tests/utilities/dbConfig")
|
require("../../tests/utilities/dbConfig")
|
||||||
|
|
||||||
const { migrateIfRequired, MIGRATION_DBS, MIGRATIONS, getMigrationsDoc } = require("../index")
|
const { runMigrations, getMigrationsDoc } = require("../index")
|
||||||
const database = require("../../db")
|
const CouchDB = require("../../db").getCouch()
|
||||||
const {
|
const {
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
|
@ -13,8 +13,14 @@ describe("migrations", () => {
|
||||||
|
|
||||||
const migrationFunction = jest.fn()
|
const migrationFunction = jest.fn()
|
||||||
|
|
||||||
|
const MIGRATIONS = [{
|
||||||
|
type: "global",
|
||||||
|
name: "test",
|
||||||
|
fn: migrationFunction
|
||||||
|
}]
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
db = database.getDB(StaticDatabases.GLOBAL.name)
|
db = new CouchDB(StaticDatabases.GLOBAL.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
@ -22,39 +28,29 @@ describe("migrations", () => {
|
||||||
await db.destroy()
|
await db.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
const validMigration = () => {
|
const migrate = () => {
|
||||||
return migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction)
|
return runMigrations(CouchDB, MIGRATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should run a new migration", async () => {
|
it("should run a new migration", async () => {
|
||||||
await validMigration()
|
await migrate()
|
||||||
expect(migrationFunction).toHaveBeenCalled()
|
expect(migrationFunction).toHaveBeenCalled()
|
||||||
|
const doc = await getMigrationsDoc(db)
|
||||||
|
expect(doc.test).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should match snapshot", async () => {
|
it("should match snapshot", async () => {
|
||||||
await validMigration()
|
await migrate()
|
||||||
const doc = await getMigrationsDoc(db)
|
const doc = await getMigrationsDoc(db)
|
||||||
expect(doc).toMatchSnapshot()
|
expect(doc).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should skip a previously run migration", async () => {
|
it("should skip a previously run migration", async () => {
|
||||||
await validMigration()
|
await migrate()
|
||||||
await validMigration()
|
const previousMigrationTime = await getMigrationsDoc(db).test
|
||||||
|
await migrate()
|
||||||
|
const currentMigrationTime = await getMigrationsDoc(db).test
|
||||||
expect(migrationFunction).toHaveBeenCalledTimes(1)
|
expect(migrationFunction).toHaveBeenCalledTimes(1)
|
||||||
|
expect(currentMigrationTime).toBe(previousMigrationTime)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should reject an unknown migration name", async () => {
|
|
||||||
expect(async () => {
|
|
||||||
await migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, "bogus_name", migrationFunction)
|
|
||||||
}).rejects.toThrow()
|
|
||||||
expect(migrationFunction).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should reject an unknown database name", async () => {
|
|
||||||
expect(async () => {
|
|
||||||
await migrateIfRequired("bogus_db", MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction)
|
|
||||||
}).rejects.toThrow()
|
|
||||||
expect(migrationFunction).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
})
|
|
@ -1,4 +1,5 @@
|
||||||
const redis = require("../redis/authRedis")
|
const redis = require("../redis/authRedis")
|
||||||
|
const { v4: uuidv4 } = require("uuid")
|
||||||
|
|
||||||
// a week in seconds
|
// a week in seconds
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = 86400 * 7
|
||||||
|
@ -16,6 +17,9 @@ function makeSessionID(userId, sessionId) {
|
||||||
exports.createASession = async (userId, session) => {
|
exports.createASession = async (userId, session) => {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const sessionId = session.sessionId
|
const sessionId = session.sessionId
|
||||||
|
if (!session.csrfToken) {
|
||||||
|
session.csrfToken = uuidv4()
|
||||||
|
}
|
||||||
session = {
|
session = {
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
lastAccessedAt: new Date().toISOString(),
|
lastAccessedAt: new Date().toISOString(),
|
||||||
|
|
|
@ -148,3 +148,15 @@ exports.isUserInAppTenant = (appId, user = null) => {
|
||||||
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
const tenantId = exports.getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||||
return tenantId === userTenantId
|
return tenantId === userTenantId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getTenantIds = async () => {
|
||||||
|
const db = getDB(PLATFORM_INFO_DB)
|
||||||
|
let tenants
|
||||||
|
try {
|
||||||
|
tenants = await db.get(TENANT_DOC)
|
||||||
|
} catch (err) {
|
||||||
|
// if theres an error the doc doesn't exist, no tenants exist
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return (tenants && tenants.tenantIds) || []
|
||||||
|
}
|
||||||
|
|
|
@ -20,9 +20,6 @@ const { hash } = require("./hashing")
|
||||||
const userCache = require("./cache/user")
|
const userCache = require("./cache/user")
|
||||||
const env = require("./environment")
|
const env = require("./environment")
|
||||||
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
const { getUserSessions, invalidateSessions } = require("./security/sessions")
|
||||||
const { migrateIfRequired } = require("./migrations")
|
|
||||||
const { USER_EMAIL_VIEW_CASING } = require("./migrations").MIGRATIONS
|
|
||||||
const { GLOBAL_DB } = require("./migrations").MIGRATION_DBS
|
|
||||||
|
|
||||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
|
|
||||||
|
@ -96,12 +93,7 @@ exports.getCookie = (ctx, name) => {
|
||||||
* @param {string|object} value The value of cookie which will be set.
|
* @param {string|object} value The value of cookie which will be set.
|
||||||
* @param {object} opts options like whether to sign.
|
* @param {object} opts options like whether to sign.
|
||||||
*/
|
*/
|
||||||
exports.setCookie = (
|
exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
|
||||||
ctx,
|
|
||||||
value,
|
|
||||||
name = "builder",
|
|
||||||
opts = { sign: true, requestDomain: false }
|
|
||||||
) => {
|
|
||||||
if (value && opts && opts.sign) {
|
if (value && opts && opts.sign) {
|
||||||
value = jwt.sign(value, options.secretOrKey)
|
value = jwt.sign(value, options.secretOrKey)
|
||||||
}
|
}
|
||||||
|
@ -113,7 +105,7 @@ exports.setCookie = (
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (environment.COOKIE_DOMAIN && !opts.requestDomain) {
|
if (environment.COOKIE_DOMAIN) {
|
||||||
config.domain = environment.COOKIE_DOMAIN
|
config.domain = environment.COOKIE_DOMAIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,11 +141,6 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
}
|
}
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
|
|
||||||
await migrateIfRequired(GLOBAL_DB, USER_EMAIL_VIEW_CASING, async () => {
|
|
||||||
// re-create the view with latest changes
|
|
||||||
await createUserEmailView(db)
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let users = (
|
let users = (
|
||||||
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
||||||
|
|
|
@ -3410,9 +3410,9 @@ node-fetch@2.6.0:
|
||||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||||
|
|
||||||
node-fetch@^2.6.1:
|
node-fetch@^2.6.1:
|
||||||
version "2.6.6"
|
version "2.6.7"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||||
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
|
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url "^5.0.0"
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
|
|
|
@ -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.44-alpha.9",
|
"version": "1.0.49-alpha.4",
|
||||||
"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",
|
||||||
|
|
|
@ -147,7 +147,9 @@
|
||||||
<img alt="preview" src={selectedUrl} />
|
<img alt="preview" src={selectedUrl} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="placeholder">
|
<div class="placeholder">
|
||||||
<div class="extension">{selectedImage.extension}</div>
|
<div class="extension">
|
||||||
|
{selectedImage.name || "Unknown file"}
|
||||||
|
</div>
|
||||||
<div>Preview not supported</div>
|
<div>Preview not supported</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -359,18 +361,21 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 0;
|
width: 0;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
user-select: all;
|
||||||
}
|
}
|
||||||
.placeholder {
|
.placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.extension {
|
.extension {
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
user-select: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
|
|
|
@ -6,11 +6,12 @@
|
||||||
export let label = null
|
export let label = null
|
||||||
export let labelPosition = "above"
|
export let labelPosition = "above"
|
||||||
export let error = null
|
export let error = null
|
||||||
|
export let tooltip = ""
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="spectrum-Form-item" class:above={labelPosition === "above"}>
|
<div class="spectrum-Form-item" class:above={labelPosition === "above"}>
|
||||||
{#if label}
|
{#if label}
|
||||||
<FieldLabel forId={id} {label} position={labelPosition} />
|
<FieldLabel forId={id} {label} position={labelPosition} {tooltip} />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="spectrum-Form-itemField">
|
<div class="spectrum-Form-itemField">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
|
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||||
|
|
||||||
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
||||||
|
|
||||||
export let forId
|
export let forId
|
||||||
export let label
|
export let label
|
||||||
export let position = "above"
|
export let position = "above"
|
||||||
|
export let tooltip = ""
|
||||||
|
|
||||||
$: className = position === "above" ? "" : `spectrum-FieldLabel--${position}`
|
$: className = position === "above" ? "" : `spectrum-FieldLabel--${position}`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<TooltipWrapper {tooltip} size="S">
|
||||||
<label
|
<label
|
||||||
for={forId}
|
for={forId}
|
||||||
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${className}`}
|
class={`spectrum-FieldLabel spectrum-FieldLabel--sizeM spectrum-Form-itemLabel ${className}`}
|
||||||
>
|
>
|
||||||
{label || ""}
|
{label || ""}
|
||||||
</label>
|
</label>
|
||||||
|
</TooltipWrapper>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
label {
|
label {
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let sort = false
|
export let sort = false
|
||||||
|
export let tooltip = ""
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {label} {labelPosition} {error}>
|
<Field {label} {labelPosition} {error} {tooltip}>
|
||||||
<Select
|
<Select
|
||||||
{quiet}
|
{quiet}
|
||||||
{error}
|
{error}
|
||||||
|
|
|
@ -1,73 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
import "@spectrum-css/fieldlabel/dist/index-vars.css"
|
||||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
|
||||||
import Icon from "../Icon/Icon.svelte"
|
|
||||||
|
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let tooltip = ""
|
export let tooltip = ""
|
||||||
export let showTooltip = false
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if tooltip}
|
<TooltipWrapper {tooltip} {size}>
|
||||||
<div class="container">
|
|
||||||
<label
|
|
||||||
for=""
|
|
||||||
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</label>
|
|
||||||
<div class="icon-container">
|
|
||||||
<div
|
|
||||||
class="icon"
|
|
||||||
class:icon-small={size === "M" || size === "S"}
|
|
||||||
on:mouseover={() => (showTooltip = true)}
|
|
||||||
on:mouseleave={() => (showTooltip = false)}
|
|
||||||
>
|
|
||||||
<Icon name="InfoOutline" size="S" disabled={true} />
|
|
||||||
</div>
|
|
||||||
{#if showTooltip}
|
|
||||||
<div class="tooltip">
|
|
||||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
|
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
|
||||||
<slot />
|
<slot />
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
</TooltipWrapper>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
label {
|
label {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.icon-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 1px;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
top: 15px;
|
|
||||||
z-index: 1;
|
|
||||||
width: 160px;
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
transform: scale(0.75);
|
|
||||||
}
|
|
||||||
.icon-small {
|
|
||||||
margin-top: -2px;
|
|
||||||
margin-bottom: -5px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script>
|
||||||
|
import Tooltip from "./Tooltip.svelte"
|
||||||
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
|
||||||
|
export let tooltip = ""
|
||||||
|
export let size = "M"
|
||||||
|
|
||||||
|
let showTooltip = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class:container={!!tooltip}>
|
||||||
|
<slot />
|
||||||
|
{#if tooltip}
|
||||||
|
<div class="icon-container">
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
class:icon-small={size === "M" || size === "S"}
|
||||||
|
on:mouseover={() => (showTooltip = true)}
|
||||||
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
>
|
||||||
|
<Icon name="InfoOutline" size="S" disabled={true} />
|
||||||
|
</div>
|
||||||
|
{#if showTooltip}
|
||||||
|
<div class="tooltip">
|
||||||
|
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.icon-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
top: 15px;
|
||||||
|
z-index: 1;
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
.icon-small {
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.44-alpha.9",
|
"version": "1.0.49-alpha.4",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.44-alpha.9",
|
"@budibase/bbui": "^1.0.49-alpha.4",
|
||||||
"@budibase/client": "^1.0.44-alpha.9",
|
"@budibase/client": "^1.0.49-alpha.4",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^1.0.44-alpha.9",
|
"@budibase/string-templates": "^1.0.49-alpha.4",
|
||||||
"@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",
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import { store } from "./index"
|
import { store } from "./index"
|
||||||
import { get as svelteGet } from "svelte/store"
|
import { get as svelteGet } from "svelte/store"
|
||||||
import { removeCookie, Cookies } from "./cookies"
|
import { removeCookie, Cookies } from "./cookies"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
const apiCall =
|
const apiCall =
|
||||||
method =>
|
method =>
|
||||||
async (url, body, headers = { "Content-Type": "application/json" }) => {
|
async (url, body, headers = { "Content-Type": "application/json" }) => {
|
||||||
headers["x-budibase-app-id"] = svelteGet(store).appId
|
headers["x-budibase-app-id"] = svelteGet(store).appId
|
||||||
|
headers["x-budibase-api-version"] = "1"
|
||||||
|
|
||||||
|
// add csrf token if authenticated
|
||||||
|
const user = svelteGet(auth).user
|
||||||
|
if (user && user.csrfToken) {
|
||||||
|
headers["x-csrf-token"] = user.csrfToken
|
||||||
|
}
|
||||||
|
|
||||||
const json = headers["Content-Type"] === "application/json"
|
const json = headers["Content-Type"] === "application/json"
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { getFrontendStore } from "./store/frontend"
|
import { getFrontendStore } from "./store/frontend"
|
||||||
import { getAutomationStore } from "./store/automation"
|
import { getAutomationStore } from "./store/automation"
|
||||||
import { getHostingStore } from "./store/hosting"
|
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
import { derived, writable } from "svelte/store"
|
import { derived, writable } from "svelte/store"
|
||||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||||
|
@ -9,7 +8,6 @@ import { findComponent } from "./componentUtils"
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
export const hostingStore = getHostingStore()
|
|
||||||
|
|
||||||
export const currentAsset = derived(store, $store => {
|
export const currentAsset = derived(store, $store => {
|
||||||
const type = $store.currentFrontEndType
|
const type = $store.currentFrontEndType
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { get, writable } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import {
|
import {
|
||||||
allScreens,
|
allScreens,
|
||||||
hostingStore,
|
|
||||||
currentAsset,
|
currentAsset,
|
||||||
mainLayout,
|
mainLayout,
|
||||||
selectedComponent,
|
selectedComponent,
|
||||||
|
@ -100,7 +99,6 @@ export const getFrontendStore = () => {
|
||||||
version: application.version,
|
version: application.version,
|
||||||
revertableVersion: application.revertableVersion,
|
revertableVersion: application.revertableVersion,
|
||||||
}))
|
}))
|
||||||
await hostingStore.actions.fetch()
|
|
||||||
|
|
||||||
// Initialise backend stores
|
// Initialise backend stores
|
||||||
const [_integrations] = await Promise.all([
|
const [_integrations] = await Promise.all([
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import api, { get } from "../api"
|
|
||||||
|
|
||||||
const INITIAL_HOSTING_UI_STATE = {
|
|
||||||
appUrl: "",
|
|
||||||
deployedApps: {},
|
|
||||||
deployedAppNames: [],
|
|
||||||
deployedAppUrls: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getHostingStore = () => {
|
|
||||||
const store = writable({ ...INITIAL_HOSTING_UI_STATE })
|
|
||||||
store.actions = {
|
|
||||||
fetch: async () => {
|
|
||||||
const response = await api.get("/api/hosting/urls")
|
|
||||||
const urls = await response.json()
|
|
||||||
store.update(state => {
|
|
||||||
state.appUrl = urls.app
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
fetchDeployedApps: async () => {
|
|
||||||
let deployments = await (await get("/api/hosting/apps")).json()
|
|
||||||
store.update(state => {
|
|
||||||
state.deployedApps = deployments
|
|
||||||
state.deployedAppNames = Object.values(deployments).map(app => app.name)
|
|
||||||
state.deployedAppUrls = Object.values(deployments).map(app => app.url)
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
return deployments
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return store
|
|
||||||
}
|
|
|
@ -22,8 +22,10 @@
|
||||||
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"
|
||||||
|
@ -150,6 +152,7 @@
|
||||||
delete field.subtype
|
delete field.subtype
|
||||||
delete field.tableId
|
delete field.tableId
|
||||||
delete field.relationshipType
|
delete field.relationshipType
|
||||||
|
delete field.formulaType
|
||||||
|
|
||||||
// Add in defaults and initial definition
|
// Add in defaults and initial definition
|
||||||
const definition = fieldDefinitions[event.detail?.toUpperCase()]
|
const definition = fieldDefinitions[event.detail?.toUpperCase()]
|
||||||
|
@ -161,6 +164,9 @@
|
||||||
if (field.type === LINK_TYPE) {
|
if (field.type === LINK_TYPE) {
|
||||||
field.relationshipType = RelationshipTypes.MANY_TO_MANY
|
field.relationshipType = RelationshipTypes.MANY_TO_MANY
|
||||||
}
|
}
|
||||||
|
if (field.type === FORMULA_TYPE) {
|
||||||
|
field.formulaType = "dynamic"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeRequired(e) {
|
function onChangeRequired(e) {
|
||||||
|
@ -241,6 +247,11 @@
|
||||||
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),
|
||||||
|
@ -431,8 +442,22 @@
|
||||||
error={errors.relatedName}
|
error={errors.relatedName}
|
||||||
/>
|
/>
|
||||||
{:else if field.type === FORMULA_TYPE}
|
{:else if field.type === FORMULA_TYPE}
|
||||||
|
{#if !table.sql}
|
||||||
|
<Select
|
||||||
|
label="Formula type"
|
||||||
|
bind:value={field.formulaType}
|
||||||
|
options={[
|
||||||
|
{ label: "Dynamic", value: "dynamic" },
|
||||||
|
{ label: "Static", value: "static" },
|
||||||
|
]}
|
||||||
|
getOptionLabel={option => option.label}
|
||||||
|
getOptionValue={option => option.value}
|
||||||
|
tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered,
|
||||||
|
while static formula are calculated when the row is saved."
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
title="Handlebars Formula"
|
title="Formula"
|
||||||
label="Formula"
|
label="Formula"
|
||||||
value={field.formula}
|
value={field.formula}
|
||||||
on:change={e => (field.formula = e.detail)}
|
on:change={e => (field.formula = e.detail)}
|
||||||
|
@ -441,7 +466,7 @@
|
||||||
/>
|
/>
|
||||||
{:else if field.type === AUTO_TYPE}
|
{:else if field.type === AUTO_TYPE}
|
||||||
<Select
|
<Select
|
||||||
label="Auto Column Type"
|
label="Auto column type"
|
||||||
value={field.subtype}
|
value={field.subtype}
|
||||||
on:change={e => (field.subtype = e.detail)}
|
on:change={e => (field.subtype = e.detail)}
|
||||||
options={Object.entries(getAutoColumnInformation())}
|
options={Object.entries(getAutoColumnInformation())}
|
||||||
|
|
|
@ -188,7 +188,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Body size="S"><i>No tables found.</i></Body>
|
<Body size="S"><i>No tables found.</i></Body>
|
||||||
{/if}
|
{/if}
|
||||||
{#if plusTables?.length !== 0}
|
{#if plusTables?.length !== 0 && integration.relationships}
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
<Heading size="S">Relationships</Heading>
|
<Heading size="S">Relationships</Heading>
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
import GoogleLogo from "assets/google-logo.png"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
export let preAuthStep
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
$: tenantId = $auth.tenantId
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
on:click={async () => {
|
||||||
|
let ds = datasource
|
||||||
|
if (!ds) {
|
||||||
|
ds = await preAuthStep()
|
||||||
|
}
|
||||||
|
window.open(
|
||||||
|
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${datasource._id}&appId=${$store.appId}`,
|
||||||
|
"_blank"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="inner">
|
||||||
|
<img src={GoogleLogo} alt="google icon" />
|
||||||
|
<p>Sign in with Google</p>
|
||||||
|
</div>
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: var(--spacing-xs);
|
||||||
|
padding-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
.inner img {
|
||||||
|
width: 18px;
|
||||||
|
margin: 3px 10px 3px 3px;
|
||||||
|
}
|
||||||
|
.inner p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,184 @@
|
||||||
|
<script>
|
||||||
|
export let width = "100"
|
||||||
|
export let height = "100"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
version="1.0"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 50 80"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="path-1"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="path-3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="path-5"
|
||||||
|
/>
|
||||||
|
<linearGradient
|
||||||
|
x1="50.0053945%"
|
||||||
|
y1="8.58610612%"
|
||||||
|
x2="50.0053945%"
|
||||||
|
y2="100.013939%"
|
||||||
|
id="linearGradient-7"
|
||||||
|
>
|
||||||
|
<stop stop-color="#263238" stop-opacity="0.2" offset="0%" />
|
||||||
|
<stop stop-color="#263238" stop-opacity="0.02" offset="100%" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="path-8"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="path-10"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="path-12"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="path-14"
|
||||||
|
/>
|
||||||
|
<radialGradient
|
||||||
|
cx="3.16804688%"
|
||||||
|
cy="2.71744318%"
|
||||||
|
fx="3.16804688%"
|
||||||
|
fy="2.71744318%"
|
||||||
|
r="161.248516%"
|
||||||
|
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)"
|
||||||
|
id="radialGradient-16"
|
||||||
|
>
|
||||||
|
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%" />
|
||||||
|
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g
|
||||||
|
id="Consumer-Apps-Sheets-Large-VD-R8-"
|
||||||
|
transform="translate(-451.000000, -451.000000)"
|
||||||
|
>
|
||||||
|
<g id="Hero" transform="translate(0.000000, 63.000000)">
|
||||||
|
<g id="Personal" transform="translate(277.000000, 299.000000)">
|
||||||
|
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
|
||||||
|
<g id="Group">
|
||||||
|
<g id="Clipped">
|
||||||
|
<mask id="mask-2" fill="white">
|
||||||
|
<use xlink:href="#path-1" />
|
||||||
|
</mask>
|
||||||
|
<g id="SVGID_1_" />
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z"
|
||||||
|
id="Path"
|
||||||
|
fill="#0F9D58"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
mask="url(#mask-2)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="Clipped">
|
||||||
|
<mask id="mask-4" fill="white">
|
||||||
|
<use xlink:href="#path-3" />
|
||||||
|
</mask>
|
||||||
|
<g id="SVGID_1_" />
|
||||||
|
<path
|
||||||
|
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z"
|
||||||
|
id="Shape"
|
||||||
|
fill="#F1F1F1"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
mask="url(#mask-4)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="Clipped">
|
||||||
|
<mask id="mask-6" fill="white">
|
||||||
|
<use xlink:href="#path-5" />
|
||||||
|
</mask>
|
||||||
|
<g id="SVGID_1_" />
|
||||||
|
<polygon
|
||||||
|
id="Path"
|
||||||
|
fill="url(#linearGradient-7)"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
mask="url(#mask-6)"
|
||||||
|
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="Clipped">
|
||||||
|
<mask id="mask-9" fill="white">
|
||||||
|
<use xlink:href="#path-8" />
|
||||||
|
</mask>
|
||||||
|
<g id="SVGID_1_" />
|
||||||
|
<g id="Group" mask="url(#mask-9)">
|
||||||
|
<g transform="translate(26.625000, -2.958333)">
|
||||||
|
<path
|
||||||
|
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z"
|
||||||
|
id="Path"
|
||||||
|
fill="#87CEAC"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="Clipped">
|
||||||
|
<mask id="mask-11" fill="white">
|
||||||
|
<use xlink:href="#path-10" />
|
||||||
|
</mask>
|
||||||
|
<g id="SVGID_1_" />
|
||||||
|
<path
|
||||||
|
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z"
|
||||||
|
id="Path"
|
||||||
|
fill-opacity="0.2"
|
||||||
|
fill="#FFFFFF"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
mask="url(#mask-11)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="Clipped">
|
||||||
|
<mask id="mask-13" fill="white">
|
||||||
|
<use xlink:href="#path-12" />
|
||||||
|
</mask>
|
||||||
|
<g id="SVGID_1_" />
|
||||||
|
<path
|
||||||
|
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z"
|
||||||
|
id="Path"
|
||||||
|
fill-opacity="0.2"
|
||||||
|
fill="#263238"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
mask="url(#mask-13)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="Clipped">
|
||||||
|
<mask id="mask-15" fill="white">
|
||||||
|
<use xlink:href="#path-14" />
|
||||||
|
</mask>
|
||||||
|
<g id="SVGID_1_" />
|
||||||
|
<path
|
||||||
|
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z"
|
||||||
|
id="Path"
|
||||||
|
fill-opacity="0.1"
|
||||||
|
fill="#263238"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
mask="url(#mask-15)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z"
|
||||||
|
id="Path"
|
||||||
|
fill="url(#radialGradient-16)"
|
||||||
|
fill-rule="nonzero"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
|
@ -11,6 +11,7 @@ import ArangoDB from "./ArangoDB.svelte"
|
||||||
import Rest from "./Rest.svelte"
|
import Rest from "./Rest.svelte"
|
||||||
import Budibase from "./Budibase.svelte"
|
import Budibase from "./Budibase.svelte"
|
||||||
import Oracle from "./Oracle.svelte"
|
import Oracle from "./Oracle.svelte"
|
||||||
|
import GoogleSheets from "./GoogleSheets.svelte"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
BUDIBASE: Budibase,
|
BUDIBASE: Budibase,
|
||||||
|
@ -26,4 +27,5 @@ export default {
|
||||||
ARANGODB: ArangoDB,
|
ARANGODB: ArangoDB,
|
||||||
REST: Rest,
|
REST: Rest,
|
||||||
ORACLE: Oracle,
|
ORACLE: Oracle,
|
||||||
|
GOOGLE_SHEETS: GoogleSheets,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
|
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||||
import { createRestDatasource } from "builderStore/datasource"
|
import { createRestDatasource } from "builderStore/datasource"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||||
|
@ -38,6 +39,7 @@
|
||||||
plus: selected.plus,
|
plus: selected.plus,
|
||||||
config,
|
config,
|
||||||
schema: selected.datasource,
|
schema: selected.datasource,
|
||||||
|
auth: selected.auth,
|
||||||
}
|
}
|
||||||
checkShowImport()
|
checkShowImport()
|
||||||
}
|
}
|
||||||
|
@ -79,7 +81,11 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={externalDatasourceModal}>
|
<Modal bind:this={externalDatasourceModal}>
|
||||||
|
{#if integration?.auth?.type === "google"}
|
||||||
|
<GoogleDatasourceConfigModal {integration} {modal} />
|
||||||
|
{:else}
|
||||||
<DatasourceConfigModal {integration} {modal} />
|
<DatasourceConfigModal {integration} {modal} />
|
||||||
|
{/if}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={importModal}>
|
<Modal bind:this={importModal}>
|
||||||
|
|
|
@ -51,13 +51,9 @@
|
||||||
>Connect your database to Budibase using the config below.
|
>Connect your database to Budibase using the config below.
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm
|
||||||
schema={datasource.schema}
|
schema={datasource.schema}
|
||||||
bind:datasource
|
bind:datasource
|
||||||
creating={true}
|
creating={true}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Body, Layout } from "@budibase/bbui"
|
||||||
|
import { IntegrationNames } from "constants/backend"
|
||||||
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
|
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||||
|
import { saveDatasource as save } from "builderStore/datasource"
|
||||||
|
|
||||||
|
export let integration
|
||||||
|
export let modal
|
||||||
|
|
||||||
|
// kill the reference so the input isn't saved
|
||||||
|
let datasource = cloneDeep(integration)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||||
|
onCancel={() => modal.show()}
|
||||||
|
cancelText="Back"
|
||||||
|
size="L"
|
||||||
|
>
|
||||||
|
<Layout noPadding>
|
||||||
|
<Body size="XS"
|
||||||
|
>Authenticate with your google account to use the {IntegrationNames[
|
||||||
|
datasource.type
|
||||||
|
]} integration.</Body
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
||||||
|
</ModalContent>
|
|
@ -53,7 +53,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create table
|
// Create table
|
||||||
const table = await tables.save(newTable)
|
let table
|
||||||
|
try {
|
||||||
|
table = await tables.save(newTable)
|
||||||
notifications.success(`Table ${name} created successfully.`)
|
notifications.success(`Table ${name} created successfully.`)
|
||||||
analytics.captureEvent(Events.TABLE.CREATED, { name })
|
analytics.captureEvent(Events.TABLE.CREATED, { name })
|
||||||
|
|
||||||
|
@ -63,6 +65,11 @@
|
||||||
? `./table/${table._id}`
|
? `./table/${table._id}`
|
||||||
: `../../table/${table._id}`
|
: `../../table/${table._id}`
|
||||||
$goto(path)
|
$goto(path)
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error(e)
|
||||||
|
// reload in case the table was created
|
||||||
|
await tables.fetch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
|
||||||
import { store, hostingStore } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
|
||||||
const DeploymentStatus = {
|
const DeploymentStatus = {
|
||||||
SUCCESS: "SUCCESS",
|
SUCCESS: "SUCCESS",
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
let poll
|
let poll
|
||||||
let deployments = []
|
let deployments = []
|
||||||
let urlComponent = $store.url || `/${appId}`
|
let urlComponent = $store.url || `/${appId}`
|
||||||
let deploymentUrl = `${$hostingStore.appUrl}${urlComponent}`
|
let deploymentUrl = `${urlComponent}`
|
||||||
|
|
||||||
const formatDate = (date, format) =>
|
const formatDate = (date, format) =>
|
||||||
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select } from "@budibase/bbui"
|
import { Select } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
const themeOptions = [
|
const themeOptions = [
|
||||||
{
|
{
|
||||||
|
@ -20,6 +21,17 @@
|
||||||
value: "spectrum--darkest",
|
value: "spectrum--darkest",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const onChangeTheme = async theme => {
|
||||||
|
await store.actions.theme.save(theme)
|
||||||
|
await store.actions.customTheme.save({
|
||||||
|
...get(store).customTheme,
|
||||||
|
navBackground:
|
||||||
|
theme === "spectrum--light"
|
||||||
|
? "var(--spectrum-global-color-gray-50)"
|
||||||
|
: "var(--spectrum-global-color-gray-100)",
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -27,7 +39,7 @@
|
||||||
value={$store.theme}
|
value={$store.theme}
|
||||||
options={themeOptions}
|
options={themeOptions}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
on:change={e => store.actions.theme.save(e.detail)}
|
on:change={e => onChangeTheme(e.detail)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
primaryColor: "var(--spectrum-global-color-blue-600)",
|
primaryColor: "var(--spectrum-global-color-blue-600)",
|
||||||
primaryColorHover: "var(--spectrum-global-color-blue-500)",
|
primaryColorHover: "var(--spectrum-global-color-blue-500)",
|
||||||
buttonBorderRadius: "16px",
|
buttonBorderRadius: "16px",
|
||||||
navBackground: "var(--spectrum-global-color-gray-100)",
|
navBackground: "var(--spectrum-global-color-gray-50)",
|
||||||
navTextColor: "var(--spectrum-global-color-gray-800)",
|
navTextColor: "var(--spectrum-global-color-gray-800)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetTheme = () => {
|
const resetTheme = () => {
|
||||||
store.actions.customTheme.save(null)
|
const theme = get(store).theme
|
||||||
|
store.actions.customTheme.save({
|
||||||
|
...defaultTheme,
|
||||||
|
navBackground:
|
||||||
|
theme === "spectrum--light"
|
||||||
|
? "var(--spectrum-global-color-gray-50)"
|
||||||
|
: "var(--spectrum-global-color-gray-100)",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,8 @@
|
||||||
"relationshipfield",
|
"relationshipfield",
|
||||||
"daterangepicker",
|
"daterangepicker",
|
||||||
"multifieldselect",
|
"multifieldselect",
|
||||||
"jsonfield"
|
"jsonfield",
|
||||||
|
"s3upload"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -55,8 +55,8 @@
|
||||||
<div>
|
<div>
|
||||||
<BindingBuilder
|
<BindingBuilder
|
||||||
bind:customParams={parameters.queryParams}
|
bind:customParams={parameters.queryParams}
|
||||||
bindings={query.parameters}
|
queryBindings={query.parameters}
|
||||||
bind:bindableOptions={bindings}
|
bind:bindings
|
||||||
/>
|
/>
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
height={200}
|
height={200}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label } from "@budibase/bbui"
|
||||||
|
import { currentAsset } from "builderStore"
|
||||||
|
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
$: components = findAllMatchingComponents($currentAsset.props, component =>
|
||||||
|
component._component.endsWith("s3upload")
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Label small>S3 Upload Component</Label>
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.componentId}
|
||||||
|
options={components}
|
||||||
|
getOptionLabel={x => x._instanceName}
|
||||||
|
getOptionValue={x => x._id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 120px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -11,3 +11,4 @@ export { default as ChangeFormStep } from "./ChangeFormStep.svelte"
|
||||||
export { default as UpdateState } from "./UpdateState.svelte"
|
export { default as UpdateState } from "./UpdateState.svelte"
|
||||||
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
||||||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||||
|
export { default as S3Upload } from "./S3Upload.svelte"
|
||||||
|
|
|
@ -70,6 +70,16 @@
|
||||||
"name": "Update State",
|
"name": "Update State",
|
||||||
"component": "UpdateState",
|
"component": "UpdateState",
|
||||||
"dependsOnFeature": "state"
|
"dependsOnFeature": "state"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Upload File to S3",
|
||||||
|
"component": "S3Upload",
|
||||||
|
"context": [
|
||||||
|
{
|
||||||
|
"label": "File URL",
|
||||||
|
"value": "publicUrl"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -169,8 +169,8 @@
|
||||||
{#if getQueryParams(value).length > 0}
|
{#if getQueryParams(value).length > 0}
|
||||||
<BindingBuilder
|
<BindingBuilder
|
||||||
bind:customParams={tmpQueryParams}
|
bind:customParams={tmpQueryParams}
|
||||||
bindings={getQueryParams(value)}
|
queryBindings={getQueryParams(value)}
|
||||||
bind:bindableOptions={bindings}
|
bind:bindings
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<IntegrationQueryEditor
|
<IntegrationQueryEditor
|
||||||
|
|
|
@ -131,7 +131,7 @@
|
||||||
{bindings}
|
{bindings}
|
||||||
on:change={event => (filter.value = event.detail)}
|
on:change={event => (filter.value = event.detail)}
|
||||||
/>
|
/>
|
||||||
{:else if ["string", "longform", "number"].includes(filter.type)}
|
{:else if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
{:else if ["options", "array"].includes(filter.type)}
|
{:else if ["options", "array"].includes(filter.type)}
|
||||||
<Combobox
|
<Combobox
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import { Select } from "@budibase/bbui"
|
||||||
|
import { datasources } from "stores/backend"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
|
||||||
|
$: dataSources = $datasources.list
|
||||||
|
.filter(ds => ds.source === "S3" && !ds.config?.endpoint)
|
||||||
|
.map(ds => ({
|
||||||
|
label: ds.name,
|
||||||
|
value: ds._id,
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select options={dataSources} {value} on:change />
|
|
@ -1,5 +1,6 @@
|
||||||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||||
|
import S3DataSourceSelect from "./S3DataSourceSelect.svelte"
|
||||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||||
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
|
import ButtonActionEditor from "./ButtonActionEditor/ButtonActionEditor.svelte"
|
||||||
import TableSelect from "./TableSelect.svelte"
|
import TableSelect from "./TableSelect.svelte"
|
||||||
|
@ -22,6 +23,7 @@ const componentMap = {
|
||||||
text: DrawerBindableCombobox,
|
text: DrawerBindableCombobox,
|
||||||
select: Select,
|
select: Select,
|
||||||
dataSource: DataSourceSelect,
|
dataSource: DataSourceSelect,
|
||||||
|
"dataSource/s3": S3DataSourceSelect,
|
||||||
dataProvider: DataProviderSelect,
|
dataProvider: DataProviderSelect,
|
||||||
boolean: Checkbox,
|
boolean: Checkbox,
|
||||||
number: Stepper,
|
number: Stepper,
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { Label, Select } from "@budibase/bbui"
|
import { Label, Select } from "@budibase/bbui"
|
||||||
import { permissions, roles } from "stores/backend"
|
import { permissions, roles } from "stores/backend"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { Roles } from "constants/backend"
|
import { Roles } from "constants/backend"
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
export let saveId
|
|
||||||
export let label
|
export let label
|
||||||
|
|
||||||
$: updateRole(roleId, saveId)
|
$: getPermissions(query)
|
||||||
|
|
||||||
let roleId, loaded
|
let roleId, loaded, fetched
|
||||||
|
|
||||||
async function updateRole(role, id) {
|
async function updateRole(role, id) {
|
||||||
roleId = role
|
roleId = role
|
||||||
|
@ -26,19 +24,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
async function getPermissions(queryToFetch) {
|
||||||
if (!query || !query._id) {
|
if (fetched?._id === queryToFetch?._id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetched = queryToFetch
|
||||||
|
if (!queryToFetch || !queryToFetch._id) {
|
||||||
roleId = Roles.BASIC
|
roleId = Roles.BASIC
|
||||||
loaded = true
|
loaded = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
roleId = (await permissions.forResource(query._id))["read"]
|
roleId = (await permissions.forResource(queryToFetch._id))["read"]
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
roleId = Roles.BASIC
|
roleId = Roles.BASIC
|
||||||
}
|
}
|
||||||
loaded = true
|
loaded = true
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
|
|
@ -7,27 +7,24 @@
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
|
|
||||||
export let bindable = true
|
export let bindable = true
|
||||||
|
export let queryBindings = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let bindableOptions = []
|
|
||||||
export let customParams = {}
|
export let customParams = {}
|
||||||
|
|
||||||
function newQueryBinding() {
|
function newQueryBinding() {
|
||||||
bindings = [...bindings, {}]
|
queryBindings = [...queryBindings, {}]
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteQueryBinding(idx) {
|
function deleteQueryBinding(idx) {
|
||||||
bindings.splice(idx, 1)
|
queryBindings.splice(idx, 1)
|
||||||
bindings = bindings
|
queryBindings = queryBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is necessary due to the way readable and writable bindings are stored.
|
// This is necessary due to the way readable and writable bindings are stored.
|
||||||
// The readable binding in the UI gets converted to a UUID value that the client understands
|
// The readable binding in the UI gets converted to a UUID value that the client understands
|
||||||
// for parsing, then converted back so we can display it the readable form in the UI
|
// for parsing, then converted back so we can display it the readable form in the UI
|
||||||
function onBindingChange(param, valueToParse) {
|
function onBindingChange(param, valueToParse) {
|
||||||
customParams[param] = readableToRuntimeBinding(
|
customParams[param] = readableToRuntimeBinding(bindings, valueToParse)
|
||||||
bindableOptions,
|
|
||||||
valueToParse
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -49,7 +46,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</Body>
|
</Body>
|
||||||
<div class="bindings" class:bindable>
|
<div class="bindings" class:bindable>
|
||||||
{#each bindings as binding, idx}
|
{#each queryBindings as binding, idx}
|
||||||
<Input
|
<Input
|
||||||
placeholder="Binding Name"
|
placeholder="Binding Name"
|
||||||
thin
|
thin
|
||||||
|
@ -69,10 +66,10 @@
|
||||||
thin
|
thin
|
||||||
on:change={evt => onBindingChange(binding.name, evt.detail)}
|
on:change={evt => onBindingChange(binding.name, evt.detail)}
|
||||||
value={runtimeToReadableBinding(
|
value={runtimeToReadableBinding(
|
||||||
bindableOptions,
|
bindings,
|
||||||
customParams?.[binding.name]
|
customParams?.[binding.name]
|
||||||
)}
|
)}
|
||||||
{bindableOptions}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Icon hoverable name="Close" on:click={() => deleteQueryBinding(idx)} />
|
<Icon hoverable name="Close" on:click={() => deleteQueryBinding(idx)} />
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
config={integrationInfo.extra}
|
config={integrationInfo.extra}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<BindingBuilder bind:bindings={query.parameters} bindable={false} />
|
<BindingBuilder bind:queryBindings={query.parameters} bindable={false} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if shouldShowQueryConfig}
|
{#if shouldShowQueryConfig}
|
||||||
|
|
|
@ -1,100 +1,46 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
import { writable, get as svelteGet } from "svelte/store"
|
||||||
|
|
||||||
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||||
import { store, automationStore, hostingStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { admin, auth } from "stores/portal"
|
import { apps, admin, auth } from "stores/portal"
|
||||||
import { string, mixed, object } from "yup"
|
|
||||||
import api, { get, post } from "builderStore/api"
|
import api, { get, post } from "builderStore/api"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { capitalise } from "helpers"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { APP_NAME_REGEX } from "constants"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
export let inline
|
|
||||||
|
|
||||||
const values = writable({ name: null })
|
const values = writable({ name: "", url: null })
|
||||||
const errors = writable({})
|
const validation = createValidationStore()
|
||||||
const touched = writable({})
|
$: validation.check($values)
|
||||||
const validator = {
|
|
||||||
name: string()
|
|
||||||
.trim()
|
|
||||||
.required("Your application must have a name")
|
|
||||||
.matches(
|
|
||||||
APP_NAME_REGEX,
|
|
||||||
"App name must be letters, numbers and spaces only"
|
|
||||||
),
|
|
||||||
file: template?.fromFile
|
|
||||||
? mixed().required("Please choose a file to import")
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
let submitting = false
|
|
||||||
let valid = false
|
|
||||||
let initialTemplateInfo = template?.fromFile || template?.key
|
|
||||||
|
|
||||||
$: checkValidity($values, validator)
|
|
||||||
$: showTemplateSelection = !template && !initialTemplateInfo
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await hostingStore.actions.fetchDeployedApps()
|
await setupValidation()
|
||||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
|
||||||
validator.name = string()
|
|
||||||
.trim()
|
|
||||||
.required("Your application must have a name")
|
|
||||||
.matches(APP_NAME_REGEX, "App name must be letters and numbers only")
|
|
||||||
.test(
|
|
||||||
"non-existing-app-name",
|
|
||||||
"Another app with the same name already exists",
|
|
||||||
value => {
|
|
||||||
return !existingAppNames.some(
|
|
||||||
appName => appName.toLowerCase() === value.toLowerCase()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const checkValidity = async (values, validator) => {
|
const setupValidation = async () => {
|
||||||
const obj = object().shape(validator)
|
const applications = svelteGet(apps)
|
||||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
appValidation.name(validation, { apps: applications })
|
||||||
if (template?.fromFile && values.file == null) {
|
appValidation.url(validation, { apps: applications })
|
||||||
valid = false
|
appValidation.file(validation, { template })
|
||||||
return
|
// init validation
|
||||||
}
|
validation.check($values)
|
||||||
|
|
||||||
try {
|
|
||||||
await obj.validate(values, { abortEarly: false })
|
|
||||||
} catch (validationErrors) {
|
|
||||||
validationErrors.inner.forEach(error => {
|
|
||||||
$errors[error.path] = capitalise(error.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
valid = await obj.isValid(values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewApp() {
|
async function createNewApp() {
|
||||||
const templateToUse = Object.keys(template).length === 0 ? null : template
|
|
||||||
submitting = true
|
|
||||||
|
|
||||||
// Check a template exists if we are important
|
|
||||||
if (templateToUse?.fromFile && !$values.file) {
|
|
||||||
$errors.file = "Please choose a file to import"
|
|
||||||
valid = false
|
|
||||||
submitting = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create form data to create app
|
// Create form data to create app
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("name", $values.name.trim())
|
data.append("name", $values.name.trim())
|
||||||
data.append("useTemplate", templateToUse != null)
|
if ($values.url) {
|
||||||
if (templateToUse) {
|
data.append("url", $values.url.trim())
|
||||||
data.append("templateName", templateToUse.name)
|
}
|
||||||
data.append("templateKey", templateToUse.key)
|
data.append("useTemplate", template != null)
|
||||||
|
if (template) {
|
||||||
|
data.append("templateName", template.name)
|
||||||
|
data.append("templateKey", template.key)
|
||||||
data.append("templateFile", $values.file)
|
data.append("templateFile", $values.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +54,7 @@
|
||||||
analytics.captureEvent(Events.APP.CREATED, {
|
analytics.captureEvent(Events.APP.CREATED, {
|
||||||
name: $values.name,
|
name: $values.name,
|
||||||
appId: appJson.instance._id,
|
appId: appJson.instance._id,
|
||||||
templateToUse,
|
templateToUse: template,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Select Correct Application/DB in prep for creating user
|
// Select Correct Application/DB in prep for creating user
|
||||||
|
@ -136,44 +82,51 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
submitting = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onCancel() {
|
// auto add slash to url
|
||||||
template = null
|
$: {
|
||||||
await auth.setInitInfo({})
|
if ($values.url && !$values.url.startsWith("/")) {
|
||||||
|
$values.url = `/${$values.url}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={"Name your app"}
|
title={"Create your app"}
|
||||||
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
||||||
onConfirm={createNewApp}
|
onConfirm={createNewApp}
|
||||||
onCancel={inline ? onCancel : null}
|
disabled={!$validation.valid}
|
||||||
cancelText={inline ? "Back" : undefined}
|
|
||||||
showCloseIcon={!inline}
|
|
||||||
disabled={!valid}
|
|
||||||
>
|
>
|
||||||
{#if template?.fromFile}
|
{#if template?.fromFile}
|
||||||
<Dropzone
|
<Dropzone
|
||||||
error={$touched.file && $errors.file}
|
error={$validation.touched.file && $validation.errors.file}
|
||||||
gallery={false}
|
gallery={false}
|
||||||
label="File to import"
|
label="File to import"
|
||||||
value={[$values.file]}
|
value={[$values.file]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
$values.file = e.detail?.[0]
|
$values.file = e.detail?.[0]
|
||||||
$touched.file = true
|
$validation.touched.file = true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<Input
|
<Input
|
||||||
bind:value={$values.name}
|
bind:value={$values.name}
|
||||||
error={$touched.name && $errors.name}
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
on:blur={() => ($touched.name = true)}
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
label="Name"
|
label="Name"
|
||||||
placeholder={$auth.user.firstName
|
placeholder={$auth.user.firstName
|
||||||
? `${$auth.user.firstName}'s app`
|
? `${$auth.user.firstName}s app`
|
||||||
: "My app"}
|
: "My app"}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.url}
|
||||||
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
label="URL"
|
||||||
|
placeholder={$values.name
|
||||||
|
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||||
|
: "/"}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -1,120 +1,75 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable, get as svelteGet } from "svelte/store"
|
import { writable, get as svelteGet } from "svelte/store"
|
||||||
import {
|
import { notifications, Input, ModalContent, Body } from "@budibase/bbui"
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Body,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { hostingStore } from "builderStore"
|
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
import { string, object } from "yup"
|
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { capitalise } from "helpers"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import { APP_NAME_REGEX } from "constants"
|
import * as appValidation from "helpers/validation/yup/app"
|
||||||
|
|
||||||
const values = writable({ name: null })
|
|
||||||
const errors = writable({})
|
|
||||||
const touched = writable({})
|
|
||||||
const validator = {
|
|
||||||
name: string()
|
|
||||||
.trim()
|
|
||||||
.required("Your application must have a name")
|
|
||||||
.matches(
|
|
||||||
APP_NAME_REGEX,
|
|
||||||
"App name must be letters, numbers and spaces only"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
|
||||||
let modal
|
const values = writable({ name: "", url: null })
|
||||||
let valid = false
|
const validation = createValidationStore()
|
||||||
let dirty = false
|
$: validation.check($values)
|
||||||
$: checkValidity($values, validator)
|
|
||||||
$: {
|
|
||||||
// prevent validation by setting name to undefined without an app
|
|
||||||
if (app) {
|
|
||||||
$values.name = app?.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await hostingStore.actions.fetchDeployedApps()
|
$values.name = app.name
|
||||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames
|
$values.url = app.url
|
||||||
validator.name = string()
|
setupValidation()
|
||||||
.trim()
|
|
||||||
.required("Your application must have a name")
|
|
||||||
.matches(
|
|
||||||
APP_NAME_REGEX,
|
|
||||||
"App name must be letters, numbers and spaces only"
|
|
||||||
)
|
|
||||||
.test(
|
|
||||||
"non-existing-app-name",
|
|
||||||
"Another app with the same name already exists",
|
|
||||||
value => {
|
|
||||||
return !existingAppNames.some(
|
|
||||||
appName => dirty && appName.toLowerCase() === value.toLowerCase()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const checkValidity = async (values, validator) => {
|
const setupValidation = async () => {
|
||||||
const obj = object().shape(validator)
|
const applications = svelteGet(apps)
|
||||||
Object.keys(validator).forEach(key => ($errors[key] = null))
|
appValidation.name(validation, { apps: applications, currentApp: app })
|
||||||
try {
|
appValidation.url(validation, { apps: applications, currentApp: app })
|
||||||
await obj.validate(values, { abortEarly: false })
|
// init validation
|
||||||
} catch (validationErrors) {
|
validation.check($values)
|
||||||
validationErrors.inner.forEach(error => {
|
|
||||||
$errors[error.path] = capitalise(error.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
valid = await obj.isValid(values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateApp() {
|
async function updateApp() {
|
||||||
try {
|
try {
|
||||||
// Update App
|
// Update App
|
||||||
await apps.update(app.instance._id, { name: $values.name.trim() })
|
const body = {
|
||||||
hide()
|
name: $values.name.trim(),
|
||||||
|
}
|
||||||
|
if ($values.url) {
|
||||||
|
body.url = $values.url.trim()
|
||||||
|
}
|
||||||
|
await apps.update(app.instance._id, body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error(error)
|
notifications.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const show = () => {
|
// auto add slash to url
|
||||||
modal.show()
|
$: {
|
||||||
|
if ($values.url && !$values.url.startsWith("/")) {
|
||||||
|
$values.url = `/${$values.url}`
|
||||||
}
|
}
|
||||||
export const hide = () => {
|
|
||||||
modal.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onShow = () => {
|
|
||||||
dirty = false
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}>
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={"Edit app"}
|
title={"Edit app"}
|
||||||
confirmText={"Save"}
|
confirmText={"Save"}
|
||||||
onConfirm={updateApp}
|
onConfirm={updateApp}
|
||||||
disabled={!(valid && dirty)}
|
disabled={!$validation.valid}
|
||||||
>
|
>
|
||||||
<Body size="S">Update the name of your app.</Body>
|
<Body size="S">Update the name of your app.</Body>
|
||||||
<Input
|
<Input
|
||||||
bind:value={$values.name}
|
bind:value={$values.name}
|
||||||
error={$touched.name && $errors.name}
|
error={$validation.touched.name && $validation.errors.name}
|
||||||
on:blur={() => ($touched.name = true)}
|
on:blur={() => ($validation.touched.name = true)}
|
||||||
on:change={() => (dirty = true)}
|
|
||||||
label="Name"
|
label="Name"
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
bind:value={$values.url}
|
||||||
|
error={$validation.touched.url && $validation.errors.url}
|
||||||
|
on:blur={() => ($validation.touched.url = true)}
|
||||||
|
label="URL"
|
||||||
|
placeholder={$values.name
|
||||||
|
? "/" + encodeURIComponent($values.name).toLowerCase()
|
||||||
|
: "/"}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
|
||||||
|
|
|
@ -148,20 +148,23 @@ export const RelationshipTypes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
|
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
|
||||||
|
|
||||||
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
||||||
opt => opt.type
|
opt => opt.type
|
||||||
)
|
)
|
||||||
|
|
||||||
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
|
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
|
||||||
|
|
||||||
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
||||||
opt => opt.type
|
opt => opt.type
|
||||||
)
|
)
|
||||||
|
|
||||||
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
|
export const ALLOWABLE_JSON_OPTIONS = [FIELDS.JSON, FIELDS.ARRAY]
|
||||||
ALLOWABLE_STRING_TYPES
|
export const ALLOWABLE_JSON_TYPES = ALLOWABLE_JSON_OPTIONS.map(opt => opt.type)
|
||||||
)
|
|
||||||
|
export const SWITCHABLE_TYPES = [
|
||||||
|
...ALLOWABLE_STRING_TYPES,
|
||||||
|
...ALLOWABLE_NUMBER_TYPES,
|
||||||
|
...ALLOWABLE_JSON_TYPES,
|
||||||
|
]
|
||||||
|
|
||||||
export const IntegrationTypes = {
|
export const IntegrationTypes = {
|
||||||
POSTGRES: "POSTGRES",
|
POSTGRES: "POSTGRES",
|
||||||
|
@ -177,6 +180,7 @@ export const IntegrationTypes = {
|
||||||
ARANGODB: "ARANGODB",
|
ARANGODB: "ARANGODB",
|
||||||
ORACLE: "ORACLE",
|
ORACLE: "ORACLE",
|
||||||
INTERNAL: "INTERNAL",
|
INTERNAL: "INTERNAL",
|
||||||
|
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationNames = {
|
export const IntegrationNames = {
|
||||||
|
@ -193,6 +197,7 @@ export const IntegrationNames = {
|
||||||
[IntegrationTypes.ARANGODB]: "ArangoDB",
|
[IntegrationTypes.ARANGODB]: "ArangoDB",
|
||||||
[IntegrationTypes.ORACLE]: "Oracle",
|
[IntegrationTypes.ORACLE]: "Oracle",
|
||||||
[IntegrationTypes.INTERNAL]: "Internal",
|
[IntegrationTypes.INTERNAL]: "Internal",
|
||||||
|
[IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SchemaTypeOptions = [
|
export const SchemaTypeOptions = [
|
||||||
|
|
|
@ -15,6 +15,22 @@ export const AppStatus = {
|
||||||
DEPLOYED: "published",
|
DEPLOYED: "published",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const IntegrationNames = {
|
||||||
|
POSTGRES: "PostgreSQL",
|
||||||
|
MONGODB: "MongoDB",
|
||||||
|
COUCHDB: "CouchDB",
|
||||||
|
S3: "S3",
|
||||||
|
MYSQL: "MySQL",
|
||||||
|
REST: "REST",
|
||||||
|
DYNAMODB: "DynamoDB",
|
||||||
|
ELASTICSEARCH: "ElasticSearch",
|
||||||
|
SQL_SERVER: "SQL Server",
|
||||||
|
AIRTABLE: "Airtable",
|
||||||
|
ARANGODB: "ArangoDB",
|
||||||
|
ORACLE: "Oracle",
|
||||||
|
GOOGLE_SHEETS: "Google Sheets",
|
||||||
|
}
|
||||||
|
|
||||||
// fields on the user table that cannot be edited
|
// fields on the user table that cannot be edited
|
||||||
export const UNEDITABLE_USER_FIELDS = [
|
export const UNEDITABLE_USER_FIELDS = [
|
||||||
"email",
|
"email",
|
||||||
|
@ -36,4 +52,7 @@ export const LAYOUT_NAMES = {
|
||||||
|
|
||||||
export const BUDIBASE_INTERNAL_DB = "bb_internal"
|
export const BUDIBASE_INTERNAL_DB = "bb_internal"
|
||||||
|
|
||||||
|
// one or more word characters and whitespace
|
||||||
export const APP_NAME_REGEX = /^[\w\s]+$/
|
export const APP_NAME_REGEX = /^[\w\s]+$/
|
||||||
|
// zero or more non-whitespace characters
|
||||||
|
export const APP_URL_REGEX = /^\S*$/
|
||||||
|
|
|
@ -59,8 +59,7 @@ export const NoEmptyFilterStrings = [
|
||||||
*/
|
*/
|
||||||
export const getValidOperatorsForType = type => {
|
export const getValidOperatorsForType = type => {
|
||||||
const Op = OperatorOptions
|
const Op = OperatorOptions
|
||||||
if (type === "string") {
|
const stringOps = [
|
||||||
return [
|
|
||||||
Op.Equals,
|
Op.Equals,
|
||||||
Op.NotEquals,
|
Op.NotEquals,
|
||||||
Op.StartsWith,
|
Op.StartsWith,
|
||||||
|
@ -68,8 +67,7 @@ export const getValidOperatorsForType = type => {
|
||||||
Op.Empty,
|
Op.Empty,
|
||||||
Op.NotEmpty,
|
Op.NotEmpty,
|
||||||
]
|
]
|
||||||
} else if (type === "number") {
|
const numOps = [
|
||||||
return [
|
|
||||||
Op.Equals,
|
Op.Equals,
|
||||||
Op.NotEquals,
|
Op.NotEquals,
|
||||||
Op.MoreThan,
|
Op.MoreThan,
|
||||||
|
@ -77,6 +75,10 @@ export const getValidOperatorsForType = type => {
|
||||||
Op.Empty,
|
Op.Empty,
|
||||||
Op.NotEmpty,
|
Op.NotEmpty,
|
||||||
]
|
]
|
||||||
|
if (type === "string") {
|
||||||
|
return stringOps
|
||||||
|
} else if (type === "number") {
|
||||||
|
return numOps
|
||||||
} else if (type === "options") {
|
} else if (type === "options") {
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
} else if (type === "array") {
|
} else if (type === "array") {
|
||||||
|
@ -84,23 +86,11 @@ export const getValidOperatorsForType = type => {
|
||||||
} else if (type === "boolean") {
|
} else if (type === "boolean") {
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
} else if (type === "longform") {
|
} else if (type === "longform") {
|
||||||
return [
|
return stringOps
|
||||||
Op.Equals,
|
|
||||||
Op.NotEquals,
|
|
||||||
Op.StartsWith,
|
|
||||||
Op.Like,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
} else if (type === "datetime") {
|
} else if (type === "datetime") {
|
||||||
return [
|
return numOps
|
||||||
Op.Equals,
|
} else if (type === "formula") {
|
||||||
Op.NotEquals,
|
return stringOps.concat([Op.MoreThan, Op.LessThan])
|
||||||
Op.MoreThan,
|
|
||||||
Op.LessThan,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,5 +27,8 @@ export function getFields(fields, { allowLinks } = { allowLinks: true }) {
|
||||||
filteredFields = filteredFields.concat(getTableFields(linkField))
|
filteredFields = filteredFields.concat(getTableFields(linkField))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filteredFields
|
const staticFormulaFields = fields.filter(
|
||||||
|
field => field.type === "formula" && field.formulaType === "static"
|
||||||
|
)
|
||||||
|
return filteredFields.concat(staticFormulaFields)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
|
|
||||||
|
// DEPRECATED - Use the yup based validators for future validation
|
||||||
|
|
||||||
export function createValidationStore(initialValue, ...validators) {
|
export function createValidationStore(initialValue, ...validators) {
|
||||||
let touched = false
|
let touched = false
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// TODO: Convert to yup based validators
|
||||||
|
|
||||||
export function emailValidator(value) {
|
export function emailValidator(value) {
|
||||||
return (
|
return (
|
||||||
(value &&
|
(value &&
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { string, mixed } from "yup"
|
||||||
|
import { APP_NAME_REGEX, APP_URL_REGEX } from "constants"
|
||||||
|
|
||||||
|
export const name = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||||
|
validation.addValidator(
|
||||||
|
"name",
|
||||||
|
string()
|
||||||
|
.trim()
|
||||||
|
.required("Your application must have a name")
|
||||||
|
.matches(
|
||||||
|
APP_NAME_REGEX,
|
||||||
|
"App name must be letters, numbers and spaces only"
|
||||||
|
)
|
||||||
|
.test(
|
||||||
|
"non-existing-app-name",
|
||||||
|
"Another app with the same name already exists",
|
||||||
|
value => {
|
||||||
|
if (!value) {
|
||||||
|
// exit early, above validator will fail
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (currentApp) {
|
||||||
|
// filter out the current app if present
|
||||||
|
apps = apps.filter(app => app.appId !== currentApp.appId)
|
||||||
|
}
|
||||||
|
return !apps
|
||||||
|
.map(app => app.name)
|
||||||
|
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const url = (validation, { apps, currentApp } = { apps: [] }) => {
|
||||||
|
validation.addValidator(
|
||||||
|
"url",
|
||||||
|
string()
|
||||||
|
.nullable()
|
||||||
|
.matches(APP_URL_REGEX, "App URL must not contain spaces")
|
||||||
|
.test(
|
||||||
|
"non-existing-app-url",
|
||||||
|
"Another app with the same URL already exists",
|
||||||
|
value => {
|
||||||
|
// url is nullable
|
||||||
|
if (!value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (currentApp) {
|
||||||
|
// filter out the current app if present
|
||||||
|
apps = apps.filter(app => app.appId !== currentApp.appId)
|
||||||
|
}
|
||||||
|
return !apps
|
||||||
|
.map(app => app.url)
|
||||||
|
.some(appUrl => appUrl?.toLowerCase() === value.toLowerCase())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.test("valid-url", "Not a valid URL", value => {
|
||||||
|
// url is nullable
|
||||||
|
if (!value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// make it clear that this is a url path and cannot be a full url
|
||||||
|
return (
|
||||||
|
value.startsWith("/") &&
|
||||||
|
!value.includes("http") &&
|
||||||
|
!value.includes("www") &&
|
||||||
|
!value.includes(".") &&
|
||||||
|
value.length > 1 // just '/' is not valid
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const file = (validation, { template } = {}) => {
|
||||||
|
const templateToUse =
|
||||||
|
template && Object.keys(template).length === 0 ? null : template
|
||||||
|
validation.addValidator(
|
||||||
|
"file",
|
||||||
|
templateToUse?.fromFile
|
||||||
|
? mixed().required("Please choose a file to import")
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { capitalise } from "helpers"
|
||||||
|
import { object } from "yup"
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export const createValidationStore = () => {
|
||||||
|
const DEFAULT = {
|
||||||
|
errors: {},
|
||||||
|
touched: {},
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = {}
|
||||||
|
const validation = writable(DEFAULT)
|
||||||
|
|
||||||
|
const addValidator = (propertyName, propertyValidator) => {
|
||||||
|
if (!propertyValidator || !propertyName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
validator[propertyName] = propertyValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = async values => {
|
||||||
|
const obj = object().shape(validator)
|
||||||
|
// clear the previous errors
|
||||||
|
const properties = Object.keys(validator)
|
||||||
|
properties.forEach(property => (get(validation).errors[property] = null))
|
||||||
|
|
||||||
|
let validationError = false
|
||||||
|
try {
|
||||||
|
await obj.validate(values, { abortEarly: false })
|
||||||
|
} catch (error) {
|
||||||
|
if (!error.inner) {
|
||||||
|
notifications.error("Unexpected validation error", error)
|
||||||
|
validationError = true
|
||||||
|
} else {
|
||||||
|
error.inner.forEach(err => {
|
||||||
|
validation.update(store => {
|
||||||
|
store.errors[err.path] = capitalise(err.message)
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid
|
||||||
|
if (properties.length && !validationError) {
|
||||||
|
valid = await obj.isValid(values)
|
||||||
|
} else {
|
||||||
|
// don't say valid until validators have been loaded
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
validation.update(store => {
|
||||||
|
store.valid = valid
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: validation.subscribe,
|
||||||
|
set: validation.set,
|
||||||
|
check,
|
||||||
|
addValidator,
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,7 +61,7 @@
|
||||||
await auth.setInitInfo({ init_template: $params["?template"] })
|
await auth.setInitInfo({ init_template: $params["?template"] })
|
||||||
}
|
}
|
||||||
|
|
||||||
await auth.checkAuth()
|
await auth.getSelf()
|
||||||
await admin.init()
|
await admin.init()
|
||||||
|
|
||||||
if (useAccountPortal && multiTenancyEnabled) {
|
if (useAccountPortal && multiTenancyEnabled) {
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
import { isEqual } from "lodash"
|
import { isEqual } from "lodash"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||||
|
|
||||||
let importQueriesModal
|
let importQueriesModal
|
||||||
|
|
||||||
let changed
|
let changed
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
Modal,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { apps, organisation, auth, admin } from "stores/portal"
|
import { apps, organisation, auth } from "stores/portal"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import { gradient } from "actions"
|
import { gradient } from "actions"
|
||||||
|
@ -34,7 +34,6 @@
|
||||||
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
|
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
$: publishedApps = $apps.filter(publishedAppsOnly)
|
$: publishedApps = $apps.filter(publishedAppsOnly)
|
||||||
$: isCloud = $admin.cloud
|
|
||||||
$: userApps = $auth.user?.builder?.global
|
$: userApps = $auth.user?.builder?.global
|
||||||
? publishedApps
|
? publishedApps
|
||||||
: publishedApps.filter(app =>
|
: publishedApps.filter(app =>
|
||||||
|
@ -42,7 +41,11 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
function getUrl(app) {
|
function getUrl(app) {
|
||||||
return !isCloud ? `/app/${encodeURIComponent(app.name)}` : `/${app.prodId}`
|
if (app.url) {
|
||||||
|
return `/app${app.url}`
|
||||||
|
} else {
|
||||||
|
return `/${app.prodId}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
await auth.getSelf()
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -49,7 +49,6 @@
|
||||||
$: filteredApps = enrichedApps.filter(app =>
|
$: filteredApps = enrichedApps.filter(app =>
|
||||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
$: isCloud = $admin.cloud
|
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
|
@ -80,7 +79,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiateAppCreation = () => {
|
const initiateAppCreation = () => {
|
||||||
template = {}
|
template = null
|
||||||
creationModal.show()
|
creationModal.show()
|
||||||
creatingApp = true
|
creatingApp = true
|
||||||
}
|
}
|
||||||
|
@ -162,12 +161,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewApp = app => {
|
const viewApp = app => {
|
||||||
if (!isCloud && app.deployed) {
|
if (app.url) {
|
||||||
// special case to use the short form name if self hosted
|
window.open(`/app${app.url}`)
|
||||||
window.open(`/app/${encodeURIComponent(app.name)}`)
|
|
||||||
} else {
|
} else {
|
||||||
const id = app.deployed ? app.prodId : app.devId
|
window.open(`/${app.prodId}`)
|
||||||
window.open(`/${id}`, "_blank")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,6 +337,14 @@
|
||||||
}}
|
}}
|
||||||
class="template-card"
|
class="template-card"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
target="_blank"
|
||||||
|
class="external-link"
|
||||||
|
on:click|stopPropagation
|
||||||
|
>
|
||||||
|
<Icon name="LinkOut" size="S" />
|
||||||
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div style="color: {item.background}" class="iconAlign">
|
<div style="color: {item.background}" class="iconAlign">
|
||||||
<svg
|
<svg
|
||||||
|
@ -442,6 +447,11 @@
|
||||||
>
|
>
|
||||||
<CreateAppModal {template} />
|
<CreateAppModal {template} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal app={selectedApp} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={deletionModal}
|
bind:this={deletionModal}
|
||||||
title="Confirm deletion"
|
title="Confirm deletion"
|
||||||
|
@ -468,7 +478,6 @@
|
||||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
<UpdateAppModal app={selectedApp} bind:this={updatingModal} />
|
|
||||||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -517,6 +526,7 @@
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-card:hover {
|
.template-card:hover {
|
||||||
|
@ -527,6 +537,18 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.external-link {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
color: var(--spectrum-global-color-gray-300);
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
.external-link:hover {
|
||||||
|
color: var(--spectrum-global-color-gray-500);
|
||||||
|
}
|
||||||
|
|
||||||
.iconAlign {
|
.iconAlign {
|
||||||
padding: 0 0 0 var(--spacing-m);
|
padding: 0 0 0 var(--spacing-m);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -108,11 +108,7 @@ export function createAuthStore() {
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const actions = {
|
||||||
subscribe: store.subscribe,
|
|
||||||
setOrganisation,
|
|
||||||
getInitInfo,
|
|
||||||
setInitInfo,
|
|
||||||
checkQueryString: async () => {
|
checkQueryString: async () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
if (urlParams.has("tenantId")) {
|
if (urlParams.has("tenantId")) {
|
||||||
|
@ -123,7 +119,7 @@ export function createAuthStore() {
|
||||||
setOrg: async tenantId => {
|
setOrg: async tenantId => {
|
||||||
await setOrganisation(tenantId)
|
await setOrganisation(tenantId)
|
||||||
},
|
},
|
||||||
checkAuth: async () => {
|
getSelf: async () => {
|
||||||
const response = await api.get("/api/global/users/self")
|
const response = await api.get("/api/global/users/self")
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
setUser(null)
|
setUser(null)
|
||||||
|
@ -138,13 +134,12 @@ export function createAuthStore() {
|
||||||
`/api/global/auth/${tenantId}/login`,
|
`/api/global/auth/${tenantId}/login`,
|
||||||
creds
|
creds
|
||||||
)
|
)
|
||||||
const json = await response.json()
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
setUser(json.user)
|
await actions.getSelf()
|
||||||
} else {
|
} else {
|
||||||
|
const json = await response.json()
|
||||||
throw new Error(json.message ? json.message : "Invalid credentials")
|
throw new Error(json.message ? json.message : "Invalid credentials")
|
||||||
}
|
}
|
||||||
return json
|
|
||||||
},
|
},
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
const response = await api.post(`/api/global/auth/logout`)
|
const response = await api.post(`/api/global/auth/logout`)
|
||||||
|
@ -197,6 +192,14 @@ export function createAuthStore() {
|
||||||
await response.json()
|
await response.json()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
setOrganisation,
|
||||||
|
getInitInfo,
|
||||||
|
setInitInfo,
|
||||||
|
...actions,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const auth = createAuthStore()
|
export const auth = createAuthStore()
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.44-alpha.9",
|
"version": "1.0.49-alpha.4",
|
||||||
"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": {
|
||||||
|
|
|
@ -3340,5 +3340,50 @@
|
||||||
"suffix": "repeater"
|
"suffix": "repeater"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"s3upload": {
|
||||||
|
"name": "S3 File Upload",
|
||||||
|
"info": "This component can't be used with S3 datasources that use custom endpoints.",
|
||||||
|
"icon": "UploadToCloud",
|
||||||
|
"styles": ["size"],
|
||||||
|
"editable": true,
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field/attachment",
|
||||||
|
"label": "Field",
|
||||||
|
"key": "field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Label",
|
||||||
|
"key": "label"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dataSource/s3",
|
||||||
|
"label": "S3 Datasource",
|
||||||
|
"key": "datasourceId"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "Bucket",
|
||||||
|
"key": "bucket"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"label": "File Name",
|
||||||
|
"key": "key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Disabled",
|
||||||
|
"key": "disabled",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "validation/attachment",
|
||||||
|
"label": "Validation",
|
||||||
|
"key": "validation"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.44-alpha.9",
|
"version": "1.0.49-alpha.4",
|
||||||
"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,10 +19,11 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.44-alpha.9",
|
"@budibase/bbui": "^1.0.49-alpha.4",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@budibase/string-templates": "^1.0.44-alpha.9",
|
"@budibase/string-templates": "^1.0.49-alpha.4",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
|
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
},
|
},
|
||||||
|
@ -45,8 +46,6 @@
|
||||||
"postcss": "^8.2.10",
|
"postcss": "^8.2.10",
|
||||||
"rollup": "^2.44.0",
|
"rollup": "^2.44.0",
|
||||||
"rollup-plugin-json": "^4.0.0",
|
"rollup-plugin-json": "^4.0.0",
|
||||||
"rollup-plugin-node-builtins": "^2.1.2",
|
|
||||||
"rollup-plugin-node-globals": "^1.4.0",
|
|
||||||
"rollup-plugin-postcss": "^4.0.0",
|
"rollup-plugin-postcss": "^4.0.0",
|
||||||
"rollup-plugin-svelte": "^7.1.0",
|
"rollup-plugin-svelte": "^7.1.0",
|
||||||
"rollup-plugin-svg": "^2.0.0",
|
"rollup-plugin-svg": "^2.0.0",
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { terser } from "rollup-plugin-terser"
|
||||||
import postcss from "rollup-plugin-postcss"
|
import postcss from "rollup-plugin-postcss"
|
||||||
import svg from "rollup-plugin-svg"
|
import svg from "rollup-plugin-svg"
|
||||||
import json from "rollup-plugin-json"
|
import json from "rollup-plugin-json"
|
||||||
import builtins from "rollup-plugin-node-builtins"
|
import nodePolyfills from "rollup-plugin-polyfill-node"
|
||||||
import globals from "rollup-plugin-node-globals"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
@ -75,8 +74,7 @@ export default {
|
||||||
}),
|
}),
|
||||||
postcss(),
|
postcss(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
globals(),
|
nodePolyfills(),
|
||||||
builtins(),
|
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true,
|
preferBuiltins: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { notificationStore } from "stores"
|
import { notificationStore, authStore } from "stores"
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { ApiVersion } from "constants"
|
import { ApiVersion } from "constants"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,6 +29,13 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||||
...(json && { "Content-Type": "application/json" }),
|
...(json && { "Content-Type": "application/json" }),
|
||||||
...(!inBuilder && { "x-budibase-type": "client" }),
|
...(!inBuilder && { "x-budibase-type": "client" }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add csrf token if authenticated
|
||||||
|
const auth = get(authStore)
|
||||||
|
if (auth && auth.csrfToken) {
|
||||||
|
headers["x-csrf-token"] = auth.csrfToken
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
|
@ -36,7 +44,11 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||||
})
|
})
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200:
|
case 200:
|
||||||
return response.json()
|
try {
|
||||||
|
return await response.json()
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
case 401:
|
case 401:
|
||||||
notificationStore.actions.error("Invalid credentials")
|
notificationStore.actions.error("Invalid credentials")
|
||||||
return handleError(`Invalid credentials`)
|
return handleError(`Invalid credentials`)
|
||||||
|
@ -82,14 +94,15 @@ const makeCachedApiCall = async params => {
|
||||||
* Constructs an API call function for a particular HTTP method.
|
* Constructs an API call function for a particular HTTP method.
|
||||||
*/
|
*/
|
||||||
const requestApiCall = method => async params => {
|
const requestApiCall = method => async params => {
|
||||||
const { url, cache = false } = params
|
const { external = false, url, cache = false } = params
|
||||||
const fixedUrl = `/${url}`.replace("//", "/")
|
const fixedUrl = external ? url : `/${url}`.replace("//", "/")
|
||||||
const enrichedParams = { ...params, method, url: fixedUrl }
|
const enrichedParams = { ...params, method, url: fixedUrl }
|
||||||
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
|
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
post: requestApiCall("POST"),
|
post: requestApiCall("POST"),
|
||||||
|
put: requestApiCall("PUT"),
|
||||||
get: requestApiCall("GET"),
|
get: requestApiCall("GET"),
|
||||||
patch: requestApiCall("PATCH"),
|
patch: requestApiCall("PATCH"),
|
||||||
del: requestApiCall("DELETE"),
|
del: requestApiCall("DELETE"),
|
||||||
|
|
|
@ -10,3 +10,41 @@ export const uploadAttachment = async (data, tableId = "") => {
|
||||||
json: false,
|
json: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a signed URL to upload a file to an external datasource.
|
||||||
|
*/
|
||||||
|
export const getSignedDatasourceURL = async (datasourceId, bucket, key) => {
|
||||||
|
if (!datasourceId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const res = await API.post({
|
||||||
|
url: `/api/attachments/${datasourceId}/url`,
|
||||||
|
body: { bucket, key },
|
||||||
|
})
|
||||||
|
if (res.error) {
|
||||||
|
throw "Could not generate signed upload URL"
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to an external datasource.
|
||||||
|
*/
|
||||||
|
export const externalUpload = async (datasourceId, bucket, key, data) => {
|
||||||
|
const { signedUrl, publicUrl } = await getSignedDatasourceURL(
|
||||||
|
datasourceId,
|
||||||
|
bucket,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
const res = await API.put({
|
||||||
|
url: signedUrl,
|
||||||
|
body: data,
|
||||||
|
json: false,
|
||||||
|
external: true,
|
||||||
|
})
|
||||||
|
if (res?.error) {
|
||||||
|
throw "Could not upload file to signed URL"
|
||||||
|
}
|
||||||
|
return { publicUrl }
|
||||||
|
}
|
||||||
|
|
|
@ -276,27 +276,29 @@
|
||||||
// reactive statements as much as possible.
|
// reactive statements as much as possible.
|
||||||
const cacheSettings = (enriched, nested, conditional) => {
|
const cacheSettings = (enriched, nested, conditional) => {
|
||||||
const allSettings = { ...enriched, ...nested, ...conditional }
|
const allSettings = { ...enriched, ...nested, ...conditional }
|
||||||
if (!cachedSettings) {
|
const mounted = ref?.$$set != null
|
||||||
|
if (!cachedSettings || !mounted) {
|
||||||
cachedSettings = { ...allSettings }
|
cachedSettings = { ...allSettings }
|
||||||
initialSettings = cachedSettings
|
initialSettings = cachedSettings
|
||||||
} else {
|
} else {
|
||||||
Object.keys(allSettings).forEach(key => {
|
Object.keys(allSettings).forEach(key => {
|
||||||
const same = propsAreSame(allSettings[key], cachedSettings[key])
|
const same = propsAreSame(allSettings[key], cachedSettings[key])
|
||||||
if (!same) {
|
if (!same) {
|
||||||
|
// Updated cachedSettings (which is assigned by reference to
|
||||||
|
// initialSettings) so that if we remount the component then the
|
||||||
|
// initial props are up to date. By setting it this way rather than
|
||||||
|
// setting it on initialSettings directly, we avoid a double render.
|
||||||
cachedSettings[key] = allSettings[key]
|
cachedSettings[key] = allSettings[key]
|
||||||
assignSetting(key, allSettings[key])
|
|
||||||
|
// Programmatically set the prop to avoid svelte reactive statements
|
||||||
|
// firing inside components. This circumvents the problems caused by
|
||||||
|
// spreading a props object.
|
||||||
|
ref.$$set({ [key]: allSettings[key] })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assigns a certain setting to this component.
|
|
||||||
// We manually use the svelte $set function to avoid triggering additional
|
|
||||||
// reactive statements.
|
|
||||||
const assignSetting = (key, value) => {
|
|
||||||
ref?.$$set?.({ [key]: value })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates a key used to determine when components need to fully remount.
|
// Generates a key used to determine when components need to fully remount.
|
||||||
// Currently only toggling editing requires remounting.
|
// Currently only toggling editing requires remounting.
|
||||||
const getRenderKey = (id, editing) => {
|
const getRenderKey = (id, editing) => {
|
||||||
|
@ -305,7 +307,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key renderKey}
|
{#key renderKey}
|
||||||
{#if constructor && cachedSettings && (visible || inSelectedPath)}
|
{#if constructor && initialSettings && (visible || inSelectedPath)}
|
||||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||||
<!-- and the performance matters for the selection indicators -->
|
<!-- and the performance matters for the selection indicators -->
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
number: "numberfield",
|
number: "numberfield",
|
||||||
datetime: "datetimefield",
|
datetime: "datetimefield",
|
||||||
boolean: "booleanfield",
|
boolean: "booleanfield",
|
||||||
|
formula: "stringfield",
|
||||||
}
|
}
|
||||||
|
|
||||||
let formId
|
let formId
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
number: "numberfield",
|
number: "numberfield",
|
||||||
datetime: "datetimefield",
|
datetime: "datetimefield",
|
||||||
boolean: "booleanfield",
|
boolean: "booleanfield",
|
||||||
|
formula: "stringfield",
|
||||||
}
|
}
|
||||||
|
|
||||||
let formId
|
let formId
|
||||||
|
@ -60,10 +61,11 @@
|
||||||
let enrichedFilter = [...(filter || [])]
|
let enrichedFilter = [...(filter || [])]
|
||||||
columns?.forEach(column => {
|
columns?.forEach(column => {
|
||||||
const safePath = column.name.split(".").map(safe).join(".")
|
const safePath = column.name.split(".").map(safe).join(".")
|
||||||
|
const stringType = column.type === "string" || column.type === "formula"
|
||||||
enrichedFilter.push({
|
enrichedFilter.push({
|
||||||
field: column.name,
|
field: column.name,
|
||||||
operator: column.type === "string" ? "string" : "equal",
|
operator: stringType ? "string" : "equal",
|
||||||
type: column.type === "string" ? "string" : "number",
|
type: stringType ? "string" : "number",
|
||||||
valueType: "Binding",
|
valueType: "Binding",
|
||||||
value: `{{ ${safe(formId)}.${safePath} }}`,
|
value: `{{ ${safe(formId)}.${safePath} }}`,
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,10 +19,14 @@
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
|
|
||||||
const BannedTypes = ["link", "attachment", "formula", "json"]
|
const BannedTypes = ["link", "attachment", "json"]
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
.filter(field => !BannedTypes.includes(field.type))
|
.filter(
|
||||||
|
field =>
|
||||||
|
!BannedTypes.includes(field.type) ||
|
||||||
|
(field.type === "formula" && field.formulaType === "static")
|
||||||
|
)
|
||||||
.map(field => field.name)
|
.map(field => field.name)
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
|
@ -114,7 +118,7 @@
|
||||||
on:change={e => onOperatorChange(filter, e.detail)}
|
on:change={e => onOperatorChange(filter, e.detail)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
{#if ["string", "longform", "number"].includes(filter.type)}
|
{#if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||||
{:else if ["options", "array"].includes(filter.type)}
|
{:else if ["options", "array"].includes(filter.type)}
|
||||||
<Combobox
|
<Combobox
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
validation,
|
validation,
|
||||||
formStep
|
formStep
|
||||||
)
|
)
|
||||||
|
$: schemaType = fieldSchema?.type !== "formula" ? fieldSchema?.type : "string"
|
||||||
|
|
||||||
// Focus label when editing
|
// Focus label when editing
|
||||||
let labelNode
|
let labelNode
|
||||||
|
@ -72,7 +73,7 @@
|
||||||
<Placeholder
|
<Placeholder
|
||||||
text="Add the Field setting to start using your component"
|
text="Add the Field setting to start using your component"
|
||||||
/>
|
/>
|
||||||
{:else if fieldSchema?.type && fieldSchema?.type !== type && type !== "options"}
|
{:else if schemaType && schemaType !== type && type !== "options"}
|
||||||
<Placeholder
|
<Placeholder
|
||||||
text="This Field setting is the wrong data type for this component"
|
text="This Field setting is the wrong data type for this component"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import { CoreDropzone, ProgressCircle } from "@budibase/bbui"
|
||||||
|
import { getContext, onMount, onDestroy } from "svelte"
|
||||||
|
|
||||||
|
export let datasourceId
|
||||||
|
export let bucket
|
||||||
|
export let key
|
||||||
|
export let field
|
||||||
|
export let label
|
||||||
|
export let disabled = false
|
||||||
|
export let validation
|
||||||
|
|
||||||
|
let fieldState
|
||||||
|
let fieldApi
|
||||||
|
|
||||||
|
const { API, notificationStore, uploadStore } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
|
// 5GB cap per item sent via S3 REST API
|
||||||
|
const MaxFileSize = 1000000000 * 5
|
||||||
|
|
||||||
|
// Actual file data to upload
|
||||||
|
let data
|
||||||
|
let loading = false
|
||||||
|
|
||||||
|
const handleFileTooLarge = () => {
|
||||||
|
notificationStore.actions.warning(
|
||||||
|
"Files cannot exceed 5GB. Please try again with a smaller file."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the file input and return a serializable structure expected by
|
||||||
|
// the dropzone component to display the file
|
||||||
|
const processFiles = async fileList => {
|
||||||
|
return await new Promise(resolve => {
|
||||||
|
if (!fileList?.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't read in non-image files
|
||||||
|
data = fileList[0]
|
||||||
|
if (!data.type?.startsWith("image")) {
|
||||||
|
resolve([
|
||||||
|
{
|
||||||
|
name: data.name,
|
||||||
|
type: data.type,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read image files and display as preview
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.addEventListener(
|
||||||
|
"load",
|
||||||
|
() => {
|
||||||
|
resolve([
|
||||||
|
{
|
||||||
|
url: reader.result,
|
||||||
|
name: data.name,
|
||||||
|
type: data.type,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
reader.readAsDataURL(fileList[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const upload = async () => {
|
||||||
|
loading = true
|
||||||
|
try {
|
||||||
|
const res = await API.externalUpload(datasourceId, bucket, key, data)
|
||||||
|
notificationStore.actions.success("File uploaded successfully")
|
||||||
|
loading = false
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
notificationStore.actions.error(`Error uploading file: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
uploadStore.actions.registerFileUpload($component.id, upload)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
uploadStore.actions.unregisterFileUpload($component.id)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
{label}
|
||||||
|
{field}
|
||||||
|
{disabled}
|
||||||
|
{validation}
|
||||||
|
type="s3upload"
|
||||||
|
bind:fieldState
|
||||||
|
bind:fieldApi
|
||||||
|
defaultValue={[]}
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
{#if fieldState}
|
||||||
|
<CoreDropzone
|
||||||
|
value={fieldState.value}
|
||||||
|
disabled={loading || fieldState.disabled}
|
||||||
|
error={fieldState.error}
|
||||||
|
on:change={e => {
|
||||||
|
fieldApi.setValue(e.detail)
|
||||||
|
}}
|
||||||
|
{processFiles}
|
||||||
|
{handleFileTooLarge}
|
||||||
|
maximum={1}
|
||||||
|
fileSizeLimit={MaxFileSize}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{#if loading}
|
||||||
|
<div class="overlay" />
|
||||||
|
<div class="loading">
|
||||||
|
<ProgressCircle />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.overlay,
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,3 +12,4 @@ export { default as relationshipfield } from "./RelationshipField.svelte"
|
||||||
export { default as passwordfield } from "./PasswordField.svelte"
|
export { default as passwordfield } from "./PasswordField.svelte"
|
||||||
export { default as formstep } from "./FormStep.svelte"
|
export { default as formstep } from "./FormStep.svelte"
|
||||||
export { default as jsonfield } from "./JSONField.svelte"
|
export { default as jsonfield } from "./JSONField.svelte"
|
||||||
|
export { default as s3upload } from "./S3Upload.svelte"
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
routeStore,
|
routeStore,
|
||||||
screenStore,
|
screenStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
|
uploadStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { styleable } from "utils/styleable"
|
import { styleable } from "utils/styleable"
|
||||||
import { linkable } from "utils/linkable"
|
import { linkable } from "utils/linkable"
|
||||||
|
@ -20,6 +21,7 @@ export default {
|
||||||
routeStore,
|
routeStore,
|
||||||
screenStore,
|
screenStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
|
uploadStore,
|
||||||
styleable,
|
styleable,
|
||||||
linkable,
|
linkable,
|
||||||
getAction,
|
getAction,
|
||||||
|
|
|
@ -9,6 +9,7 @@ export { confirmationStore } from "./confirmation"
|
||||||
export { peekStore } from "./peek"
|
export { peekStore } from "./peek"
|
||||||
export { stateStore } from "./state"
|
export { stateStore } from "./state"
|
||||||
export { themeStore } from "./theme"
|
export { themeStore } from "./theme"
|
||||||
|
export { uploadStore } from "./uploads.js"
|
||||||
|
|
||||||
// Context stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
export { createContextStore } from "./context"
|
export { createContextStore } from "./context"
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
|
||||||
|
export const createUploadStore = () => {
|
||||||
|
const store = writable([])
|
||||||
|
|
||||||
|
// Registers a new file upload component
|
||||||
|
const registerFileUpload = (componentId, callback) => {
|
||||||
|
if (!componentId || !callback) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(state => {
|
||||||
|
state.push({
|
||||||
|
componentId,
|
||||||
|
callback,
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregisters a file upload component
|
||||||
|
const unregisterFileUpload = componentId => {
|
||||||
|
store.update(state => state.filter(c => c.componentId !== componentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processes a file upload for a given component ID
|
||||||
|
const processFileUpload = async componentId => {
|
||||||
|
if (!componentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = get(store).find(c => c.componentId === componentId)
|
||||||
|
return await component?.callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { registerFileUpload, unregisterFileUpload, processFileUpload },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadStore = createUploadStore()
|
|
@ -5,6 +5,7 @@ import {
|
||||||
confirmationStore,
|
confirmationStore,
|
||||||
authStore,
|
authStore,
|
||||||
stateStore,
|
stateStore,
|
||||||
|
uploadStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "constants"
|
||||||
|
@ -169,6 +170,17 @@ const updateStateHandler = action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const s3UploadHandler = async action => {
|
||||||
|
const { componentId } = action.parameters
|
||||||
|
if (!componentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await uploadStore.actions.processFileUpload(componentId)
|
||||||
|
return {
|
||||||
|
publicUrl: res?.publicUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handlerMap = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
["Duplicate Row"]: duplicateRowHandler,
|
["Duplicate Row"]: duplicateRowHandler,
|
||||||
|
@ -183,6 +195,7 @@ const handlerMap = {
|
||||||
["Close Screen Modal"]: closeScreenModalHandler,
|
["Close Screen Modal"]: closeScreenModalHandler,
|
||||||
["Change Form Step"]: changeFormStepHandler,
|
["Change Form Step"]: changeFormStepHandler,
|
||||||
["Update State"]: updateStateHandler,
|
["Update State"]: updateStateHandler,
|
||||||
|
["Upload File to S3"]: s3UploadHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTextMap = {
|
const confirmTextMap = {
|
||||||
|
|
|
@ -110,12 +110,6 @@ export default class DataFetch {
|
||||||
*/
|
*/
|
||||||
async getInitialData() {
|
async getInitialData() {
|
||||||
const { datasource, filter, sortColumn, paginate } = this.options
|
const { datasource, filter, sortColumn, paginate } = this.options
|
||||||
const tableId = datasource?.tableId
|
|
||||||
|
|
||||||
// Ensure table ID exists
|
|
||||||
if (!tableId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch datasource definition and determine feature flags
|
// Fetch datasource definition and determine feature flags
|
||||||
const definition = await this.constructor.getDefinition(datasource)
|
const definition = await this.constructor.getDefinition(datasource)
|
||||||
|
|
|
@ -16,7 +16,12 @@ export default class QueryFetch extends DataFetch {
|
||||||
if (!datasource?._id) {
|
if (!datasource?._id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return await fetchQueryDefinition(datasource._id)
|
const definition = await fetchQueryDefinition(datasource._id)
|
||||||
|
// After getting the definition of query, it loses "fields" attribute because of security reason from the server. However, this attribute needs to be inside of defintion for pagination.
|
||||||
|
if (!definition.fields) {
|
||||||
|
definition.fields = datasource.fields
|
||||||
|
}
|
||||||
|
return definition
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData() {
|
async getData() {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"watch": ["src", "../auth"],
|
"watch": ["src", "../backend-core"],
|
||||||
"ext": "js,ts,json",
|
"ext": "js,ts,json",
|
||||||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"],
|
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"],
|
||||||
"exec": "ts-node src/index.ts"
|
"exec": "ts-node src/index.ts"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.44-alpha.9",
|
"version": "1.0.49-alpha.4",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -70,9 +70,9 @@
|
||||||
"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.44-alpha.9",
|
"@budibase/backend-core": "^1.0.49-alpha.4",
|
||||||
"@budibase/client": "^1.0.44-alpha.9",
|
"@budibase/client": "^1.0.49-alpha.4",
|
||||||
"@budibase/string-templates": "^1.0.44-alpha.9",
|
"@budibase/string-templates": "^1.0.49-alpha.4",
|
||||||
"@bull-board/api": "^3.7.0",
|
"@bull-board/api": "^3.7.0",
|
||||||
"@bull-board/koa": "^3.7.0",
|
"@bull-board/koa": "^3.7.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
@ -92,6 +92,8 @@
|
||||||
"fix-path": "3.0.0",
|
"fix-path": "3.0.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"fs-extra": "8.1.0",
|
"fs-extra": "8.1.0",
|
||||||
|
"google-auth-library": "^7.11.0",
|
||||||
|
"google-spreadsheet": "^3.2.0",
|
||||||
"jimp": "0.16.1",
|
"jimp": "0.16.1",
|
||||||
"joi": "17.2.1",
|
"joi": "17.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
@ -110,7 +112,7 @@
|
||||||
"mongodb": "3.6.3",
|
"mongodb": "3.6.3",
|
||||||
"mssql": "6.2.3",
|
"mssql": "6.2.3",
|
||||||
"mysql2": "^2.3.1",
|
"mysql2": "^2.3.1",
|
||||||
"node-fetch": "2.6.0",
|
"node-fetch": "2.6.7",
|
||||||
"open": "^8.4.0",
|
"open": "^8.4.0",
|
||||||
"pg": "8.5.1",
|
"pg": "8.5.1",
|
||||||
"pino-pretty": "4.0.0",
|
"pino-pretty": "4.0.0",
|
||||||
|
@ -139,6 +141,7 @@
|
||||||
"@jest/test-sequencer": "^24.8.0",
|
"@jest/test-sequencer": "^24.8.0",
|
||||||
"@types/apidoc": "^0.50.0",
|
"@types/apidoc": "^0.50.0",
|
||||||
"@types/bull": "^3.15.1",
|
"@types/bull": "^3.15.1",
|
||||||
|
"@types/google-spreadsheet": "^3.1.5",
|
||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/koa": "^2.13.3",
|
"@types/koa": "^2.13.3",
|
||||||
"@types/koa-router": "^7.4.2",
|
"@types/koa-router": "^7.4.2",
|
||||||
|
|
|
@ -33,10 +33,7 @@ const {
|
||||||
Replication,
|
Replication,
|
||||||
} = require("@budibase/backend-core/db")
|
} = require("@budibase/backend-core/db")
|
||||||
const { USERS_TABLE_SCHEMA } = require("../../constants")
|
const { USERS_TABLE_SCHEMA } = require("../../constants")
|
||||||
const {
|
const { removeAppFromUserRoles } = require("../../utilities/workerRequests")
|
||||||
getDeployedApps,
|
|
||||||
removeAppFromUserRoles,
|
|
||||||
} = require("../../utilities/workerRequests")
|
|
||||||
const { clientLibraryPath, stringToReadStream } = require("../../utilities")
|
const { clientLibraryPath, stringToReadStream } = require("../../utilities")
|
||||||
const { getAllLocks } = require("../../utilities/redis")
|
const { getAllLocks } = require("../../utilities/redis")
|
||||||
const {
|
const {
|
||||||
|
@ -78,29 +75,42 @@ function getUserRoleId(ctx) {
|
||||||
: ctx.user.role._id
|
: ctx.user.role._id
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAppUrlIfNotInUse(ctx) {
|
exports.getAppUrl = ctx => {
|
||||||
|
// construct the url
|
||||||
let url
|
let url
|
||||||
if (ctx.request.body.url) {
|
if (ctx.request.body.url) {
|
||||||
|
// if the url is provided, use that
|
||||||
url = encodeURI(ctx.request.body.url)
|
url = encodeURI(ctx.request.body.url)
|
||||||
} else if (ctx.request.body.name) {
|
} else if (ctx.request.body.name) {
|
||||||
|
// otherwise use the name
|
||||||
url = encodeURI(`${ctx.request.body.name}`)
|
url = encodeURI(`${ctx.request.body.name}`)
|
||||||
}
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase()
|
url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase()
|
||||||
}
|
}
|
||||||
if (!env.SELF_HOSTED) {
|
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
const deployedApps = await getDeployedApps()
|
|
||||||
if (
|
const checkAppUrl = (ctx, apps, url, currentAppId) => {
|
||||||
url &&
|
if (currentAppId) {
|
||||||
deployedApps[url] != null &&
|
apps = apps.filter(app => app.appId !== currentAppId)
|
||||||
ctx.params != null &&
|
}
|
||||||
deployedApps[url].appId !== ctx.params.appId
|
if (apps.some(app => app.url === url)) {
|
||||||
) {
|
ctx.throw(400, "App URL is already in use.")
|
||||||
ctx.throw(400, "App name/URL is already in use.")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkAppName = (ctx, apps, name, currentAppId) => {
|
||||||
|
// TODO: Replace with Joi
|
||||||
|
if (!name) {
|
||||||
|
ctx.throw(400, "Name is required")
|
||||||
|
}
|
||||||
|
if (currentAppId) {
|
||||||
|
apps = apps.filter(app => app.appId !== currentAppId)
|
||||||
|
}
|
||||||
|
if (apps.some(app => app.name === name)) {
|
||||||
|
ctx.throw(400, "App name is already in use.")
|
||||||
}
|
}
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInstance(template) {
|
async function createInstance(template) {
|
||||||
|
@ -206,6 +216,12 @@ exports.fetchAppPackage = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async ctx => {
|
exports.create = async ctx => {
|
||||||
|
const apps = await getAllApps(CouchDB, { dev: true })
|
||||||
|
const name = ctx.request.body.name
|
||||||
|
checkAppName(ctx, apps, name)
|
||||||
|
const url = exports.getAppUrl(ctx)
|
||||||
|
checkAppUrl(ctx, apps, url)
|
||||||
|
|
||||||
const { useTemplate, templateKey, templateString } = ctx.request.body
|
const { useTemplate, templateKey, templateString } = ctx.request.body
|
||||||
const instanceConfig = {
|
const instanceConfig = {
|
||||||
useTemplate,
|
useTemplate,
|
||||||
|
@ -218,7 +234,6 @@ exports.create = async ctx => {
|
||||||
const instance = await createInstance(instanceConfig)
|
const instance = await createInstance(instanceConfig)
|
||||||
const appId = instance._id
|
const appId = instance._id
|
||||||
|
|
||||||
const url = await getAppUrlIfNotInUse(ctx)
|
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
let _rev
|
let _rev
|
||||||
try {
|
try {
|
||||||
|
@ -235,7 +250,7 @@ exports.create = async ctx => {
|
||||||
type: "app",
|
type: "app",
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
componentLibraries: ["@budibase/standard-components"],
|
componentLibraries: ["@budibase/standard-components"],
|
||||||
name: ctx.request.body.name,
|
name: name,
|
||||||
url: url,
|
url: url,
|
||||||
template: ctx.request.body.template,
|
template: ctx.request.body.template,
|
||||||
instance: instance,
|
instance: instance,
|
||||||
|
@ -262,8 +277,22 @@ exports.create = async ctx => {
|
||||||
ctx.body = newApplication
|
ctx.body = newApplication
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This endpoint currently operates as a PATCH rather than a PUT
|
||||||
|
// Thus name and url fields are handled only if present
|
||||||
exports.update = async ctx => {
|
exports.update = async ctx => {
|
||||||
const data = await updateAppPackage(ctx, ctx.request.body, ctx.params.appId)
|
const apps = await getAllApps(CouchDB, { dev: true })
|
||||||
|
// validation
|
||||||
|
const name = ctx.request.body.name
|
||||||
|
if (name) {
|
||||||
|
checkAppName(ctx, apps, name, ctx.params.appId)
|
||||||
|
}
|
||||||
|
const url = await exports.getAppUrl(ctx)
|
||||||
|
if (url) {
|
||||||
|
checkAppUrl(ctx, apps, url, ctx.params.appId)
|
||||||
|
ctx.request.body.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await updateAppPackage(ctx.request.body, ctx.params.appId)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = data
|
ctx.body = data
|
||||||
}
|
}
|
||||||
|
@ -285,7 +314,7 @@ exports.updateClient = async ctx => {
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
revertableVersion: currentVersion,
|
revertableVersion: currentVersion,
|
||||||
}
|
}
|
||||||
const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId)
|
const data = await updateAppPackage(appPackageUpdates, ctx.params.appId)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = data
|
ctx.body = data
|
||||||
}
|
}
|
||||||
|
@ -308,7 +337,7 @@ exports.revertClient = async ctx => {
|
||||||
version: application.revertableVersion,
|
version: application.revertableVersion,
|
||||||
revertableVersion: null,
|
revertableVersion: null,
|
||||||
}
|
}
|
||||||
const data = await updateAppPackage(ctx, appPackageUpdates, ctx.params.appId)
|
const data = await updateAppPackage(appPackageUpdates, ctx.params.appId)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = data
|
ctx.body = data
|
||||||
}
|
}
|
||||||
|
@ -381,12 +410,11 @@ exports.sync = async (ctx, next) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAppPackage = async (ctx, appPackage, appId) => {
|
const updateAppPackage = async (appPackage, appId) => {
|
||||||
const url = await getAppUrlIfNotInUse(ctx)
|
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
const application = await db.get(DocumentTypes.APP_METADATA)
|
const application = await db.get(DocumentTypes.APP_METADATA)
|
||||||
|
|
||||||
const newAppPackage = { ...application, ...appPackage, url }
|
const newAppPackage = { ...application, ...appPackage }
|
||||||
if (appPackage._rev !== application._rev) {
|
if (appPackage._rev !== application._rev) {
|
||||||
newAppPackage._rev = application._rev
|
newAppPackage._rev = application._rev
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue