forked from tsai/budibase
Browse Source
# Conflicts: # packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.sveltegh-pages
236 changed files with 3916 additions and 1204 deletions
@ -0,0 +1,14 @@ |
|||
--- |
|||
name: Feature Request |
|||
about: Request a new budibase feature or enhancement |
|||
title: '' |
|||
labels: enhancement |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Describe the feature request** |
|||
A clear and concise description of what the feature request. |
|||
|
|||
**Screenshots** |
|||
If applicable, add screenshots to help explain your problem. |
|||
@ -1 +1,4 @@ |
|||
module.exports = require("./src/db/utils") |
|||
module.exports = { |
|||
...require("./src/db/utils"), |
|||
...require("./src/db/constants"), |
|||
} |
|||
|
|||
@ -0,0 +1,17 @@ |
|||
exports.SEPARATOR = "_" |
|||
|
|||
exports.StaticDatabases = { |
|||
GLOBAL: { |
|||
name: "global-db", |
|||
docs: { |
|||
apiKeys: "apikeys", |
|||
}, |
|||
}, |
|||
// contains information about tenancy and so on
|
|||
PLATFORM_INFO: { |
|||
name: "global-info", |
|||
docs: { |
|||
tenants: "tenants", |
|||
}, |
|||
}, |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g |
|||
|
|||
exports.buildMatcherRegex = patterns => { |
|||
if (!patterns) { |
|||
return [] |
|||
} |
|||
return patterns.map(pattern => { |
|||
const isObj = typeof pattern === "object" && pattern.route |
|||
const method = isObj ? pattern.method : "GET" |
|||
let route = isObj ? pattern.route : pattern |
|||
|
|||
const matches = route.match(PARAM_REGEX) |
|||
if (matches) { |
|||
for (let match of matches) { |
|||
const pattern = "/.*" + (match.endsWith("/") ? "/" : "") |
|||
route = route.replace(match, pattern) |
|||
} |
|||
} |
|||
return { regex: new RegExp(route), method } |
|||
}) |
|||
} |
|||
|
|||
exports.matches = (ctx, options) => { |
|||
return options.find(({ regex, method }) => { |
|||
const urlMatch = regex.test(ctx.request.url) |
|||
const methodMatch = |
|||
method === "ALL" |
|||
? true |
|||
: ctx.request.method.toLowerCase() === method.toLowerCase() |
|||
|
|||
return urlMatch && methodMatch |
|||
}) |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
const { setTenantId } = require("../tenancy") |
|||
const ContextFactory = require("../tenancy/FunctionContext") |
|||
const { buildMatcherRegex, matches } = require("./matchers") |
|||
|
|||
module.exports = (allowQueryStringPatterns, noTenancyPatterns) => { |
|||
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) |
|||
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) |
|||
|
|||
return ContextFactory.getMiddleware(ctx => { |
|||
const allowNoTenant = !!matches(ctx, noTenancyOptions) |
|||
const allowQs = !!matches(ctx, allowQsOptions) |
|||
setTenantId(ctx, { allowQs, allowNoTenant }) |
|||
}) |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
const cls = require("cls-hooked") |
|||
const { newid } = require("../hashing") |
|||
|
|||
const REQUEST_ID_KEY = "requestId" |
|||
|
|||
class FunctionContext { |
|||
static getMiddleware(updateCtxFn = null) { |
|||
const namespace = this.createNamespace() |
|||
|
|||
return async function (ctx, next) { |
|||
await new Promise( |
|||
namespace.bind(function (resolve, reject) { |
|||
// store a contextual request ID that can be used anywhere (audit logs)
|
|||
namespace.set(REQUEST_ID_KEY, newid()) |
|||
namespace.bindEmitter(ctx.req) |
|||
namespace.bindEmitter(ctx.res) |
|||
|
|||
if (updateCtxFn) { |
|||
updateCtxFn(ctx) |
|||
} |
|||
next().then(resolve).catch(reject) |
|||
}) |
|||
) |
|||
} |
|||
} |
|||
|
|||
static run(callback) { |
|||
const namespace = this.createNamespace() |
|||
|
|||
return namespace.runAndReturn(callback) |
|||
} |
|||
|
|||
static setOnContext(key, value) { |
|||
const namespace = this.createNamespace() |
|||
namespace.set(key, value) |
|||
} |
|||
|
|||
static getContextStorage() { |
|||
if (this._namespace && this._namespace.active) { |
|||
let contextData = this._namespace.active |
|||
delete contextData.id |
|||
delete contextData._ns_name |
|||
return contextData |
|||
} |
|||
|
|||
return {} |
|||
} |
|||
|
|||
static getFromContext(key) { |
|||
const context = this.getContextStorage() |
|||
if (context) { |
|||
return context[key] |
|||
} else { |
|||
return null |
|||
} |
|||
} |
|||
|
|||
static destroyNamespace() { |
|||
if (this._namespace) { |
|||
cls.destroyNamespace("session") |
|||
this._namespace = null |
|||
} |
|||
} |
|||
|
|||
static createNamespace() { |
|||
if (!this._namespace) { |
|||
this._namespace = cls.createNamespace("session") |
|||
} |
|||
return this._namespace |
|||
} |
|||
} |
|||
|
|||
module.exports = FunctionContext |
|||
@ -0,0 +1,81 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../../constants") |
|||
const cls = require("./FunctionContext") |
|||
|
|||
exports.DEFAULT_TENANT_ID = "default" |
|||
|
|||
exports.isDefaultTenant = () => { |
|||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID |
|||
} |
|||
|
|||
exports.isMultiTenant = () => { |
|||
return env.MULTI_TENANCY |
|||
} |
|||
|
|||
const TENANT_ID = "tenantId" |
|||
|
|||
// used for automations, API endpoints should always be in context already
|
|||
exports.doInTenant = (tenantId, task) => { |
|||
return cls.run(() => { |
|||
// set the tenant id
|
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
|
|||
// invoke the task
|
|||
const result = task() |
|||
|
|||
return result |
|||
}) |
|||
} |
|||
|
|||
exports.updateTenantId = tenantId => { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
|
|||
exports.setTenantId = ( |
|||
ctx, |
|||
opts = { allowQs: false, allowNoTenant: false } |
|||
) => { |
|||
let tenantId |
|||
// exit early if not multi-tenant
|
|||
if (!exports.isMultiTenant()) { |
|||
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID) |
|||
return |
|||
} |
|||
|
|||
const allowQs = opts && opts.allowQs |
|||
const allowNoTenant = opts && opts.allowNoTenant |
|||
const header = ctx.request.headers[Headers.TENANT_ID] |
|||
const user = ctx.user || {} |
|||
if (allowQs) { |
|||
const query = ctx.request.query || {} |
|||
tenantId = query.tenantId |
|||
} |
|||
// override query string (if allowed) by user, or header
|
|||
// URL params cannot be used in a middleware, as they are
|
|||
// processed later in the chain
|
|||
tenantId = user.tenantId || header || tenantId |
|||
|
|||
if (!tenantId && !allowNoTenant) { |
|||
ctx.throw(403, "Tenant id not set") |
|||
} |
|||
// check tenant ID just incase no tenant was allowed
|
|||
if (tenantId) { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
} |
|||
|
|||
exports.isTenantIdSet = () => { |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
return !!tenantId |
|||
} |
|||
|
|||
exports.getTenantId = () => { |
|||
if (!exports.isMultiTenant()) { |
|||
return exports.DEFAULT_TENANT_ID |
|||
} |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
if (!tenantId) { |
|||
throw Error("Tenant id not found") |
|||
} |
|||
return tenantId |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
module.exports = { |
|||
...require("./context"), |
|||
...require("./tenancy"), |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
const { getDB } = require("../db") |
|||
const { SEPARATOR, StaticDatabases } = require("../db/constants") |
|||
const { getTenantId, DEFAULT_TENANT_ID, isMultiTenant } = require("./context") |
|||
const env = require("../environment") |
|||
|
|||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants |
|||
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name |
|||
|
|||
exports.addTenantToUrl = url => { |
|||
const tenantId = getTenantId() |
|||
|
|||
if (isMultiTenant()) { |
|||
const char = url.indexOf("?") === -1 ? "?" : "&" |
|||
url += `${char}tenantId=${tenantId}` |
|||
} |
|||
|
|||
return url |
|||
} |
|||
|
|||
exports.doesTenantExist = async tenantId => { |
|||
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 false |
|||
} |
|||
return ( |
|||
tenants && |
|||
Array.isArray(tenants.tenantIds) && |
|||
tenants.tenantIds.indexOf(tenantId) !== -1 |
|||
) |
|||
} |
|||
|
|||
exports.tryAddTenant = async (tenantId, userId, email) => { |
|||
const db = getDB(PLATFORM_INFO_DB) |
|||
const getDoc = async id => { |
|||
if (!id) { |
|||
return null |
|||
} |
|||
try { |
|||
return await db.get(id) |
|||
} catch (err) { |
|||
return { _id: id } |
|||
} |
|||
} |
|||
let [tenants, userIdDoc, emailDoc] = await Promise.all([ |
|||
getDoc(TENANT_DOC), |
|||
getDoc(userId), |
|||
getDoc(email), |
|||
]) |
|||
if (!Array.isArray(tenants.tenantIds)) { |
|||
tenants = { |
|||
_id: TENANT_DOC, |
|||
tenantIds: [], |
|||
} |
|||
} |
|||
let promises = [] |
|||
if (userIdDoc) { |
|||
userIdDoc.tenantId = tenantId |
|||
promises.push(db.put(userIdDoc)) |
|||
} |
|||
if (emailDoc) { |
|||
emailDoc.tenantId = tenantId |
|||
promises.push(db.put(emailDoc)) |
|||
} |
|||
if (tenants.tenantIds.indexOf(tenantId) === -1) { |
|||
tenants.tenantIds.push(tenantId) |
|||
promises.push(db.put(tenants)) |
|||
} |
|||
await Promise.all(promises) |
|||
} |
|||
|
|||
exports.getGlobalDB = (tenantId = null) => { |
|||
// tenant ID can be set externally, for example user API where
|
|||
// new tenants are being created, this may be the case
|
|||
if (!tenantId) { |
|||
tenantId = getTenantId() |
|||
} |
|||
|
|||
let dbName |
|||
|
|||
if (tenantId === DEFAULT_TENANT_ID) { |
|||
dbName = StaticDatabases.GLOBAL.name |
|||
} else { |
|||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` |
|||
} |
|||
|
|||
return getDB(dbName) |
|||
} |
|||
|
|||
exports.lookupTenantId = async userId => { |
|||
const db = getDB(StaticDatabases.PLATFORM_INFO.name) |
|||
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null |
|||
try { |
|||
const doc = await db.get(userId) |
|||
if (doc && doc.tenantId) { |
|||
tenantId = doc.tenantId |
|||
} |
|||
} catch (err) { |
|||
// just return the default
|
|||
} |
|||
return tenantId |
|||
} |
|||
@ -0,0 +1 @@ |
|||
module.exports = require("./src/tenancy") |
|||
@ -0,0 +1,20 @@ |
|||
<script> |
|||
export let type = "info" |
|||
export let icon = "Info" |
|||
export let message = "" |
|||
</script> |
|||
|
|||
<div class="spectrum-Toast spectrum-Toast--{type}"> |
|||
{#if icon} |
|||
<svg |
|||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-icon-18-{icon}" /> |
|||
</svg> |
|||
{/if} |
|||
<div class="spectrum-Toast-body"> |
|||
<div class="spectrum-Toast-content">{message || ""}</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,35 @@ |
|||
<script> |
|||
import { Select, Label } from "@budibase/bbui" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { getActionProviderComponents } from "builderStore/dataBinding" |
|||
|
|||
export let parameters |
|||
|
|||
$: actionProviders = getActionProviderComponents( |
|||
$currentAsset, |
|||
$store.selectedComponentId, |
|||
"ClearForm" |
|||
) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Label small>Form</Label> |
|||
<Select |
|||
bind:value={parameters.componentId} |
|||
options={actionProviders} |
|||
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: 60px 1fr; |
|||
align-items: center; |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,17 @@ |
|||
<script> |
|||
import { Body } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Body size="S">This action doesn't require any additional settings.</Body> |
|||
<Body size="S"> |
|||
This action won't do anything if there isn't a screen modal open. |
|||
</Body> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,48 @@ |
|||
<script> |
|||
import { Select, Label, Input } from "@budibase/bbui" |
|||
|
|||
/** |
|||
* This component takes the query object and populates the 'extra' property |
|||
* when a datasource has specified a configuration for these fields in SCHEMA.extra |
|||
*/ |
|||
export let populateExtraQuery |
|||
export let config |
|||
export let query |
|||
|
|||
$: extraFields = Object.keys(config).map(key => ({ |
|||
...config[key], |
|||
key, |
|||
})) |
|||
|
|||
$: extraQueryFields = query.fields.extra || {} |
|||
</script> |
|||
|
|||
{#each extraFields as { key, displayName, type }} |
|||
<div class="config-field"> |
|||
<Label>{displayName}</Label> |
|||
{#if type === "string"} |
|||
<Input |
|||
on:change={() => populateExtraQuery(extraQueryFields)} |
|||
bind:value={extraQueryFields[key]} |
|||
/> |
|||
{/if} |
|||
|
|||
{#if type === "list"} |
|||
<Select |
|||
on:change={() => populateExtraQuery(extraQueryFields)} |
|||
bind:value={extraQueryFields[key]} |
|||
options={config[key].data[query.queryVerb]} |
|||
getOptionLabel={current => current} |
|||
/> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
|
|||
<style> |
|||
.config-field { |
|||
display: grid; |
|||
grid-template-columns: 20% 1fr; |
|||
grid-gap: var(--spacing-l); |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,111 @@ |
|||
<script> |
|||
import { writable, get as svelteGet } from "svelte/store" |
|||
import { |
|||
notifications, |
|||
Input, |
|||
Modal, |
|||
ModalContent, |
|||
Body, |
|||
} from "@budibase/bbui" |
|||
import { hostingStore } from "builderStore" |
|||
import { apps } from "stores/portal" |
|||
import { string, object } from "yup" |
|||
import { onMount } from "svelte" |
|||
import { capitalise } from "helpers" |
|||
|
|||
const values = writable({ name: null }) |
|||
const errors = writable({}) |
|||
const touched = writable({}) |
|||
const validator = { |
|||
name: string().required("Your application must have a name"), |
|||
} |
|||
|
|||
export let app |
|||
|
|||
let modal |
|||
let valid = false |
|||
let dirty = false |
|||
$: checkValidity($values, validator) |
|||
$: { |
|||
// prevent validation by setting name to undefined without an app |
|||
if (app) { |
|||
$values.name = app?.name |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await hostingStore.actions.fetchDeployedApps() |
|||
const existingAppNames = svelteGet(hostingStore).deployedAppNames |
|||
validator.name = string() |
|||
.required("Your application must have a name") |
|||
.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 obj = object().shape(validator) |
|||
Object.keys(validator).forEach(key => ($errors[key] = null)) |
|||
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 updateApp() { |
|||
try { |
|||
// Update App |
|||
await apps.update(app.instance._id, $values.name) |
|||
hide() |
|||
} catch (error) { |
|||
console.error(error) |
|||
notifications.error(error) |
|||
} |
|||
} |
|||
|
|||
export const show = () => { |
|||
modal.show() |
|||
} |
|||
export const hide = () => { |
|||
modal.hide() |
|||
} |
|||
|
|||
const onCancel = () => { |
|||
hide() |
|||
} |
|||
|
|||
const onShow = () => { |
|||
dirty = false |
|||
} |
|||
</script> |
|||
|
|||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}> |
|||
<ModalContent |
|||
title={"Update app"} |
|||
confirmText={"Update app"} |
|||
onConfirm={updateApp} |
|||
disabled={!(valid && dirty)} |
|||
> |
|||
<Body size="S"> |
|||
Give your new app a name, and choose which groups have access (paid plans |
|||
only). |
|||
</Body> |
|||
<Input |
|||
bind:value={$values.name} |
|||
error={$touched.name && $errors.name} |
|||
on:blur={() => ($touched.name = true)} |
|||
on:change={() => (dirty = true)} |
|||
label="Name" |
|||
/> |
|||
</ModalContent> |
|||
</Modal> |
|||
@ -1,4 +1,24 @@ |
|||
<script> |
|||
import { redirect } from "@roxi/routify" |
|||
$redirect("./login") |
|||
import { auth, admin } from "stores/portal" |
|||
import { onMount } from "svelte" |
|||
|
|||
$: tenantSet = $auth.tenantSet |
|||
$: multiTenancyEnabled = $admin.multiTenancy |
|||
|
|||
let loaded = false |
|||
|
|||
$: { |
|||
if (loaded && multiTenancyEnabled && !tenantSet) { |
|||
$redirect("./org") |
|||
} else if (loaded) { |
|||
$redirect("./login") |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await admin.init() |
|||
await auth.checkQueryString() |
|||
loaded = true |
|||
}) |
|||
</script> |
|||
|
|||
@ -0,0 +1,73 @@ |
|||
<script> |
|||
import { Body, Button, Divider, Heading, Input, Layout } from "@budibase/bbui" |
|||
import { goto } from "@roxi/routify" |
|||
import { auth, admin } from "stores/portal" |
|||
import Logo from "assets/bb-emblem.svg" |
|||
import { get } from "svelte/store" |
|||
import { onMount } from "svelte" |
|||
|
|||
let tenantId = get(auth).tenantSet ? get(auth).tenantId : "" |
|||
$: multiTenancyEnabled = $admin.multiTenancy |
|||
|
|||
async function setOrg() { |
|||
if (tenantId == null || tenantId === "") { |
|||
tenantId = "default" |
|||
} |
|||
await auth.setOrg(tenantId) |
|||
// re-init now org selected |
|||
await admin.init() |
|||
$goto("../") |
|||
} |
|||
|
|||
function handleKeydown(evt) { |
|||
if (evt.key === "Enter") setOrg() |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await auth.checkQueryString() |
|||
if (!multiTenancyEnabled) { |
|||
$goto("../") |
|||
} else { |
|||
admin.unload() |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<svelte:window on:keydown={handleKeydown} /> |
|||
<div class="login"> |
|||
<div class="main"> |
|||
<Layout> |
|||
<Layout noPadding justifyItems="center"> |
|||
<img alt="logo" src={Logo} /> |
|||
<Heading>Set Budibase organisation</Heading> |
|||
</Layout> |
|||
<Divider noGrid /> |
|||
<Layout gap="XS" noPadding> |
|||
<Body size="S" textAlign="center">Set organisation</Body> |
|||
<Input label="Organisation" bind:value={tenantId} /> |
|||
</Layout> |
|||
<Layout gap="XS" noPadding> |
|||
<Button cta on:click={setOrg}>Set organisation</Button> |
|||
</Layout> |
|||
</Layout> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.login { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.main { |
|||
width: 300px; |
|||
} |
|||
|
|||
img { |
|||
width: 48px; |
|||
} |
|||
</style> |
|||
@ -1,4 +1,11 @@ |
|||
<script> |
|||
import { redirect } from "@roxi/routify" |
|||
$redirect("./builder") |
|||
import { auth } from "../stores/portal" |
|||
import { onMount } from "svelte" |
|||
|
|||
auth.checkQueryString() |
|||
|
|||
onMount(() => { |
|||
$redirect(`./builder`) |
|||
}) |
|||
</script> |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue