forked from tsai/budibase
51 changed files with 1479 additions and 1206 deletions
@ -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> |
||||
@ -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> |
||||
@ -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> |
|
||||
File diff suppressed because it is too large
@ -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 |
||||
@ -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} |
|
||||
Loading…
Reference in new issue