Merge branch 'master' of github.com:budibase/budibase into budi-7754-make-our-helm-chart-work-out-of-the-box

This commit is contained in:
Sam Rose 2023-11-22 14:45:54 +00:00
commit 2fb9748979
No known key found for this signature in database
23 changed files with 357 additions and 270 deletions

View File

@ -1,24 +0,0 @@
#!/bin/bash
echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views}
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
else
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
fi

View File

@ -25,7 +25,7 @@ if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) -
healthy=false healthy=false
fi fi
if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; then
echo 'ERROR: CouchDB is not running'; echo 'ERROR: CouchDB is not running';
healthy=false healthy=false
fi fi

View File

@ -2,7 +2,7 @@ import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { getGlobalDB, getIdentity, getTenantId } from "../context" import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"
@ -10,12 +10,10 @@ import * as sessions from "../security/sessions"
import * as usersCore from "./users" import * as usersCore from "./users"
import { import {
Account, Account,
AllDocsResponse,
BulkUserCreated, BulkUserCreated,
BulkUserDeleted, BulkUserDeleted,
isSSOAccount, isSSOAccount,
isSSOUser, isSSOUser,
RowResponse,
SaveUserOpts, SaveUserOpts,
User, User,
UserStatus, UserStatus,
@ -487,6 +485,37 @@ export class UserDB {
await sessions.invalidateSessions(userId, { reason: "deletion" }) await sessions.invalidateSessions(userId, { reason: "deletion" })
} }
static async createAdminUser(
email: string,
password: string,
tenantId: string,
opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean }
) {
const user: User = {
email: email,
password: password,
createdAt: Date.now(),
roles: {},
builder: {
global: true,
},
admin: {
global: true,
},
tenantId,
}
if (opts?.ssoId) {
user.ssoId = opts.ssoId
}
// always bust checklist beforehand, if an error occurs but can proceed, don't get
// stuck in a cycle
await cache.bustCache(cache.CacheKey.CHECKLIST)
return await UserDB.save(user, {
hashPassword: opts?.hashPassword,
requirePassword: opts?.requirePassword,
})
}
static async getGroups(groupIds: string[]) { static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds) return await this.groups.getBulk(groupIds)
} }

View File

