Merge branch 'develop' of github.com:Budibase/budibase into budi-day-02-11-cheeks-joe
This commit is contained in:
commit
09e3ab9078
|
@ -6,9 +6,11 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -64,9 +64,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.58.5",
|
||||
"@budibase/client": "^0.7.6",
|
||||
"@budibase/client": "^0.7.8",
|
||||
"@budibase/colorpicker": "1.0.1",
|
||||
"@budibase/string-templates": "^0.7.6",
|
||||
"@budibase/string-templates": "^0.7.8",
|
||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@svelteschool/svelte-forms": "0.7.0",
|
||||
|
|
|
@ -11,21 +11,24 @@ const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
|||
/**
|
||||
* Gets all bindable data context fields and instance fields.
|
||||
*/
|
||||
export const getBindableProperties = (rootComponent, componentId) => {
|
||||
return getContextBindings(rootComponent, componentId)
|
||||
export const getBindableProperties = (asset, componentId) => {
|
||||
const contextBindings = getContextBindings(asset, componentId)
|
||||
const userBindings = getUserBindings()
|
||||
const urlBindings = getUrlBindings(asset, componentId)
|
||||
return [...contextBindings, ...userBindings, ...urlBindings]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
export const getDataProviderComponents = (rootComponent, componentId) => {
|
||||
if (!rootComponent || !componentId) {
|
||||
export const getDataProviderComponents = (asset, componentId) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(rootComponent, componentId)
|
||||
const path = findComponentPath(asset.props, componentId)
|
||||
path.pop()
|
||||
|
||||
// Filter by only data provider components
|
||||
|
@ -38,18 +41,14 @@ export const getDataProviderComponents = (rootComponent, componentId) => {
|
|||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
export const getActionProviderComponents = (
|
||||
rootComponent,
|
||||
componentId,
|
||||
actionType
|
||||
) => {
|
||||
if (!rootComponent || !componentId) {
|
||||
export const getActionProviderComponents = (asset, componentId, actionType) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the component tree leading up to this component, ignoring the component
|
||||
// itself
|
||||
const path = findComponentPath(rootComponent, componentId)
|
||||
const path = findComponentPath(asset.props, componentId)
|
||||
path.pop()
|
||||
|
||||
// Filter by only data provider components
|
||||
|
@ -92,13 +91,12 @@ export const getDatasourceForProvider = component => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable data contexts. These are fields of schemas of data contexts
|
||||
* provided by data provider components, such as lists or row detail components.
|
||||
* Gets all bindable data properties from component data contexts.
|
||||
*/
|
||||
export const getContextBindings = (rootComponent, componentId) => {
|
||||
const getContextBindings = (asset, componentId) => {
|
||||
// Extract any components which provide data contexts
|
||||
const dataProviders = getDataProviderComponents(rootComponent, componentId)
|
||||
let contextBindings = []
|
||||
const dataProviders = getDataProviderComponents(asset, componentId)
|
||||
let bindings = []
|
||||
|
||||
// Create bindings for each data provider
|
||||
dataProviders.forEach(component => {
|
||||
|
@ -109,7 +107,7 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
// Forms are an edge case which do not need table schemas
|
||||
if (isForm) {
|
||||
schema = buildFormSchema(component)
|
||||
tableName = "Schema"
|
||||
tableName = "Fields"
|
||||
} else {
|
||||
if (!datasource) {
|
||||
return
|
||||
|
@ -143,7 +141,7 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
runtimeBoundKey = `${key}_first`
|
||||
}
|
||||
|
||||
contextBindings.push({
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(
|
||||
runtimeBoundKey
|
||||
|
@ -157,7 +155,14 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
})
|
||||
})
|
||||
|
||||
// Add logged in user bindings
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable properties from the logged in user.
|
||||
*/
|
||||
const getUserBindings = () => {
|
||||
let bindings = []
|
||||
const tables = get(backendUiStore).tables
|
||||
const userTable = tables.find(table => table._id === TableNames.USERS)
|
||||
const schema = {
|
||||
|
@ -176,7 +181,7 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
runtimeBoundKey = `${key}_first`
|
||||
}
|
||||
|
||||
contextBindings.push({
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding: `user.${runtimeBoundKey}`,
|
||||
readableBinding: `Current User.${key}`,
|
||||
|
@ -187,7 +192,26 @@ export const getContextBindings = (rootComponent, componentId) => {
|
|||
})
|
||||
})
|
||||
|
||||
return contextBindings
|
||||
return bindings
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable properties from URL parameters.
|
||||
*/
|
||||
const getUrlBindings = asset => {
|
||||
const url = asset?.routing?.route ?? ""
|
||||
const split = url.split("/")
|
||||
let params = []
|
||||
split.forEach(part => {
|
||||
if (part.startsWith(":") && part.length > 1) {
|
||||
params.push(part.replace(/:/g, "").replace(/\?/g, ""))
|
||||
}
|
||||
})
|
||||
return params.map(param => ({
|
||||
type: "context",
|
||||
runtimeBinding: `url.${param}`,
|
||||
readableBinding: `URL.${param}`,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,6 +30,7 @@ export const getBackendUiStore = () => {
|
|||
const queries = await queriesResponse.json()
|
||||
const integrationsResponse = await api.get("/api/integrations")
|
||||
const integrations = await integrationsResponse.json()
|
||||
const permissionLevels = await store.actions.permissions.fetchLevels()
|
||||
|
||||
store.update(state => {
|
||||
state.selectedDatabase = db
|
||||
|
@ -37,6 +38,7 @@ export const getBackendUiStore = () => {
|
|||
state.datasources = datasources
|
||||
state.queries = queries
|
||||
state.integrations = integrations
|
||||
state.permissionLevels = permissionLevels
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -328,6 +330,25 @@ export const getBackendUiStore = () => {
|
|||
return response
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
fetchLevels: async () => {
|
||||
const response = await api.get("/api/permission/levels")
|
||||
const json = await response.json()
|
||||
return json
|
||||
},
|
||||
forResource: async resourceId => {
|
||||
const response = await api.get(`/api/permission/${resourceId}`)
|
||||
const json = await response.json()
|
||||
return json
|
||||
},
|
||||
save: async ({ role, resource, level }) => {
|
||||
const response = await api.post(
|
||||
`/api/permission/${role}/${resource}/${level}`
|
||||
)
|
||||
const json = await response.json()
|
||||
return json
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return store
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||
import ExportButton from "./buttons/ExportButton.svelte"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import * as api from "./api"
|
||||
import Table from "./Table.svelte"
|
||||
import { TableNames } from "constants"
|
||||
|
@ -47,6 +48,7 @@
|
|||
title={isUsersTable ? 'Create New User' : 'Create New Row'}
|
||||
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
|
||||
<CreateViewButton />
|
||||
<ManageAccessButton resourceId={$backendUiStore.selectedTable?._id} />
|
||||
<ExportButton view={tableView} />
|
||||
{/if}
|
||||
{#if isUsersTable}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import GroupByButton from "./buttons/GroupByButton.svelte"
|
||||
import FilterButton from "./buttons/FilterButton.svelte"
|
||||
import ExportButton from "./buttons/ExportButton.svelte"
|
||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
|
||||
export let view = {}
|
||||
|
||||
|
@ -53,5 +54,6 @@
|
|||
{#if view.calculation}
|
||||
<GroupByButton {view} />
|
||||
{/if}
|
||||
<ManageAccessButton resourceId={decodeURI(name)} />
|
||||
<ExportButton {view} />
|
||||
</Table>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import { TextButton, Icon, Popover } from "@budibase/bbui"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Roles } from "constants/backend"
|
||||
import api from "builderStore/api"
|
||||
import ManageAccessPopover from "../popovers/ManageAccessPopover.svelte"
|
||||
|
||||
export let resourceId
|
||||
|
||||
let anchor
|
||||
let dropdown
|
||||
let levels
|
||||
let permissions
|
||||
|
||||
async function openDropdown() {
|
||||
permissions = await backendUiStore.actions.permissions.forResource(
|
||||
resourceId
|
||||
)
|
||||
levels = await backendUiStore.actions.permissions.fetchLevels()
|
||||
dropdown.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
<TextButton text small on:click={openDropdown}>
|
||||
<i class="ri-lock-line" />
|
||||
Manage Access
|
||||
</TextButton>
|
||||
</div>
|
||||
<Popover bind:this={dropdown} {anchor} align="left">
|
||||
<ManageAccessPopover
|
||||
{resourceId}
|
||||
{levels}
|
||||
{permissions}
|
||||
onClosed={dropdown.hide} />
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
i {
|
||||
margin-right: var(--spacing-xs);
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
</style>
|
|
@ -142,7 +142,7 @@
|
|||
thin
|
||||
text="Use as table display column" />
|
||||
|
||||
<Label gray small>Search Indexes</Label>
|
||||
<Label grey small>Search Indexes</Label>
|
||||
<Toggle
|
||||
checked={indexes[0] === field.name}
|
||||
disabled={indexes[1] === field.name}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Roles } from "constants/backend"
|
||||
import api from "builderStore/api"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { Button, Label, Input, Select, Spacer } from "@budibase/bbui"
|
||||
|
||||
export let resourceId
|
||||
export let permissions
|
||||
export let onClosed
|
||||
|
||||
async function changePermission(level, role) {
|
||||
await backendUiStore.actions.permissions.save({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
|
||||
// Show updated permissions in UI: REMOVE
|
||||
permissions = await backendUiStore.actions.permissions.forResource(
|
||||
resourceId
|
||||
)
|
||||
notifier.success("Updated permissions.")
|
||||
// TODO: update permissions
|
||||
// permissions[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="popover">
|
||||
<h5>Who Can Access This Data?</h5>
|
||||
<div class="note">
|
||||
<Label extraSmall grey>
|
||||
Specify the minimum access level role for this data.
|
||||
</Label>
|
||||
</div>
|
||||
<Spacer large />
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(permissions) as level}
|
||||
<Input secondary thin value={level} disabled={true} />
|
||||
<Select
|
||||
secondary
|
||||
thin
|
||||
value={permissions[level]}
|
||||
on:change={e => changePermission(level, e.target.value)}>
|
||||
{#each $backendUiStore.roles as role}
|
||||
<option value={role._id}>{role.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Spacer large />
|
||||
|
||||
<div class="footer">
|
||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.popover {
|
||||
display: grid;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: var(--spacing-s) 0 var(--spacing-m) 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-m);
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.note {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import { Icon, Input, Drawer, Body, Button } from "@budibase/bbui"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value = ""
|
||||
export let bindings = []
|
||||
|
||||
let bindingDrawer
|
||||
let tempValue = value
|
||||
|
||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||
|
||||
const handleClose = () => {
|
||||
onChange(tempValue)
|
||||
bindingDrawer.hide()
|
||||
}
|
||||
|
||||
const onChange = value => {
|
||||
dispatch("change", readableToRuntimeBinding(bindings, value))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control">
|
||||
<Input
|
||||
thin
|
||||
value={readableValue}
|
||||
on:change={event => onChange(event.target.value)}
|
||||
placeholder="/screen" />
|
||||
<div class="icon" on:click={bindingDrawer.show}>
|
||||
<Icon name="lightning" />
|
||||
</div>
|
||||
</div>
|
||||
<Drawer bind:this={bindingDrawer} title="Bindings">
|
||||
<div slot="description">
|
||||
<Body extraSmall grey>
|
||||
Add the objects on the left to enrich your text.
|
||||
</Body>
|
||||
</div>
|
||||
<heading slot="buttons">
|
||||
<Button thin blue on:click={handleClose}>Save</Button>
|
||||
</heading>
|
||||
<div slot="body">
|
||||
<BindingPanel
|
||||
value={readableValue}
|
||||
close={handleClose}
|
||||
on:update={event => (tempValue = event.detail)}
|
||||
bindableProperties={bindings} />
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.control {
|
||||
flex: 1;
|
||||
margin-left: var(--spacing-l);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon {
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding-left: 7px;
|
||||
border-left: 1px solid var(--grey-4);
|
||||
background-color: var(--grey-2);
|
||||
border-top-right-radius: var(--border-radius-m);
|
||||
border-bottom-right-radius: var(--border-radius-m);
|
||||
color: var(--grey-7);
|
||||
font-size: 14px;
|
||||
}
|
||||
.icon:hover {
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
$: value && checkValid()
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: dispatch("update", value)
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
type: "query",
|
||||
}))
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: queryBindableProperties = bindableProperties.map(property => ({
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviderComponents = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
ds => ds._id === parameters.datasourceId
|
||||
)
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
).map(property => ({
|
||||
...property,
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
<script>
|
||||
import { DataList, Label } from "@budibase/bbui"
|
||||
import { allScreens } from "builderStore"
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import DrawerBindableInput from "components/common/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
|
||||
let bindingDrawer
|
||||
let tempValue = parameters.url
|
||||
|
||||
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label size="m" color="dark">Screen</Label>
|
||||
<DataList secondary bind:value={parameters.url}>
|
||||
<option value="" />
|
||||
{#each $allScreens as screen}
|
||||
<option value={screen.routing.route}>{screen.props._instanceName}</option>
|
||||
{/each}
|
||||
</DataList>
|
||||
<DrawerBindableInput
|
||||
value={parameters.url}
|
||||
on:change={value => (parameters.url = value.detail)}
|
||||
{bindings} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviders = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
</script>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
const emptyField = () => ({ name: "", value: "" })
|
||||
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
export let parameters
|
||||
|
||||
$: dataProviderComponents = getDataProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: providerComponent = dataProviderComponents.find(
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
export let parameters
|
||||
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"ValidateForm"
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
let valid
|
||||
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset.props,
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
<script>
|
||||
import { DataList } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { store, allScreens, currentAsset } from "builderStore"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
|
||||
export let value = ""
|
||||
|
||||
$: urls = getUrls($allScreens, $currentAsset, $store.selectedComponentId)
|
||||
|
||||
// Update value on blur
|
||||
const dispatch = createEventDispatcher()
|
||||
const handleBlur = () => dispatch("change", value)
|
||||
|
||||
// Get all valid screen URL, as well as detail screens which can be used in
|
||||
// the current data context
|
||||
const getUrls = (screens, asset, componentId) => {
|
||||
// Get all screens which aren't detail screens
|
||||
let urls = screens
|
||||
.filter(screen => !screen.props._component.endsWith("/rowdetail"))
|
||||
.map(screen => ({
|
||||
name: screen.props._instanceName,
|
||||
url: screen.routing.route,
|
||||
sort: screen.props._component,
|
||||
}))
|
||||
|
||||
// Add detail screens enriched with the current data context
|
||||
const bindableProperties = getBindableProperties(asset.props, componentId)
|
||||
screens
|
||||
.filter(screen => screen.props._component.endsWith("/rowdetail"))
|
||||
.forEach(detailScreen => {
|
||||
// Find any _id bindings that match the detail screen's table
|
||||
const binding = bindableProperties.find(p => {
|
||||
return (
|
||||
p.type === "context" &&
|
||||
p.runtimeBinding.endsWith("._id") &&
|
||||
p.tableId === detailScreen.props.table
|
||||
)
|
||||
})
|
||||
if (binding) {
|
||||
urls.push({
|
||||
name: detailScreen.props._instanceName,
|
||||
url: detailScreen.routing.route.replace(
|
||||
":id",
|
||||
`{{ ${binding.runtimeBinding} }}`
|
||||
),
|
||||
sort: detailScreen.props._component,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return urls
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<DataList
|
||||
editable
|
||||
secondary
|
||||
extraThin
|
||||
on:blur={handleBlur}
|
||||
on:change
|
||||
bind:value>
|
||||
<option value="" />
|
||||
{#each urls as url}
|
||||
<option value={url.url}>{url.name}</option>
|
||||
{/each}
|
||||
</DataList>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
div :global(> div) {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
|
@ -19,7 +19,6 @@
|
|||
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
|
||||
import ScreenSelect from "./PropertyControls/ScreenSelect.svelte"
|
||||
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
|
||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||
|
@ -63,7 +62,6 @@
|
|||
text: Input,
|
||||
select: OptionSelect,
|
||||
datasource: DatasourceSelect,
|
||||
screen: ScreenSelect,
|
||||
detailScreen: DetailScreenSelect,
|
||||
boolean: Checkbox,
|
||||
number: Input,
|
||||
|
|
|
@ -92,3 +92,11 @@ export const HostingTypes = {
|
|||
CLOUD: "cloud",
|
||||
SELF: "self",
|
||||
}
|
||||
|
||||
export const Roles = {
|
||||
ADMIN: "ADMIN",
|
||||
POWER: "POWER",
|
||||
BASIC: "BASIC",
|
||||
PUBLIC: "PUBLIC",
|
||||
BUILDER: "BUILDER",
|
||||
}
|
||||
|
|
|
@ -48,6 +48,11 @@
|
|||
modal.show()
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
template = null
|
||||
modal.hide()
|
||||
}
|
||||
|
||||
checkIfKeysAndApps()
|
||||
</script>
|
||||
|
||||
|
@ -73,7 +78,7 @@
|
|||
<AppList />
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modal} padding={false} width="600px">
|
||||
<Modal bind:this={modal} padding={false} width="600px" on:hide={closeModal}>
|
||||
<CreateAppModal {hasKey} {template} />
|
||||
</Modal>
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/budibase-client.js",
|
||||
"module": "dist/budibase-client.js",
|
||||
|
@ -9,14 +9,14 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/string-templates": "^0.7.6",
|
||||
"@budibase/string-templates": "^0.7.8",
|
||||
"deep-equal": "^2.0.1",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@budibase/standard-components": "^0.7.6",
|
||||
"@budibase/standard-components": "^0.7.8",
|
||||
"@rollup/plugin-commonjs": "^16.0.0",
|
||||
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { getContext } from "svelte"
|
||||
import Router from "svelte-spa-router"
|
||||
import { routeStore } from "../store"
|
||||
import Screen from "./Screen.svelte"
|
||||
|
@ -10,12 +10,10 @@
|
|||
// Only wrap this as an array to take advantage of svelte keying,
|
||||
// to ensure the svelte-spa-router is fully remounted when route config
|
||||
// changes
|
||||
$: configs = [
|
||||
{
|
||||
routes: getRouterConfig($routeStore.routes),
|
||||
id: $routeStore.routeSessionId,
|
||||
},
|
||||
]
|
||||
$: config = {
|
||||
routes: getRouterConfig($routeStore.routes),
|
||||
id: $routeStore.routeSessionId,
|
||||
}
|
||||
|
||||
const getRouterConfig = routes => {
|
||||
let config = {}
|
||||
|
@ -33,11 +31,11 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#each configs as config (config.id)}
|
||||
{#key config.id}
|
||||
<div use:styleable={$component.styles}>
|
||||
<Router on:routeLoading={onRouteLoading} routes={config.routes} />
|
||||
</div>
|
||||
{/each}
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
div {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { fade } from "svelte/transition"
|
||||
import { screenStore, routeStore } from "../store"
|
||||
import Component from "./Component.svelte"
|
||||
import Provider from "./Provider.svelte"
|
||||
|
||||
// Keep route params up to date
|
||||
export let params = {}
|
||||
|
@ -12,16 +13,16 @@
|
|||
|
||||
// Redirect to home layout if no matching route
|
||||
$: screenDefinition == null && routeStore.actions.navigate("/")
|
||||
|
||||
// Make a screen array so we can use keying to properly re-render each screen
|
||||
$: screens = screenDefinition ? [screenDefinition] : []
|
||||
</script>
|
||||
|
||||
{#each screens as screen (screen._id)}
|
||||
<div in:fade>
|
||||
<Component definition={screen} />
|
||||
</div>
|
||||
{/each}
|
||||
<!-- Ensure to fully remount when screen changes -->
|
||||
{#key screenDefinition?._id}
|
||||
<Provider key="url" data={params}>
|
||||
<div in:fade>
|
||||
<Component definition={screenDefinition} />
|
||||
</div>
|
||||
</Provider>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
div {
|
||||
|
|
|
@ -44,7 +44,7 @@ export const enrichProps = async (props, context) => {
|
|||
let enrichedProps = await enrichDataBindings(validProps, totalContext)
|
||||
|
||||
// Enrich button actions if they exist
|
||||
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||
if (props._component?.endsWith("/button") && enrichedProps.onClick) {
|
||||
enrichedProps.onClick = enrichButtonActions(
|
||||
enrichedProps.onClick,
|
||||
totalContext
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/electron.js",
|
||||
"repository": {
|
||||
|
@ -50,8 +50,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/client": "^0.7.6",
|
||||
"@budibase/string-templates": "^0.7.6",
|
||||
"@budibase/client": "^0.7.8",
|
||||
"@budibase/string-templates": "^0.7.8",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
"@koa/router": "8.0.0",
|
||||
"@sendgrid/mail": "7.1.1",
|
||||
|
|
|
@ -14,7 +14,6 @@ exports.fetchInfo = async ctx => {
|
|||
}
|
||||
|
||||
exports.save = async ctx => {
|
||||
console.trace("DID A SAVE!")
|
||||
const db = new CouchDB(BUILDER_CONFIG_DB)
|
||||
const { type } = ctx.request.body
|
||||
if (type === HostingTypes.CLOUD && ctx.request.body._rev) {
|
||||
|
|
|
@ -1,23 +1,45 @@
|
|||
const {
|
||||
BUILTIN_PERMISSIONS,
|
||||
getBuiltinPermissions,
|
||||
PermissionLevels,
|
||||
isPermissionLevelHigherThanRead,
|
||||
higherPermission,
|
||||
} = require("../../utilities/security/permissions")
|
||||
const {
|
||||
isBuiltin,
|
||||
getDBRoleID,
|
||||
getExternalRoleID,
|
||||
BUILTIN_ROLES,
|
||||
getBuiltinRoles,
|
||||
} = require("../../utilities/security/roles")
|
||||
const { getRoleParams } = require("../../db/utils")
|
||||
const CouchDB = require("../../db")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
const {
|
||||
CURRENTLY_SUPPORTED_LEVELS,
|
||||
getBasePermissions,
|
||||
} = require("../../utilities/security/utilities")
|
||||
|
||||
const PermissionUpdateType = {
|
||||
REMOVE: "remove",
|
||||
ADD: "add",
|
||||
}
|
||||
|
||||
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
||||
|
||||
// quick function to perform a bit of weird logic, make sure fetch calls
|
||||
// always say a write role also has read permission
|
||||
function fetchLevelPerms(permissions, level, roleId) {
|
||||
if (!permissions) {
|
||||
permissions = {}
|
||||
}
|
||||
permissions[level] = roleId
|
||||
if (
|
||||
isPermissionLevelHigherThanRead(level) &&
|
||||
!permissions[PermissionLevels.READ]
|
||||
) {
|
||||
permissions[PermissionLevels.READ] = roleId
|
||||
}
|
||||
return permissions
|
||||
}
|
||||
|
||||
// utility function to stop this repetition - permissions always stored under roles
|
||||
async function getAllDBRoles(db) {
|
||||
const body = await db.allDocs(
|
||||
|
@ -42,7 +64,7 @@ async function updatePermissionOnRole(
|
|||
|
||||
// the permission is for a built in, make sure it exists
|
||||
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
|
||||
const builtin = cloneDeep(BUILTIN_ROLES[roleId])
|
||||
const builtin = getBuiltinRoles()[roleId]
|
||||
builtin._id = getDBRoleID(builtin._id)
|
||||
dbRoles.push(builtin)
|
||||
}
|
||||
|
@ -65,7 +87,10 @@ async function updatePermissionOnRole(
|
|||
}
|
||||
// handle the adding, we're on the correct role, at it to this
|
||||
if (!remove && role._id === dbRoleId) {
|
||||
rolePermissions[resourceId] = level
|
||||
rolePermissions[resourceId] = higherPermission(
|
||||
rolePermissions[resourceId],
|
||||
level
|
||||
)
|
||||
updated = true
|
||||
}
|
||||
// handle the update, add it to bulk docs to perform at end
|
||||
|
@ -84,12 +109,12 @@ async function updatePermissionOnRole(
|
|||
}
|
||||
|
||||
exports.fetchBuiltin = function(ctx) {
|
||||
ctx.body = Object.values(BUILTIN_PERMISSIONS)
|
||||
ctx.body = Object.values(getBuiltinPermissions())
|
||||
}
|
||||
|
||||
exports.fetchLevels = function(ctx) {
|
||||
// for now only provide the read/write perms externally
|
||||
ctx.body = [PermissionLevels.WRITE, PermissionLevels.READ]
|
||||
ctx.body = SUPPORTED_LEVELS
|
||||
}
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
|
@ -98,20 +123,25 @@ exports.fetch = async function(ctx) {
|
|||
let permissions = {}
|
||||
// create an object with structure role ID -> resource ID -> level
|
||||
for (let role of roles) {
|
||||
if (role.permissions) {
|
||||
const roleId = getExternalRoleID(role._id)
|
||||
if (permissions[roleId] == null) {
|
||||
permissions[roleId] = {}
|
||||
}
|
||||
for (let [resource, level] of Object.entries(role.permissions)) {
|
||||
permissions[roleId][resource] = higherPermission(
|
||||
permissions[roleId][resource],
|
||||
level
|
||||
)
|
||||
}
|
||||
if (!role.permissions) {
|
||||
continue
|
||||
}
|
||||
const roleId = getExternalRoleID(role._id)
|
||||
for (let [resource, level] of Object.entries(role.permissions)) {
|
||||
permissions[resource] = fetchLevelPerms(
|
||||
permissions[resource],
|
||||
level,
|
||||
roleId
|
||||
)
|
||||
}
|
||||
}
|
||||
ctx.body = permissions
|
||||
// apply the base permissions
|
||||
const finalPermissions = {}
|
||||
for (let [resource, permission] of Object.entries(permissions)) {
|
||||
const basePerms = getBasePermissions(resource)
|
||||
finalPermissions[resource] = Object.assign(basePerms, permission)
|
||||
}
|
||||
ctx.body = finalPermissions
|
||||
}
|
||||
|
||||
exports.getResourcePerms = async function(ctx) {
|
||||
|
@ -123,18 +153,20 @@ exports.getResourcePerms = async function(ctx) {
|
|||
})
|
||||
)
|
||||
const roles = body.rows.map(row => row.doc)
|
||||
const resourcePerms = {}
|
||||
for (let role of roles) {
|
||||
let permissions = {}
|
||||
for (let level of SUPPORTED_LEVELS) {
|
||||
// update the various roleIds in the resource permissions
|
||||
if (role.permissions && role.permissions[resourceId]) {
|
||||
const roleId = getExternalRoleID(role._id)
|
||||
resourcePerms[roleId] = higherPermission(
|
||||
resourcePerms[roleId],
|
||||
role.permissions[resourceId]
|
||||
)
|
||||
for (let role of roles) {
|
||||
if (role.permissions && role.permissions[resourceId] === level) {
|
||||
permissions = fetchLevelPerms(
|
||||
permissions,
|
||||
level,
|
||||
getExternalRoleID(role._id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.body = resourcePerms
|
||||
ctx.body = Object.assign(getBasePermissions(resourceId), permissions)
|
||||
}
|
||||
|
||||
exports.addPermission = async function(ctx) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const CouchDB = require("../../db")
|
||||
const {
|
||||
BUILTIN_ROLES,
|
||||
getBuiltinRoles,
|
||||
BUILTIN_ROLE_IDS,
|
||||
Role,
|
||||
getRole,
|
||||
|
@ -57,17 +57,20 @@ exports.fetch = async function(ctx) {
|
|||
include_docs: true,
|
||||
})
|
||||
)
|
||||
const roles = body.rows.map(row => row.doc)
|
||||
let roles = body.rows.map(row => row.doc)
|
||||
const builtinRoles = getBuiltinRoles()
|
||||
|
||||
// need to combine builtin with any DB record of them (for sake of permissions)
|
||||
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
||||
const builtinRole = BUILTIN_ROLES[builtinRoleId]
|
||||
const builtinRole = builtinRoles[builtinRoleId]
|
||||
const dbBuiltin = roles.filter(
|
||||
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
|
||||
)[0]
|
||||
if (dbBuiltin == null) {
|
||||
roles.push(builtinRole)
|
||||
} else {
|
||||
// remove role and all back after combining with the builtin
|
||||
roles = roles.filter(role => role._id !== dbBuiltin._id)
|
||||
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
|
||||
roles.push(Object.assign(builtinRole, dbBuiltin))
|
||||
}
|
||||
|
|
|
@ -71,21 +71,22 @@ describe("/permission", () => {
|
|||
.set(defaultHeaders(appId))
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body[STD_ROLE_ID]).toEqual("read")
|
||||
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||
})
|
||||
|
||||
it("should get resource permissions with multiple roles", async () => {
|
||||
perms = await addPermission(request, appId, HIGHER_ROLE_ID, table._id, "write")
|
||||
const res = await getTablePermissions()
|
||||
expect(res.body[HIGHER_ROLE_ID]).toEqual("write")
|
||||
expect(res.body[STD_ROLE_ID]).toEqual("read")
|
||||
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||
const allRes = await request
|
||||
.get(`/api/permission`)
|
||||
.set(defaultHeaders(appId))
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(allRes.body[HIGHER_ROLE_ID][table._id]).toEqual("write")
|
||||
expect(allRes.body[STD_ROLE_ID][table._id]).toEqual("read")
|
||||
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
|
||||
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ const Router = require("@koa/router")
|
|||
const viewController = require("../controllers/view")
|
||||
const rowController = require("../controllers/row")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { paramResource } = require("../../middleware/resourceId")
|
||||
const {
|
||||
BUILDER,
|
||||
PermissionTypes,
|
||||
|
@ -15,12 +16,14 @@ router
|
|||
.get("/api/views/export", authorized(BUILDER), viewController.exportView)
|
||||
.get(
|
||||
"/api/views/:viewName",
|
||||
paramResource("viewName"),
|
||||
authorized(PermissionTypes.VIEW, PermissionLevels.READ),
|
||||
rowController.fetchView
|
||||
)
|
||||
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
||||
.delete(
|
||||
"/api/views/:viewName",
|
||||
paramResource("viewName"),
|
||||
authorized(BUILDER),
|
||||
usage,
|
||||
viewController.destroy
|
||||
|
|
|
@ -53,6 +53,10 @@ module.exports.getAction = async function(actionName) {
|
|||
if (BUILTIN_ACTIONS[actionName] != null) {
|
||||
return BUILTIN_ACTIONS[actionName]
|
||||
}
|
||||
// worker pools means that a worker may not have manifest
|
||||
if (env.CLOUD && MANIFEST == null) {
|
||||
MANIFEST = await module.exports.init()
|
||||
}
|
||||
// env setup to get async packages
|
||||
if (!MANIFEST || !MANIFEST.packages || !MANIFEST.packages[actionName]) {
|
||||
return null
|
||||
|
@ -86,8 +90,10 @@ module.exports.init = async function() {
|
|||
? Object.assign(MANIFEST.packages, BUILTIN_DEFINITIONS)
|
||||
: BUILTIN_DEFINITIONS
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Sentry.captureException(err)
|
||||
}
|
||||
return MANIFEST
|
||||
}
|
||||
|
||||
module.exports.DEFINITIONS = BUILTIN_DEFINITIONS
|
||||
|
|
|
@ -34,7 +34,7 @@ module.exports.init = function() {
|
|||
actions.init().then(() => {
|
||||
triggers.automationQueue.process(async job => {
|
||||
try {
|
||||
if (env.CLOUD && job.data.automation) {
|
||||
if (env.CLOUD && job.data.automation && !env.SELF_HOSTED) {
|
||||
job.data.automation.apiKey = await updateQuota(job.data.automation)
|
||||
}
|
||||
if (env.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const jwt = require("jsonwebtoken")
|
||||
const STATUS_CODES = require("../utilities/statusCodes")
|
||||
const { getRole, BUILTIN_ROLES } = require("../utilities/security/roles")
|
||||
const { getRole, getBuiltinRoles } = require("../utilities/security/roles")
|
||||
const { AuthTypes } = require("../constants")
|
||||
const {
|
||||
getAppId,
|
||||
|
@ -20,6 +20,7 @@ module.exports = async (ctx, next) => {
|
|||
// we hold it in state as a
|
||||
let appId = getAppId(ctx)
|
||||
const cookieAppId = ctx.cookies.get(getCookieName("currentapp"))
|
||||
const builtinRoles = getBuiltinRoles()
|
||||
if (appId && cookieAppId !== appId) {
|
||||
setCookie(ctx, appId, "currentapp")
|
||||
} else if (cookieAppId) {
|
||||
|
@ -40,7 +41,7 @@ module.exports = async (ctx, next) => {
|
|||
ctx.appId = appId
|
||||
ctx.user = {
|
||||
appId,
|
||||
role: BUILTIN_ROLES.PUBLIC,
|
||||
role: builtinRoles.PUBLIC,
|
||||
}
|
||||
await next()
|
||||
return
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const { flatten } = require("lodash")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
|
||||
const PermissionLevels = {
|
||||
READ: "read",
|
||||
|
@ -23,6 +24,22 @@ function Permission(type, level) {
|
|||
this.type = type
|
||||
}
|
||||
|
||||
function levelToNumber(perm) {
|
||||
switch (perm) {
|
||||
// not everything has execute privileges
|
||||
case PermissionLevels.EXECUTE:
|
||||
return 0
|
||||
case PermissionLevels.READ:
|
||||
return 1
|
||||
case PermissionLevels.WRITE:
|
||||
return 2
|
||||
case PermissionLevels.ADMIN:
|
||||
return 3
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the specified permission level for the user return the levels they are allowed to carry out.
|
||||
* @param {string} userPermLevel The permission level of the user.
|
||||
|
@ -47,13 +64,21 @@ function getAllowedLevels(userPermLevel) {
|
|||
}
|
||||
|
||||
exports.BUILTIN_PERMISSION_IDS = {
|
||||
PUBLIC: "public",
|
||||
READ_ONLY: "read_only",
|
||||
WRITE: "write",
|
||||
ADMIN: "admin",
|
||||
POWER: "power",
|
||||
}
|
||||
|
||||
exports.BUILTIN_PERMISSIONS = {
|
||||
const BUILTIN_PERMISSIONS = {
|
||||
PUBLIC: {
|
||||
_id: exports.BUILTIN_PERMISSION_IDS.PUBLIC,
|
||||
name: "Public",
|
||||
permissions: [
|
||||
new Permission(PermissionTypes.WEBHOOK, PermissionLevels.EXECUTE),
|
||||
],
|
||||
},
|
||||
READ_ONLY: {
|
||||
_id: exports.BUILTIN_PERMISSION_IDS.READ_ONLY,
|
||||
name: "Read only",
|
||||
|
@ -97,6 +122,15 @@ exports.BUILTIN_PERMISSIONS = {
|
|||
},
|
||||
}
|
||||
|
||||
exports.getBuiltinPermissions = () => {
|
||||
return cloneDeep(BUILTIN_PERMISSIONS)
|
||||
}
|
||||
|
||||
exports.getBuiltinPermissionByID = id => {
|
||||
const perms = Object.values(BUILTIN_PERMISSIONS)
|
||||
return perms.find(perm => perm._id === id)
|
||||
}
|
||||
|
||||
exports.doesHaveResourcePermission = (
|
||||
permissions,
|
||||
permLevel,
|
||||
|
@ -126,7 +160,7 @@ exports.doesHaveResourcePermission = (
|
|||
}
|
||||
|
||||
exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
|
||||
const builtins = Object.values(exports.BUILTIN_PERMISSIONS)
|
||||
const builtins = Object.values(BUILTIN_PERMISSIONS)
|
||||
let permissions = flatten(
|
||||
builtins
|
||||
.filter(builtin => permissionIds.indexOf(builtin._id) !== -1)
|
||||
|
@ -144,22 +178,11 @@ exports.doesHaveBasePermission = (permType, permLevel, permissionIds) => {
|
|||
}
|
||||
|
||||
exports.higherPermission = (perm1, perm2) => {
|
||||
function toNum(perm) {
|
||||
switch (perm) {
|
||||
// not everything has execute privileges
|
||||
case PermissionLevels.EXECUTE:
|
||||
return 0
|
||||
case PermissionLevels.READ:
|
||||
return 1
|
||||
case PermissionLevels.WRITE:
|
||||
return 2
|
||||
case PermissionLevels.ADMIN:
|
||||
return 3
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return toNum(perm1) > toNum(perm2) ? perm1 : perm2
|
||||
return levelToNumber(perm1) > levelToNumber(perm2) ? perm1 : perm2
|
||||
}
|
||||
|
||||
exports.isPermissionLevelHigherThanRead = level => {
|
||||
return levelToNumber(level) > 1
|
||||
}
|
||||
|
||||
// utility as a lot of things need simply the builder permission
|
||||
|
|
|
@ -26,7 +26,7 @@ Role.prototype.addInheritance = function(inherits) {
|
|||
return this
|
||||
}
|
||||
|
||||
exports.BUILTIN_ROLES = {
|
||||
const BUILTIN_ROLES = {
|
||||
ADMIN: new Role(BUILTIN_IDS.ADMIN, "Admin")
|
||||
.addPermission(BUILTIN_PERMISSION_IDS.ADMIN)
|
||||
.addInheritance(BUILTIN_IDS.POWER),
|
||||
|
@ -37,18 +37,22 @@ exports.BUILTIN_ROLES = {
|
|||
.addPermission(BUILTIN_PERMISSION_IDS.WRITE)
|
||||
.addInheritance(BUILTIN_IDS.PUBLIC),
|
||||
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public").addPermission(
|
||||
BUILTIN_PERMISSION_IDS.READ_ONLY
|
||||
BUILTIN_PERMISSION_IDS.PUBLIC
|
||||
),
|
||||
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder").addPermission(
|
||||
BUILTIN_PERMISSION_IDS.ADMIN
|
||||
),
|
||||
}
|
||||
|
||||
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
||||
exports.getBuiltinRoles = () => {
|
||||
return cloneDeep(BUILTIN_ROLES)
|
||||
}
|
||||
|
||||
exports.BUILTIN_ROLE_ID_ARRAY = Object.values(BUILTIN_ROLES).map(
|
||||
role => role._id
|
||||
)
|
||||
|
||||
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(exports.BUILTIN_ROLES).map(
|
||||
exports.BUILTIN_ROLE_NAME_ARRAY = Object.values(BUILTIN_ROLES).map(
|
||||
role => role.name
|
||||
)
|
||||
|
||||
|
@ -56,6 +60,42 @@ function isBuiltin(role) {
|
|||
return exports.BUILTIN_ROLE_ID_ARRAY.some(builtin => role.includes(builtin))
|
||||
}
|
||||
|
||||
/**
|
||||
* Works through the inheritance ranks to see how far up the builtin stack this ID is.
|
||||
*/
|
||||
function builtinRoleToNumber(id) {
|
||||
const builtins = exports.getBuiltinRoles()
|
||||
const MAX = Object.values(BUILTIN_IDS).length + 1
|
||||
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
|
||||
return MAX
|
||||
}
|
||||
let role = builtins[id],
|
||||
count = 0
|
||||
do {
|
||||
if (!role) {
|
||||
break
|
||||
}
|
||||
role = builtins[role.inherits]
|
||||
count++
|
||||
} while (role !== null)
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whichever builtin roleID is lower.
|
||||
*/
|
||||
exports.lowerBuiltinRoleID = (roleId1, roleId2) => {
|
||||
if (!roleId1) {
|
||||
return roleId2
|
||||
}
|
||||
if (!roleId2) {
|
||||
return roleId1
|
||||
}
|
||||
return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2)
|
||||
? roleId2
|
||||
: roleId1
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
||||
* to check if the role inherits any others.
|
||||
|
@ -72,7 +112,7 @@ exports.getRole = async (appId, roleId) => {
|
|||
// but can be extended by a doc stored about them (e.g. permissions)
|
||||
if (isBuiltin(roleId)) {
|
||||
role = cloneDeep(
|
||||
Object.values(exports.BUILTIN_ROLES).find(role => role._id === roleId)
|
||||
Object.values(BUILTIN_ROLES).find(role => role._id === roleId)
|
||||
)
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
const {
|
||||
PermissionLevels,
|
||||
PermissionTypes,
|
||||
getBuiltinPermissionByID,
|
||||
isPermissionLevelHigherThanRead,
|
||||
} = require("../../utilities/security/permissions")
|
||||
const {
|
||||
lowerBuiltinRoleID,
|
||||
getBuiltinRoles,
|
||||
} = require("../../utilities/security/roles")
|
||||
const { DocumentTypes } = require("../../db/utils")
|
||||
|
||||
const CURRENTLY_SUPPORTED_LEVELS = [
|
||||
PermissionLevels.WRITE,
|
||||
PermissionLevels.READ,
|
||||
]
|
||||
|
||||
exports.getPermissionType = resourceId => {
|
||||
const docType = Object.values(DocumentTypes).filter(docType =>
|
||||
resourceId.startsWith(docType)
|
||||
)[0]
|
||||
switch (docType) {
|
||||
case DocumentTypes.TABLE:
|
||||
case DocumentTypes.ROW:
|
||||
return PermissionTypes.TABLE
|
||||
case DocumentTypes.AUTOMATION:
|
||||
return PermissionTypes.AUTOMATION
|
||||
case DocumentTypes.WEBHOOK:
|
||||
return PermissionTypes.WEBHOOK
|
||||
case DocumentTypes.QUERY:
|
||||
case DocumentTypes.DATASOURCE:
|
||||
return PermissionTypes.QUERY
|
||||
default:
|
||||
// views don't have an ID, will end up here
|
||||
return PermissionTypes.VIEW
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* works out the basic permissions based on builtin roles for a resource, using its ID
|
||||
* @param resourceId
|
||||
* @returns {{}}
|
||||
*/
|
||||
exports.getBasePermissions = resourceId => {
|
||||
const type = exports.getPermissionType(resourceId)
|
||||
const permissions = {}
|
||||
for (let [roleId, role] of Object.entries(getBuiltinRoles())) {
|
||||
if (!role.permissionId) {
|
||||
continue
|
||||
}
|
||||
const perms = getBuiltinPermissionByID(role.permissionId)
|
||||
const typedPermission = perms.permissions.find(perm => perm.type === type)
|
||||
if (
|
||||
typedPermission &&
|
||||
CURRENTLY_SUPPORTED_LEVELS.indexOf(typedPermission.level) !== -1
|
||||
) {
|
||||
const level = typedPermission.level
|
||||
permissions[level] = lowerBuiltinRoleID(permissions[level], roleId)
|
||||
if (isPermissionLevelHigherThanRead(level)) {
|
||||
permissions[PermissionLevels.READ] = lowerBuiltinRoleID(
|
||||
permissions[PermissionLevels.READ],
|
||||
roleId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return permissions
|
||||
}
|
||||
|
||||
exports.CURRENTLY_SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
|
@ -50,6 +50,9 @@ exports.Properties = {
|
|||
}
|
||||
|
||||
exports.getAPIKey = async appId => {
|
||||
if (env.SELF_HOSTED) {
|
||||
return { apiKey: null }
|
||||
}
|
||||
return apiKeyTable.get({ primary: appId })
|
||||
}
|
||||
|
||||
|
@ -63,7 +66,7 @@ exports.getAPIKey = async appId => {
|
|||
*/
|
||||
exports.update = async (apiKey, property, usage) => {
|
||||
// don't try validate in builder
|
||||
if (!env.CLOUD) {
|
||||
if (!env.CLOUD || env.SELF_HOSTED) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -181,9 +181,10 @@
|
|||
"key": "subheading"
|
||||
},
|
||||
{
|
||||
"type": "screen",
|
||||
"type": "text",
|
||||
"label": "Link URL",
|
||||
"key": "destinationUrl"
|
||||
"key": "destinationUrl",
|
||||
"placeholder": "/screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -214,9 +215,10 @@
|
|||
"key": "linkText"
|
||||
},
|
||||
{
|
||||
"type": "screen",
|
||||
"label": "Link Url",
|
||||
"key": "linkUrl"
|
||||
"type": "text",
|
||||
"label": "Link URL",
|
||||
"key": "linkUrl",
|
||||
"placeholder": "/screen"
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
|
@ -383,9 +385,10 @@
|
|||
"key": "text"
|
||||
},
|
||||
{
|
||||
"type": "screen",
|
||||
"type": "text",
|
||||
"label": "URL",
|
||||
"key": "url"
|
||||
"key": "url",
|
||||
"placeholder": "/screen"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
|
@ -456,9 +459,10 @@
|
|||
"key": "linkText"
|
||||
},
|
||||
{
|
||||
"type": "screen",
|
||||
"type": "text",
|
||||
"label": "Link URL",
|
||||
"key": "linkUrl"
|
||||
"key": "linkUrl",
|
||||
"placeholder": "/screen"
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"keywords": [
|
||||
"svelte"
|
||||
],
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"license": "MIT",
|
||||
"gitHead": "1a80b09fd093f2599a68f7db72ad639dd50922dd",
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const { styleable, linkable } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export const className = ""
|
||||
|
@ -38,8 +38,11 @@
|
|||
<h2 class="heading">{heading}</h2>
|
||||
<h4 class="text">{description}</h4>
|
||||
<a
|
||||
use:linkable
|
||||
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
|
||||
href={linkUrl}>{linkText}</a>
|
||||
href={linkUrl}>
|
||||
{linkText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
<script>
|
||||
import { Label, Multiselect } from "@budibase/bbui"
|
||||
import { capitalise } from "./helpers"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
|
||||
export let schema = {}
|
||||
export let linkedRows = []
|
||||
export let showLabel = true
|
||||
export let secondary
|
||||
|
||||
let linkedTable
|
||||
let allRows = []
|
||||
|
||||
$: label = capitalise(schema.name)
|
||||
$: linkedTableId = schema.tableId
|
||||
$: fetchRows(linkedTableId)
|
||||
$: fetchTable(linkedTableId)
|
||||
|
||||
async function fetchTable(id) {
|
||||
if (id != null) {
|
||||
linkedTable = await API.fetchTableDefinition(id)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRows(id) {
|
||||
if (id != null) {
|
||||
allRows = await API.fetchTableData(id)
|
||||
}
|
||||
}
|
||||
|
||||
function getPrettyName(row) {
|
||||
return row[(linkedTable && linkedTable.primaryDisplay) || "_id"]
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if linkedTable != null}
|
||||
{#if linkedTable.primaryDisplay == null}
|
||||
{#if showLabel}
|
||||
<Label extraSmall grey>{label}</Label>
|
||||
{/if}
|
||||
<Label small black>
|
||||
Please choose a display column for the
|
||||
<b>{linkedTable.name}</b>
|
||||
table.
|
||||
</Label>
|
||||
{:else}
|
||||
<Multiselect
|
||||
{secondary}
|
||||
bind:value={linkedRows}
|
||||
label={showLabel ? label : null}
|
||||
placeholder="Choose some options">
|
||||
{#each allRows as row}
|
||||
<option value={row._id}>{getPrettyName(row)}</option>
|
||||
{/each}
|
||||
</Multiselect>
|
||||
{/if}
|
||||
{/if}
|
|
@ -18,16 +18,16 @@
|
|||
let table
|
||||
let fieldMap = {}
|
||||
|
||||
// Checks if the closest data context matches the model for this forms
|
||||
// datasource, and use it as the initial form values if so
|
||||
// Returns the closes data context which isn't a built in context
|
||||
const getInitialValues = context => {
|
||||
return context && context.tableId === datasource?.tableId ? context : {}
|
||||
if (["user", "url"].includes(context.closestComponentId)) {
|
||||
return {}
|
||||
}
|
||||
return context[`${context.closestComponentId}`] || {}
|
||||
}
|
||||
|
||||
// Use the closest data context as the initial form values if it matches
|
||||
const initialValues = getInitialValues(
|
||||
$context[`${$context.closestComponentId}`]
|
||||
)
|
||||
// Use the closest data context as the initial form values
|
||||
const initialValues = getInitialValues($context)
|
||||
|
||||
// Form state contains observable data about the form
|
||||
const formState = writable({ values: initialValues, errors: {}, valid: true })
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
$: fetchTable(linkedTableId)
|
||||
|
||||
const fetchTable = async id => {
|
||||
if (id != null) {
|
||||
if (id) {
|
||||
const result = await API.fetchTableDefinition(id)
|
||||
if (!result.error) {
|
||||
tableDefinition = result
|
||||
|
@ -33,7 +33,7 @@
|
|||
}
|
||||
|
||||
const fetchRows = async id => {
|
||||
if (id != null) {
|
||||
if (id) {
|
||||
const rows = await API.fetchTableData(id)
|
||||
options = rows && !rows.error ? rows : []
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.js",
|
||||
"module": "src/index.js",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/deployment",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.7.6",
|
||||
"version": "0.7.8",
|
||||
"description": "Budibase Deployment Server",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
|
Loading…
Reference in New Issue