@ -43,7 +43,7 @@ function removeUserPassword(users: User | User[]) {
return users return users
} }
export const isSupportedUserSearch = (query: SearchQuery) => { export function isSupportedUserSearch(query: SearchQuery) {
const allowed = [ const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" }, { op: SearchQueryOperators.EQUAL, key: "_id" },
@ -68,10 +68,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => {
return true return true
} }
export const bulkGetGlobalUsersById = async ( export async function bulkGetGlobalUsersById(
userIds: string[], userIds: string[],
opts?: GetOpts opts?: GetOpts
) => { ) {
const db = getGlobalDB() const db = getGlobalDB()
let users = ( let users = (
await db.allDocs({ await db.allDocs({
@ -85,7 +85,7 @@ export const bulkGetGlobalUsersById = async (
return users return users
} }
export const getAllUserIds = async () => { export async function getAllUserIds() {
const db = getGlobalDB() const db = getGlobalDB()
const startKey = `${DocumentType.USER}${SEPARATOR}` const startKey = `${DocumentType.USER}${SEPARATOR}`
const response = await db.allDocs({ const response = await db.allDocs({
@ -95,7 +95,7 @@ export const getAllUserIds = async () => {
return response.rows.map(row => row.id) return response.rows.map(row => row.id)
} }
export const bulkUpdateGlobalUsers = async (users: User[]) => { export async function bulkUpdateGlobalUsers(users: User[]) {
const db = getGlobalDB() const db = getGlobalDB()
return (await db.bulkDocs(users)) as BulkDocsResponse return (await db.bulkDocs(users)) as BulkDocsResponse
} }
@ -113,10 +113,10 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
* all the users to find one with this email address. * all the users to find one with this email address.
*/ */
export const getGlobalUserByEmail = async ( export async function getGlobalUserByEmail(
email: String, email: String,
opts?: GetOpts opts?: GetOpts
): Promise<User | undefined> => { ): Promise<User | undefined> {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
@ -139,11 +139,23 @@ export const getGlobalUserByEmail = async (
return user return user
} }
export const searchGlobalUsersByApp = async ( export async function doesUserExist(email: string) {
try {
const user = await getGlobalUserByEmail(email)
if (Array.isArray(user) || user != null) {
return true
}
} catch (err) {
return false
}
return false
}
export async function searchGlobalUsersByApp(
appId: any, appId: any,
opts: DatabaseQueryOpts, opts: DatabaseQueryOpts,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof appId !== "string") { if (typeof appId !== "string") {
throw new Error("Must provide a string based app ID") throw new Error("Must provide a string based app ID")
} }
@ -167,10 +179,10 @@ export const searchGlobalUsersByApp = async (
Return any user who potentially has access to the application Return any user who potentially has access to the application
Admins, developers and app users with the explicitly role. Admins, developers and app users with the explicitly role.
*/ */
export const searchGlobalUsersByAppAccess = async ( export async function searchGlobalUsersByAppAccess(
appId: any, appId: any,
opts?: { limit?: number } opts?: { limit?: number }
) => { ) {
const roleSelector = `roles.${appId}` const roleSelector = `roles.${appId}`
let orQuery: any[] = [ let orQuery: any[] = [
@ -205,7 +217,7 @@ export const searchGlobalUsersByAppAccess = async (
return resp.rows return resp.rows
} }
export const getGlobalUserByAppPage = (appId: string, user: User) => { export function getGlobalUserByAppPage(appId: string, user: User) {
if (!user) { if (!user) {
return return
} }
@ -215,11 +227,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
/** /**
* Performs a starts with search on the global email view. * Performs a starts with search on the global email view.
*/ */
export const searchGlobalUsersByEmail = async ( export async function searchGlobalUsersByEmail(
email: string | unknown, email: string | unknown,
opts: any, opts: any,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof email !== "string") { if (typeof email !== "string") {
throw new Error("Must provide a string to search by") throw new Error("Must provide a string to search by")
} }
@ -242,12 +254,12 @@ export const searchGlobalUsersByEmail = async (
} }
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
export const paginatedUsers = async ({ export async function paginatedUsers({
bookmark, bookmark,
query, query,
appId, appId,
limit, limit,
}: SearchUsersRequest = {}) => { }: SearchUsersRequest = {}) {
const db = getGlobalDB() const db = getGlobalDB()
const pageSize = limit ?? PAGE_LIMIT const pageSize = limit ?? PAGE_LIMIT
const pageLimit = pageSize + 1 const pageLimit = pageSize + 1

View File

@ -228,7 +228,12 @@ export const getContextProviderComponents = (
/** /**
* Gets all data provider components above a component. * Gets all data provider components above a component.
*/ */
export const getActionProviderComponents = (asset, componentId, actionType) => { export const getActionProviders = (
asset,
componentId,
actionType,
options = { includeSelf: false }
) => {
if (!asset || !componentId) { if (!asset || !componentId) {
return [] return []
} }
@ -236,13 +241,30 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
// Get the component tree leading up to this component, ignoring the component // Get the component tree leading up to this component, ignoring the component
// itself // itself
const path = findComponentPath(asset.props, componentId) const path = findComponentPath(asset.props, componentId)
if (!options?.includeSelf) {
path.pop() path.pop()
}
// Filter by only data provider components // Find matching contexts and generate bindings
return path.filter(component => { let providers = []
path.forEach(component => {
const def = store.actions.components.getDefinition(component._component) const def = store.actions.components.getDefinition(component._component)
return def?.actions?.includes(actionType) const actions = (def?.actions || []).map(action => {
return typeof action === "string" ? { type: action } : action
}) })
const action = actions.find(x => x.type === actionType)
if (action) {
let runtimeBinding = component._id
if (action.suffix) {
runtimeBinding += `-${action.suffix}`
}
providers.push({
readableBinding: component._instanceName,
runtimeBinding,
})
}
})
return providers
} }
/** /**

View File

@ -1,17 +1,19 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
import { onMount } from "svelte" import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ChangeFormStep" "ChangeFormStep",
{ includeSelf: nested }
) )
const typeOptions = [ const typeOptions = [
@ -46,8 +48,8 @@
placeholder={null} placeholder={null}
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Step</Label> <Label small>Step</Label>
<Select bind:value={parameters.type} options={typeOptions} /> <Select bind:value={parameters.type} options={typeOptions} />

View File

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ClearForm" "ClearForm",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
</div> </div>

View File

@ -2,27 +2,20 @@
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables, viewsV2 } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
getContextProviderComponents,
getSchemaForDatasourcePlus,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
$: formComponents = getContextProviderComponents( $: providerOptions = getDatasourceLikeProviders({
$currentAsset, asset: $currentAsset,
$store.selectedComponentId, componentId: $store.selectedComponentId,
"form" nested,
) })
$: schemaComponents = getContextProviderComponents( $: schemaFields = getSchemaFields(parameters?.tableId)
$currentAsset,
$store.selectedComponentId,
"schema"
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
$: tableOptions = $tables.list.map(table => ({ $: tableOptions = $tables.list.map(table => ({
label: table.name, label: table.name,
resourceId: table._id, resourceId: table._id,
@ -33,44 +26,8 @@
})) }))
$: options = [...(tableOptions || []), ...(viewOptions || [])] $: options = [...(tableOptions || []), ...(viewOptions || [])]
// Gets a context definition of a certain type from a component definition const getSchemaFields = resourceId => {
const extractComponentContext = (component, contextType) => { const { schema } = getSchemaForDatasourcePlus(resourceId)
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
const contexts = Array.isArray(def.context) ? def.context : [def.context]
return contexts.find(context => context?.type === contextType)
}
// Gets options for valid context keys which provide valid data to submit
const getProviderOptions = (formComponents, schemaComponents) => {
const formContexts = formComponents.map(component => ({
component,
context: extractComponentContext(component, "form"),
}))
const schemaContexts = schemaComponents.map(component => ({
component,
context: extractComponentContext(component, "schema"),
}))
const allContexts = formContexts.concat(schemaContexts)
return allContexts.map(({ component, context }) => {
let runtimeBinding = component._id
if (context.suffix) {
runtimeBinding += `-${context.suffix}`
}
return {
label: component._instanceName,
value: runtimeBinding,
}
})
}
const getSchemaFields = (asset, tableId) => {
const { schema } = getSchemaForDatasourcePlus(tableId)
delete schema._id
delete schema._rev
return Object.values(schema || {}) return Object.values(schema || {})
} }

View File

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"RefreshDatasource" "RefreshDatasource",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
</div> </div>

View File

@ -2,29 +2,19 @@
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables, viewsV2 } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import { getSchemaForDatasourcePlus } from "builderStore/dataBinding"
getContextProviderComponents,
getSchemaForDatasourcePlus,
} from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { getDatasourceLikeProviders } from "components/design/settings/controls/ButtonActionEditor/actions/utils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested export let nested
$: formComponents = getContextProviderComponents( $: providerOptions = getDatasourceLikeProviders({
$currentAsset, asset: $currentAsset,
$store.selectedComponentId, componentId: $store.selectedComponentId,
"form", nested,
{ includeSelf: nested } })
)
$: schemaComponents = getContextProviderComponents(
$currentAsset,
$store.selectedComponentId,
"schema",
{ includeSelf: nested }
)
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields(parameters?.tableId) $: schemaFields = getSchemaFields(parameters?.tableId)
$: tableOptions = $tables.list.map(table => ({ $: tableOptions = $tables.list.map(table => ({
label: table.name, label: table.name,
@ -36,40 +26,6 @@
})) }))
$: options = [...(tableOptions || []), ...(viewOptions || [])] $: options = [...(tableOptions || []), ...(viewOptions || [])]
// Gets a context definition of a certain type from a component definition
const extractComponentContext = (component, contextType) => {
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
const contexts = Array.isArray(def.context) ? def.context : [def.context]
return contexts.find(context => context?.type === contextType)
}
// Gets options for valid context keys which provide valid data to submit
const getProviderOptions = (formComponents, schemaComponents) => {
const formContexts = formComponents.map(component => ({
component,
context: extractComponentContext(component, "form"),
}))
const schemaContexts = schemaComponents.map(component => ({
component,
context: extractComponentContext(component, "schema"),
}))
const allContexts = formContexts.concat(schemaContexts)
return allContexts.map(({ component, context }) => {
let runtimeBinding = component._id
if (context.suffix) {
runtimeBinding += `-${context.suffix}`
}
return {
label: component._instanceName,
value: runtimeBinding,
}
})
}
const getSchemaFields = resourceId => { const getSchemaFields = resourceId => {
const { schema } = getSchemaForDatasourcePlus(resourceId) const { schema } = getSchemaForDatasourcePlus(resourceId)
return Object.values(schema || {}) return Object.values(schema || {})

View File

@ -1,22 +1,36 @@
<script> <script>
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { Label, Combobox, Select } from "@budibase/bbui" import { Label, Combobox, Select } from "@budibase/bbui"
import { import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
getActionProviderComponents,
buildFormSchema,
} from "builderStore/dataBinding"
import { findComponent } from "builderStore/componentUtils" import { findComponent } from "builderStore/componentUtils"
export let parameters export let parameters
export let nested
$: formComponent = findComponent($currentAsset.props, parameters.componentId) $: formComponent = getFormComponent(
$currentAsset.props,
parameters.componentId
)
$: formSchema = buildFormSchema(formComponent) $: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {}) $: fieldOptions = Object.keys(formSchema || {})
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ScrollTo" "ScrollTo",
{ includeSelf: nested }
) )
const getFormComponent = (asset, id) => {
let component = findComponent(asset, id)
if (component) {
return component
}
// Check for block component IDs, and use the block itself instead
if (id?.includes("-")) {
return findComponent(asset, id.split("-")[0])
}
return null
}
</script> </script>
<div class="root"> <div class="root">
@ -24,8 +38,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Field</Label> <Label small>Field</Label>
<Combobox bind:value={parameters.field} options={fieldOptions} /> <Combobox bind:value={parameters.field} options={fieldOptions} />

View File

@ -3,14 +3,12 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { import { getActionProviders, buildFormSchema } from "builderStore/dataBinding"
getActionProviderComponents,
buildFormSchema,
} from "builderStore/dataBinding"
import { findComponent } from "builderStore/componentUtils" import { findComponent } from "builderStore/componentUtils"
export let parameters export let parameters
export let bindings = [] export let bindings = []
export let nested
const typeOptions = [ const typeOptions = [
{ {
@ -23,15 +21,31 @@
}, },
] ]
$: formComponent = findComponent($currentAsset.props, parameters.componentId) $: formComponent = getFormComponent(
$currentAsset.props,
parameters.componentId
)
$: formSchema = buildFormSchema(formComponent) $: formSchema = buildFormSchema(formComponent)
$: fieldOptions = Object.keys(formSchema || {}) $: fieldOptions = Object.keys(formSchema || {})
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ValidateForm" "ValidateForm",
{ includeSelf: nested }
) )
const getFormComponent = (asset, id) => {
let component = findComponent(asset, id)
if (component) {
return component
}
// Check for block component IDs, and use the block itself instead
if (id?.includes("-")) {
return findComponent(asset, id.split("-")[0])
}
return null
}
onMount(() => { onMount(() => {
if (!parameters.type) { if (!parameters.type) {
parameters.type = "set" parameters.type = "set"
@ -44,8 +58,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<Label small>Type</Label> <Label small>Type</Label>
<Select <Select

View File

@ -1,14 +1,16 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { currentAsset, store } from "builderStore" import { currentAsset, store } from "builderStore"
import { getActionProviderComponents } from "builderStore/dataBinding" import { getActionProviders } from "builderStore/dataBinding"
export let parameters export let parameters
export let nested
$: actionProviders = getActionProviderComponents( $: actionProviders = getActionProviders(
$currentAsset, $currentAsset,
$store.selectedComponentId, $store.selectedComponentId,
"ValidateForm" "ValidateForm",
{ includeSelf: nested }
) )
</script> </script>
@ -17,8 +19,8 @@
<Select <Select
bind:value={parameters.componentId} bind:value={parameters.componentId}
options={actionProviders} options={actionProviders}
getOptionLabel={x => x._instanceName} getOptionLabel={x => x.readableBinding}
getOptionValue={x => x._id} getOptionValue={x => x.runtimeBinding}
/> />
<div /> <div />
</div> </div>

View File

@ -0,0 +1,82 @@
import { getContextProviderComponents } from "builderStore/dataBinding"
import { store } from "builderStore"
import { capitalise } from "helpers"
// Generates bindings for all components that provider "datasource like"
// contexts. This includes "form" contexts and "schema" contexts. This is used
// by various button actions as candidates for whole "row" objects.
// Some examples are saving rows or duplicating rows.
export const getDatasourceLikeProviders = ({ asset, componentId, nested }) => {
// Get all form context providers
const formComponents = getContextProviderComponents(
asset,
componentId,
"form",
{ includeSelf: nested }
)
// Get all schema context providers
const schemaComponents = getContextProviderComponents(
asset,
componentId,
"schema",
{ includeSelf: nested }
)
// Generate contexts for all form providers
const formContexts = formComponents.map(component => ({
component,
context: extractComponentContext(component, "form"),
}))
// Generate contexts for all schema providers
const schemaContexts = schemaComponents.map(component => ({
component,
context: extractComponentContext(component, "schema"),
}))
// Check for duplicate contexts by the same component. In this case, attempt
// to label contexts with their suffixes
schemaContexts.forEach(schemaContext => {
// Check if we have a form context for this component
const id = schemaContext.component._id
const existing = formContexts.find(x => x.component._id === id)
if (existing) {
if (existing.context.suffix) {
const suffix = capitalise(existing.context.suffix)
existing.readableSuffix = ` - ${suffix}`
}
if (schemaContext.context.suffix) {
const suffix = capitalise(schemaContext.context.suffix)
schemaContext.readableSuffix = ` - ${suffix}`
}
}
})
// Generate bindings for all contexts
const allContexts = formContexts.concat(schemaContexts)
return allContexts.map(({ component, context, readableSuffix }) => {
let readableBinding = component._instanceName
let runtimeBinding = component._id
if (context.suffix) {
runtimeBinding += `-${context.suffix}`
}
if (readableSuffix) {
readableBinding += readableSuffix
}
return {
label: readableBinding,
value: runtimeBinding,
}
})
}
// Gets a context definition of a certain type from a component definition
const extractComponentContext = (component, contextType) => {
const def = store.actions.components.getDefinition(component?._component)
if (!def) {
return null
}
const contexts = Array.isArray(def.context) ? def.context : [def.context]
return contexts.find(context => context?.type === contextType)
}

View File

@ -6165,6 +6165,24 @@
"defaultValue": "spectrum--medium" "defaultValue": "spectrum--medium"
} }
], ],
"actions": [
{
"type": "ValidateForm",
"suffix": "form"
},
{
"type": "ClearForm",
"suffix": "form"
},
{
"type": "UpdateFieldValue",
"suffix": "form"
},
{
"type": "ScrollTo",
"suffix": "form"
}
],
"context": [ "context": [
{ {
"type": "form", "type": "form",
@ -6420,7 +6438,8 @@
], ],
"context": { "context": {
"type": "schema" "type": "schema"
} },
"actions": ["RefreshDatasource"]
}, },
"bbreferencefield": { "bbreferencefield": {
"devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels", "devComment": "As bb reference is only used for user subtype for now, we are using user for icon and labels",

View File

@ -3,9 +3,9 @@
import Block from "../Block.svelte" import Block from "../Block.svelte"
export let buttons = [] export let buttons = []
export let direction export let direction = "row"
export let hAlign export let hAlign = "left"
export let vAlign export let vAlign = "top"
export let gap = "S" export let gap = "S"
</script> </script>

View File

@ -27,8 +27,12 @@
builderStore, builderStore,
notificationStore, notificationStore,
enrichButtonActions, enrichButtonActions,
ActionTypes,
createContextStore,
} = getContext("sdk") } = getContext("sdk")
let grid
$: columnWhitelist = columns?.map(col => col.name) $: columnWhitelist = columns?.map(col => col.name)
$: schemaOverrides = getSchemaOverrides(columns) $: schemaOverrides = getSchemaOverrides(columns)
$: enrichedButtons = enrichButtons(buttons) $: enrichedButtons = enrichButtons(buttons)
@ -53,11 +57,16 @@
text: settings.text, text: settings.text,
type: settings.type, type: settings.type,
onClick: async row => { onClick: async row => {
// We add a fake context binding in here, which allows us to pretend // Create a fake, ephemeral context to run the buttons actions with
// that the grid provides a "schema" binding - that lets us use the const id = get(component).id
// clicked row in things like save row actions const gridContext = createContextStore(context)
const enrichedContext = { ...get(context), [get(component).id]: row } gridContext.actions.provideData(id, row)
const fn = enrichButtonActions(settings.onClick, enrichedContext) gridContext.actions.provideAction(
id,
ActionTypes.RefreshDatasource,
() => grid?.getContext()?.rows.actions.refreshData()
)
const fn = enrichButtonActions(settings.onClick, get(gridContext))
return await fn?.({ row }) return await fn?.({ row })
}, },
})) }))
@ -69,6 +78,7 @@
class:in-builder={$builderStore.inBuilder} class:in-builder={$builderStore.inBuilder}
> >
<Grid <Grid
bind:this={grid}
datasource={table} datasource={table}
{API} {API}
{stripeRows} {stripeRows}

View File

@ -45,13 +45,14 @@
// Register any "refresh datasource" actions with a singleton store // Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource // so we can easily refresh data at all levels for any datasource
if (type === ActionTypes.RefreshDatasource) { if (type === ActionTypes.RefreshDatasource) {
const { dataSource } = metadata || {} if (metadata?.dataSource) {
dataSourceStore.actions.registerDataSource( dataSourceStore.actions.registerDataSource(
dataSource, metadata.dataSource,
instanceId, instanceId,
callback callback
) )
} }
}
}) })
} }
} }

View File

@ -15,6 +15,7 @@ import {
confirmationStore, confirmationStore,
roleStore, roleStore,
stateStore, stateStore,
createContextStore,
} from "stores" } from "stores"
import { styleable } from "utils/styleable" import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable" import { linkable } from "utils/linkable"
@ -57,6 +58,7 @@ export default {
enrichButtonActions, enrichButtonActions,
processStringSync, processStringSync,
makePropSafe, makePropSafe,
createContextStore,
// Components // Components
Provider, Provider,

View File

@ -33,6 +33,7 @@ const environment = {
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED, REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
HTTP_MIGRATIONS: process.env.HTTP_MIGRATIONS, HTTP_MIGRATIONS: process.env.HTTP_MIGRATIONS,
CLUSTER_MODE: process.env.CLUSTER_MODE,
API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC, API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,

View File

@ -1,11 +1,13 @@
import env from "./environment" import env from "./environment"
import * as redis from "./utilities/redis" import * as redis from "./utilities/redis"
import { generateApiKey, getChecklist } from "./utilities/workerRequests"
import { import {
createAdminUser, events,
generateApiKey, installation,
getChecklist, logging,
} from "./utilities/workerRequests" tenancy,
import { events, installation, logging, tenancy } from "@budibase/backend-core" users,
} from "@budibase/backend-core"
import fs from "fs" import fs from "fs"
import { watch } from "./watch" import { watch } from "./watch"
import * as automations from "./automations" import * as automations from "./automations"
@ -58,7 +60,7 @@ export async function startup(app?: any, server?: any) {
return return
} }
STARTUP_RAN = true STARTUP_RAN = true
if (server) { if (server && !env.CLUSTER_MODE) {
console.log(`Budibase running on ${JSON.stringify(server.address())}`) console.log(`Budibase running on ${JSON.stringify(server.address())}`)
env._set("PORT", server.address().port) env._set("PORT", server.address().port)
} }
@ -110,34 +112,37 @@ export async function startup(app?: any, server?: any) {
// check and create admin user if required // check and create admin user if required
// this must be run after the api has been initialised due to // this must be run after the api has been initialised due to
// the app user sync // the app user sync
const bbAdminEmail = env.BB_ADMIN_USER_EMAIL,
bbAdminPassword = env.BB_ADMIN_USER_PASSWORD
if ( if (
env.SELF_HOSTED && env.SELF_HOSTED &&
!env.MULTI_TENANCY && !env.MULTI_TENANCY &&
env.BB_ADMIN_USER_EMAIL && bbAdminEmail &&
env.BB_ADMIN_USER_PASSWORD bbAdminPassword
) { ) {
const checklist = await getChecklist()
if (!checklist?.adminUser?.checked) {
try {
const tenantId = tenancy.getTenantId() const tenantId = tenancy.getTenantId()
const user = await createAdminUser( await tenancy.doInTenant(tenantId, async () => {
env.BB_ADMIN_USER_EMAIL, const exists = await users.doesUserExist(bbAdminEmail)
env.BB_ADMIN_USER_PASSWORD, const checklist = await getChecklist()
tenantId if (!checklist?.adminUser?.checked || !exists) {
try {
const user = await users.UserDB.createAdminUser(
bbAdminEmail,
bbAdminPassword,
tenantId,
{ hashPassword: true, requirePassword: true }
) )
// Need to set up an API key for automated integration tests // Need to set up an API key for automated integration tests
if (env.isTest()) { if (env.isTest()) {
await generateApiKey(user._id) await generateApiKey(user._id!)
} }
console.log( console.log("Admin account automatically created for", bbAdminEmail)
"Admin account automatically created for",
env.BB_ADMIN_USER_EMAIL
)
} catch (e) { } catch (e) {
logging.logAlert("Error creating initial admin user. Exiting.", e) logging.logAlert("Error creating initial admin user. Exiting.", e)
shutdown(server) shutdown(server)
} }
} }
})
} }
} }

View File

@ -155,19 +155,9 @@ export async function readGlobalUser(ctx: Ctx): Promise<User> {
return checkResponse(response, "get user", { ctx }) return checkResponse(response, "get user", { ctx })
} }
export async function createAdminUser( export async function getChecklist(): Promise<{
email: string, adminUser: { checked: boolean }
password: string, }> {
tenantId: string
) {
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/users/init"),
request(undefined, { method: "POST", body: { email, password, tenantId } })
)
return checkResponse(response, "create admin user")
}
export async function getChecklist() {
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"), checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"),
request(undefined, { method: "GET" }) request(undefined, { method: "GET" })

View File

@ -120,28 +120,17 @@ export const adminUser = async (
) )
} }
const user: User = {
email: email,
password: password,
createdAt: Date.now(),
roles: {},
builder: {
global: true,
},
admin: {
global: true,
},
tenantId,
ssoId,
}
try { try {
// always bust checklist beforehand, if an error occurs but can proceed, don't get const finalUser = await userSdk.db.createAdminUser(
// stuck in a cycle email,
await cache.bustCache(cache.CacheKey.CHECKLIST) password,
const finalUser = await userSdk.db.save(user, { tenantId,
{
ssoId,
hashPassword, hashPassword,
requirePassword, requirePassword,
}) }
)
// events // events
let account: CloudAccount | undefined let account: CloudAccount | undefined