commit
45dbfd2e35
|
@ -213,6 +213,22 @@ export function getBuiltinRole(roleId: string): Role | undefined {
|
|||
return cloneDeep(role)
|
||||
}
|
||||
|
||||
export function validInherits(
|
||||
allRoles: RoleDoc[],
|
||||
inherits?: string | string[]
|
||||
): boolean {
|
||||
if (!inherits) {
|
||||
return false
|
||||
}
|
||||
const find = (id: string) => allRoles.find(r => roleIDsAreEqual(r._id!, id))
|
||||
if (Array.isArray(inherits)) {
|
||||
const filtered = inherits.filter(roleId => find(roleId))
|
||||
return inherits.length !== 0 && filtered.length === inherits.length
|
||||
} else {
|
||||
return !!find(inherits)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Works through the inheritance ranks to see how far up the builtin stack this ID is.
|
||||
*/
|
||||
|
@ -290,7 +306,7 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
|
|||
: roleId1
|
||||
}
|
||||
|
||||
export function compareRoleIds(roleId1: string, roleId2: string) {
|
||||
export function roleIDsAreEqual(roleId1: string, roleId2: string) {
|
||||
// make sure both role IDs are prefixed correctly
|
||||
return prefixRoleID(roleId1) === prefixRoleID(roleId2)
|
||||
}
|
||||
|
@ -323,7 +339,7 @@ export function findRole(
|
|||
roleId = prefixRoleID(roleId)
|
||||
}
|
||||
const dbRole = roles.find(
|
||||
role => role._id && compareRoleIds(role._id, roleId)
|
||||
role => role._id && roleIDsAreEqual(role._id, roleId)
|
||||
)
|
||||
if (!dbRole && !isBuiltin(roleId) && opts?.defaultPublic) {
|
||||
return cloneDeep(BUILTIN_ROLES.PUBLIC)
|
||||
|
@ -557,7 +573,7 @@ export class AccessController {
|
|||
}
|
||||
|
||||
return (
|
||||
roleIds?.find(roleId => compareRoleIds(roleId, tryingRoleId)) !==
|
||||
roleIds?.find(roleId => roleIDsAreEqual(roleId, tryingRoleId)) !==
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let onConfirm = undefined
|
||||
export let buttonText = ""
|
||||
export let cta = false
|
||||
|
||||
$: icon = selectIcon(type)
|
||||
// if newlines used, convert them to different elements
|
||||
$: split = message.split("\n")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import StatusLight from "../StatusLight/StatusLight.svelte"
|
||||
|
||||
export let icon = null
|
||||
export let iconColor = null
|
||||
|
@ -8,6 +9,7 @@
|
|||
export let url = null
|
||||
export let hoverable = false
|
||||
export let showArrow = false
|
||||
export let selected = false
|
||||
</script>
|
||||
|
||||
<a
|
||||
|
@ -15,9 +17,12 @@
|
|||
class="list-item"
|
||||
class:hoverable={hoverable || url != null}
|
||||
on:click
|
||||
class:selected
|
||||
>
|
||||
<div class="left">
|
||||
{#if icon}
|
||||
{#if icon === "StatusLight"}
|
||||
<StatusLight square size="L" color={iconColor} />
|
||||
{:else if icon}
|
||||
<Icon name={icon} color={iconColor} />
|
||||
{/if}
|
||||
<div class="list-item__text">
|
||||
|
@ -43,7 +48,7 @@
|
|||
|
||||
<style>
|
||||
.list-item {
|
||||
padding: var(--spacing-m);
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -66,15 +71,20 @@
|
|||
}
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.hoverable:not(.selected):hover {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.selected {
|
||||
background: var(--spectrum-global-color-blue-100);
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.left {
|
||||
width: 0;
|
||||
|
|
|
@ -21,6 +21,7 @@ export { default as EnvDropdown } from "./Form/EnvDropdown.svelte"
|
|||
export { default as Multiselect } from "./Form/Multiselect.svelte"
|
||||
export { default as Search } from "./Form/Search.svelte"
|
||||
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||
export { default as FieldLabel } from "./Form/FieldLabel.svelte"
|
||||
export { default as Slider } from "./Form/Slider.svelte"
|
||||
export { default as File } from "./Form/File.svelte"
|
||||
|
||||
|
|
|
@ -59,12 +59,14 @@
|
|||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.11.2",
|
||||
"@dagrejs/dagre": "1.1.4",
|
||||
"@fontsource/source-sans-pro": "^5.0.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
"@xyflow/svelte": "^0.1.18",
|
||||
"@zerodevx/svelte-json-view": "^1.0.7",
|
||||
"codemirror": "^5.65.16",
|
||||
"cron-parser": "^4.9.0",
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { Button, Modal } from "@budibase/bbui"
|
||||
import EditRolesModal from "../modals/EditRoles.svelte"
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Button secondary icon="UsersLock" on:click on:click={modal.show}>
|
||||
Edit roles
|
||||
</Button>
|
||||
</div>
|
||||
<Modal bind:this={modal} on:show on:hide>
|
||||
<EditRolesModal />
|
||||
</Modal>
|
|
@ -1,31 +1,195 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import { permissions } from "stores/builder"
|
||||
import ManageAccessModal from "../modals/ManageAccessModal.svelte"
|
||||
import {
|
||||
ActionButton,
|
||||
Input,
|
||||
Select,
|
||||
Label,
|
||||
List,
|
||||
ListItem,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { permissions as permissionsStore, roles } from "stores/builder"
|
||||
import DetailPopover from "components/common/DetailPopover.svelte"
|
||||
import EditRolesButton from "./EditRolesButton.svelte"
|
||||
import { PermissionSource } from "@budibase/types"
|
||||
import { capitalise } from "helpers"
|
||||
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
export let resourceId
|
||||
|
||||
let resourcePermissions
|
||||
const inheritedRoleId = "inherited"
|
||||
const builtins = [Roles.ADMIN, Roles.POWER, Roles.BASIC, Roles.PUBLIC]
|
||||
|
||||
let permissions
|
||||
let showPopover = true
|
||||
let dependantsInfoMessage
|
||||
|
||||
$: fetchPermissions(resourceId)
|
||||
$: loadDependantInfo(resourceId)
|
||||
$: roleMismatch = checkRoleMismatch(permissions)
|
||||
$: selectedRole = roleMismatch ? null : permissions?.[0]?.value
|
||||
$: readableRole = selectedRole
|
||||
? $roles.find(x => x._id === selectedRole)?.uiMetadata.displayName
|
||||
: null
|
||||
$: buttonLabel = readableRole ? `Access: ${readableRole}` : "Access"
|
||||
$: highlight = roleMismatch || selectedRole === Roles.PUBLIC
|
||||
|
||||
$: builtInRoles = builtins.map(roleId => $roles.find(x => x._id === roleId))
|
||||
$: customRoles = $roles
|
||||
.filter(x => !builtins.includes(x._id))
|
||||
.slice()
|
||||
.toSorted((a, b) => {
|
||||
const aName = a.uiMetadata.displayName || a.name
|
||||
const bName = b.uiMetadata.displayName || b.name
|
||||
return aName < bName ? -1 : 1
|
||||
})
|
||||
|
||||
const fetchPermissions = async id => {
|
||||
resourcePermissions = await permissions.forResourceDetailed(id)
|
||||
const res = await permissionsStore.forResourceDetailed(id)
|
||||
permissions = Object.entries(res?.permissions || {}).map(([perm, info]) => {
|
||||
let enriched = {
|
||||
permission: perm,
|
||||
value:
|
||||
info.permissionType === PermissionSource.INHERITED
|
||||
? inheritedRoleId
|
||||
: info.role,
|
||||
options: [...$roles],
|
||||
}
|
||||
if (info.inheritablePermission) {
|
||||
enriched.options.unshift({
|
||||
_id: inheritedRoleId,
|
||||
name: `Inherit (${
|
||||
$roles.find(x => x._id === info.inheritablePermission).name
|
||||
})`,
|
||||
})
|
||||
}
|
||||
return enriched
|
||||
})
|
||||
}
|
||||
|
||||
const checkRoleMismatch = permissions => {
|
||||
if (!permissions || permissions.length < 2) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
permissions[0].value !== permissions[1].value ||
|
||||
permissions[0].value === inheritedRoleId
|
||||
)
|
||||
}
|
||||
|
||||
const loadDependantInfo = async resourceId => {
|
||||
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
|
||||
const resourceByType = dependantsInfo?.resourceByType
|
||||
if (resourceByType) {
|
||||
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
|
||||
let resourceDisplay =
|
||||
Object.keys(resourceByType).length === 1 && resourceByType.view
|
||||
? "view"
|
||||
: "resource"
|
||||
|
||||
if (total === 1) {
|
||||
dependantsInfoMessage = `1 ${resourceDisplay} is inheriting this access`
|
||||
} else if (total > 1) {
|
||||
dependantsInfoMessage = `${total} ${resourceDisplay}s are inheriting this access`
|
||||
} else {
|
||||
dependantsInfoMessage = null
|
||||
}
|
||||
} else {
|
||||
dependantsInfoMessage = null
|
||||
}
|
||||
}
|
||||
|
||||
const changePermission = async role => {
|
||||
try {
|
||||
await permissionsStore.save({
|
||||
level: "read",
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
await permissionsStore.save({
|
||||
level: "write",
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
await fetchPermissions(resourceId)
|
||||
notifications.success("Updated permissions")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notifications.error("Error updating permissions")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetailPopover title="Manage access" {showPopover}>
|
||||
<DetailPopover title="Select access role" {showPopover}>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
<ActionButton icon="LockClosed" selected={open} quiet>Access</ActionButton>
|
||||
<ActionButton
|
||||
icon="LockClosed"
|
||||
selected={open || highlight}
|
||||
quiet
|
||||
accentColor={highlight ? "#ff0000" : null}
|
||||
>
|
||||
{buttonLabel}
|
||||
</ActionButton>
|
||||
</svelte:fragment>
|
||||
{#if resourcePermissions}
|
||||
<ManageAccessModal {resourceId} permissions={resourcePermissions} />
|
||||
|
||||
{#if roleMismatch}
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each permissions as permission}
|
||||
<Input value={capitalise(permission.permission)} disabled />
|
||||
<Select
|
||||
placeholder={false}
|
||||
value={permission.value}
|
||||
on:change={e => changePermission(e.detail)}
|
||||
disabled
|
||||
options={permission.options}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<InfoDisplay
|
||||
error
|
||||
icon="Alert"
|
||||
body="Your previous configuration is shown above.<br/> Please choose a single role for read and write access."
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<List>
|
||||
{#each builtInRoles as role}
|
||||
<ListItem
|
||||
title={role.uiMetadata.displayName}
|
||||
subtitle={role.uiMetadata.description}
|
||||
hoverable
|
||||
selected={selectedRole === role._id}
|
||||
icon="StatusLight"
|
||||
iconColor={role.uiMetadata.color}
|
||||
on:click={() => changePermission(role._id)}
|
||||
/>
|
||||
{/each}
|
||||
{#each customRoles as role}
|
||||
<ListItem
|
||||
title={role.uiMetadata.displayName}
|
||||
subtitle={role.uiMetadata.description}
|
||||
hoverable
|
||||
selected={selectedRole === role._id}
|
||||
icon="StatusLight"
|
||||
iconColor={role.uiMetadata.color}
|
||||
on:click={() => changePermission(role._id)}
|
||||
/>
|
||||
{/each}
|
||||
</List>
|
||||
|
||||
{#if dependantsInfoMessage}
|
||||
<InfoDisplay info body={dependantsInfoMessage} />
|
||||
{/if}
|
||||
<EditRolesButton
|
||||
on:show={() => (showPopover = false)}
|
||||
on:hide={() => (showPopover = true)}
|
||||
/>
|
||||
</DetailPopover>
|
||||
|
||||
<style>
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
const generateAutomation = () => {
|
||||
popover?.hide()
|
||||
dispatch("request-generate")
|
||||
dispatch("generate")
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
|
||||
<DetailPopover title="Generate" bind:this={popover}>
|
||||
<svelte:fragment slot="anchor" let:open>
|
||||
<ActionButton selected={open}>
|
||||
<ActionButton quiet selected={open}>
|
||||
<div class="center">
|
||||
<img height={16} alt="magic wand" src={MagicWand} />
|
||||
Generate
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
const generateScreen = () => {
|
||||
popover?.hide()
|
||||
dispatch("request-generate")
|
||||
dispatch("generate")
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@
|
|||
label="Role"
|
||||
bind:value={row.roleId}
|
||||
options={$roles}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionLabel={role => role.uiMetadata.displayName}
|
||||
getOptionValue={role => role._id}
|
||||
disabled={!creating}
|
||||
/>
|
||||
|
|
|
@ -1,174 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
keepOpen,
|
||||
ModalContent,
|
||||
Select,
|
||||
Input,
|
||||
Button,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||
import { roles } from "stores/builder"
|
||||
|
||||
const BASE_ROLE = { _id: "", inherits: "BASIC", permissionId: "write" }
|
||||
|
||||
let basePermissions = []
|
||||
let selectedRole = BASE_ROLE
|
||||
let errors = []
|
||||
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
||||
let validRegex = /^[a-zA-Z0-9_]*$/
|
||||
// Don't allow editing of public role
|
||||
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
|
||||
$: selectedRoleId = selectedRole._id
|
||||
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
|
||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||
|
||||
$: roleNameError = getRoleNameError(selectedRole.name)
|
||||
|
||||
$: valid =
|
||||
selectedRole.name &&
|
||||
selectedRole.inherits &&
|
||||
selectedRole.permissionId &&
|
||||
!builtInRoles.includes(selectedRole.name)
|
||||
|
||||
$: shouldDisableRoleInput =
|
||||
builtInRoles.includes(selectedRole.name) &&
|
||||
selectedRole.name?.toLowerCase() === selectedRoleId?.toLowerCase()
|
||||
|
||||
const fetchBasePermissions = async () => {
|
||||
try {
|
||||
basePermissions = await API.getBasePermissions()
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching base permission options")
|
||||
basePermissions = []
|
||||
}
|
||||
}
|
||||
|
||||
// Changes the selected role
|
||||
const changeRole = event => {
|
||||
const id = event?.detail
|
||||
const role = $roles.find(role => role._id === id)
|
||||
if (role) {
|
||||
selectedRole = {
|
||||
...role,
|
||||
inherits: role.inherits ?? "",
|
||||
permissionId: role.permissionId ?? "",
|
||||
}
|
||||
} else {
|
||||
selectedRole = BASE_ROLE
|
||||
}
|
||||
errors = []
|
||||
}
|
||||
|
||||
// Saves or creates the selected role
|
||||
const saveRole = async () => {
|
||||
errors = []
|
||||
|
||||
// Clean up empty strings
|
||||
const keys = ["_id", "inherits", "permissionId"]
|
||||
keys.forEach(key => {
|
||||
if (selectedRole[key] === "") {
|
||||
delete selectedRole[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Validation
|
||||
if (!selectedRole.name || selectedRole.name.trim() === "") {
|
||||
errors.push({ message: "Please enter a role name" })
|
||||
}
|
||||
if (!selectedRole.permissionId) {
|
||||
errors.push({ message: "Please choose permissions" })
|
||||
}
|
||||
if (errors.length) {
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
// Save/create the role
|
||||
try {
|
||||
await roles.save(selectedRole)
|
||||
notifications.success("Role saved successfully")
|
||||
} catch (error) {
|
||||
notifications.error(`Error saving role - ${error.message}`)
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
|
||||
// Deletes the selected role
|
||||
const deleteRole = async () => {
|
||||
try {
|
||||
await roles.delete(selectedRole)
|
||||
changeRole()
|
||||
notifications.success("Role deleted successfully")
|
||||
} catch (error) {
|
||||
notifications.error(`Error deleting role - ${error.message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleNameError = name => {
|
||||
const hasUniqueRoleName = !otherRoles
|
||||
?.map(role => role.name)
|
||||
?.includes(name)
|
||||
const invalidRoleName = !validRegex.test(name)
|
||||
if (!hasUniqueRoleName) {
|
||||
return "Select a unique role name."
|
||||
} else if (invalidRoleName) {
|
||||
return "Please enter a role name consisting of only alphanumeric symbols and underscores"
|
||||
}
|
||||
}
|
||||
|
||||
onMount(fetchBasePermissions)
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Edit Roles"
|
||||
confirmText={isCreating ? "Create" : "Save"}
|
||||
onConfirm={saveRole}
|
||||
disabled={!valid || roleNameError}
|
||||
>
|
||||
{#if errors.length}
|
||||
<ErrorsBox {errors} />
|
||||
{/if}
|
||||
<Select
|
||||
thin
|
||||
secondary
|
||||
label="Role"
|
||||
value={selectedRoleId}
|
||||
on:change={changeRole}
|
||||
options={editableRoles}
|
||||
placeholder="Create new role"
|
||||
getOptionValue={role => role._id}
|
||||
getOptionLabel={role => role.name}
|
||||
/>
|
||||
{#if selectedRole}
|
||||
<Input
|
||||
label="Name"
|
||||
bind:value={selectedRole.name}
|
||||
disabled={!!selectedRoleId}
|
||||
error={roleNameError}
|
||||
/>
|
||||
<Select
|
||||
label="Inherits Role"
|
||||
bind:value={selectedRole.inherits}
|
||||
options={selectedRole._id === "BASIC" ? $roles : otherRoles}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionLabel={role => role.name}
|
||||
disabled={shouldDisableRoleInput}
|
||||
/>
|
||||
<Select
|
||||
label="Base Permissions"
|
||||
bind:value={selectedRole.permissionId}
|
||||
options={basePermissions}
|
||||
getOptionValue={x => x._id}
|
||||
getOptionLabel={x => x.name}
|
||||
disabled={shouldDisableRoleInput}
|
||||
/>
|
||||
{/if}
|
||||
<div slot="footer">
|
||||
{#if !isCreating && !builtInRoles.includes(selectedRole.name)}
|
||||
<Button warning on:click={deleteRole}>Delete</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalContent>
|
|
@ -1,127 +0,0 @@
|
|||
<script>
|
||||
import { PermissionSource } from "@budibase/types"
|
||||
import { roles, permissions as permissionsStore } from "stores/builder"
|
||||
import {
|
||||
Label,
|
||||
Input,
|
||||
Select,
|
||||
notifications,
|
||||
Body,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let resourceId
|
||||
export let permissions
|
||||
|
||||
const inheritedRoleId = "inherited"
|
||||
|
||||
let dependantsInfoMessage
|
||||
|
||||
$: loadDependantInfo(resourceId)
|
||||
$: computedPermissions = Object.entries(permissions.permissions).reduce(
|
||||
(p, [level, roleInfo]) => {
|
||||
p[level] = {
|
||||
selectedValue:
|
||||
roleInfo.permissionType === PermissionSource.INHERITED
|
||||
? inheritedRoleId
|
||||
: roleInfo.role,
|
||||
options: [...$roles],
|
||||
}
|
||||
if (roleInfo.inheritablePermission) {
|
||||
p[level].inheritOption = roleInfo.inheritablePermission
|
||||
p[level].options.unshift({
|
||||
_id: inheritedRoleId,
|
||||
name: `Inherit (${
|
||||
get(roles).find(x => x._id === roleInfo.inheritablePermission).name
|
||||
})`,
|
||||
})
|
||||
}
|
||||
return p
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
async function changePermission(level, role) {
|
||||
try {
|
||||
if (role === inheritedRoleId) {
|
||||
await permissionsStore.remove({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
} else {
|
||||
await permissionsStore.save({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
}
|
||||
|
||||
// Show updated permissions in UI: REMOVE
|
||||
permissions = await permissionsStore.forResourceDetailed(resourceId)
|
||||
notifications.success("Updated permissions")
|
||||
} catch (error) {
|
||||
notifications.error("Error updating permissions")
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDependantInfo(resourceId) {
|
||||
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
|
||||
const resourceByType = dependantsInfo?.resourceByType
|
||||
if (resourceByType) {
|
||||
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
|
||||
let resourceDisplay =
|
||||
Object.keys(resourceByType).length === 1 && resourceByType.view
|
||||
? "view"
|
||||
: "resource"
|
||||
|
||||
if (total === 1) {
|
||||
dependantsInfoMessage = `1 ${resourceDisplay} is inheriting this access.`
|
||||
} else if (total > 1) {
|
||||
dependantsInfoMessage = `${total} ${resourceDisplay}s are inheriting this access.`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(computedPermissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
placeholder={false}
|
||||
value={computedPermissions[level].selectedValue}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={computedPermissions[level].options}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if dependantsInfoMessage}
|
||||
<div class="inheriting-resources">
|
||||
<Icon name="Alert" />
|
||||
<Body size="S">
|
||||
<i>
|
||||
{dependantsInfoMessage}
|
||||
</i>
|
||||
</Body>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
.inheriting-resources {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
|
@ -76,6 +76,13 @@
|
|||
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||
/>
|
||||
{/if}
|
||||
<NavItem
|
||||
icon="UserAdmin"
|
||||
text="Manage roles"
|
||||
selected={$isActive("./roles")}
|
||||
on:click={() => $goto("./roles")}
|
||||
selectedBy={$userSelectedResourceMap.roles}
|
||||
/>
|
||||
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
|
||||
<DatasourceNavItem
|
||||
{datasource}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<script>
|
||||
import { BaseEdge } from "@xyflow/svelte"
|
||||
import { NodeWidth, GridResolution } from "./constants"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let sourceX
|
||||
export let sourceY
|
||||
|
||||
const { bounds } = getContext("flow")
|
||||
|
||||
$: bracketWidth = GridResolution * 3
|
||||
$: bracketHeight = $bounds.height / 2 + GridResolution * 2
|
||||
$: path = getCurlyBracePath(
|
||||
sourceX + bracketWidth,
|
||||
sourceY - bracketHeight,
|
||||
sourceX + bracketWidth,
|
||||
sourceY + bracketHeight
|
||||
)
|
||||
|
||||
const getCurlyBracePath = (x1, y1, x2, y2) => {
|
||||
const w = 2 // Thickness
|
||||
const q = 1 // Intensity
|
||||
const i = 28 // Inner radius strenth (lower is stronger)
|
||||
const j = 32 // Outer radius strength (higher is stronger)
|
||||
|
||||
// Calculate unit vector
|
||||
var dx = x1 - x2
|
||||
var dy = y1 - y2
|
||||
var len = Math.sqrt(dx * dx + dy * dy)
|
||||
dx = dx / len
|
||||
dy = dy / len
|
||||
|
||||
// Path control points
|
||||
const qx1 = x1 + q * w * dy - j
|
||||
const qy1 = y1 - q * w * dx
|
||||
const qx2 = x1 - 0.25 * len * dx + (1 - q) * w * dy - i
|
||||
const qy2 = y1 - 0.25 * len * dy - (1 - q) * w * dx
|
||||
const tx1 = x1 - 0.5 * len * dx + w * dy - bracketWidth
|
||||
const ty1 = y1 - 0.5 * len * dy - w * dx
|
||||
const qx3 = x2 + q * w * dy - j
|
||||
const qy3 = y2 - q * w * dx
|
||||
const qx4 = x1 - 0.75 * len * dx + (1 - q) * w * dy - i
|
||||
const qy4 = y1 - 0.75 * len * dy - (1 - q) * w * dx
|
||||
|
||||
return `M ${x1} ${y1} Q ${qx1} ${qy1} ${qx2} ${qy2} T ${tx1} ${ty1} M ${x2} ${y2} Q ${qx3} ${qy3} ${qx4} ${qy4} T ${tx1} ${ty1}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<BaseEdge
|
||||
{...$$props}
|
||||
{path}
|
||||
style="--width:{NodeWidth}px; --x:{sourceX}px; --y:{sourceY}px;"
|
||||
/>
|
||||
|
||||
<style>
|
||||
:global(#basic-bracket) {
|
||||
animation-timing-function: linear(1, 0);
|
||||
}
|
||||
:global(#admin-bracket) {
|
||||
transform: scale(-1, 1) translateX(calc(var(--width) + 8px));
|
||||
transform-origin: var(--x) var(--y);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
<script>
|
||||
import { Button, ActionButton } from "@budibase/bbui"
|
||||
import { useSvelteFlow } from "@xyflow/svelte"
|
||||
import { getContext } from "svelte"
|
||||
import { ZoomDuration } from "./constants"
|
||||
|
||||
const { createRole, layoutAndFit } = getContext("flow")
|
||||
const flow = useSvelteFlow()
|
||||
</script>
|
||||
|
||||
<div class="control top-right">
|
||||
<div class="group">
|
||||
<ActionButton
|
||||
icon="Add"
|
||||
quiet
|
||||
on:click={() => flow.zoomIn({ duration: ZoomDuration })}
|
||||
/>
|
||||
<ActionButton
|
||||
icon="Remove"
|
||||
quiet
|
||||
on:click={() => flow.zoomOut({ duration: ZoomDuration })}
|
||||
/>
|
||||
</div>
|
||||
<Button secondary on:click={layoutAndFit}>Auto layout</Button>
|
||||
</div>
|
||||
<div class="control bottom-right">
|
||||
<Button icon="Add" cta on:click={createRole}>Add role</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.control {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.top-right {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
.bottom-right {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
.top-right :global(.spectrum-Button),
|
||||
.top-right :global(.spectrum-ActionButton),
|
||||
.top-right :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-gray-900) !important;
|
||||
}
|
||||
.top-right :global(.spectrum-Button),
|
||||
.top-right :global(.spectrum-ActionButton) {
|
||||
background: var(--spectrum-global-color-gray-200) !important;
|
||||
}
|
||||
.top-right :global(.spectrum-Button:hover),
|
||||
.top-right :global(.spectrum-ActionButton:hover) {
|
||||
background: var(--spectrum-global-color-gray-300) !important;
|
||||
}
|
||||
.group {
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.group :global(> *:not(:first-child)) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 2px solid var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.group :global(> *:not(:last-child)) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import { NodeWidth, NodeHeight } from "./constants"
|
||||
</script>
|
||||
|
||||
<div class="node" style={`--width:${NodeWidth}px; --height:${NodeHeight}px;`}>
|
||||
Add custom roles for more granular control over permissions
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.node {
|
||||
border-radius: 4px;
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
text-shadow: 4px 4px 10px var(--background-color),
|
||||
4px -4px 10px var(--background-color),
|
||||
-4px 4px 10px var(--background-color),
|
||||
-4px -4px 10px var(--background-color);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,123 @@
|
|||
<script>
|
||||
import { getBezierPath, BaseEdge, EdgeLabelRenderer } from "@xyflow/svelte"
|
||||
import { Icon, TooltipPosition } from "@budibase/bbui"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { roles } from "stores/builder"
|
||||
|
||||
export let sourceX
|
||||
export let sourceY
|
||||
export let sourcePosition
|
||||
export let targetX
|
||||
export let targetY
|
||||
export let targetPosition
|
||||
export let id
|
||||
export let source
|
||||
export let target
|
||||
|
||||
const { deleteEdge, selectedNodes } = getContext("flow")
|
||||
|
||||
let iconHovered = false
|
||||
let edgeHovered = false
|
||||
|
||||
$: hovered = iconHovered || edgeHovered
|
||||
$: active =
|
||||
hovered ||
|
||||
$selectedNodes.includes(source) ||
|
||||
$selectedNodes.includes(target)
|
||||
$: edgeClasses = getEdgeClasses(active, iconHovered)
|
||||
$: [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
})
|
||||
$: sourceRole = $roles.find(x => x._id === source)
|
||||
$: targetRole = $roles.find(x => x._id === target)
|
||||
$: tooltip =
|
||||
sourceRole && targetRole
|
||||
? `Stop ${targetRole.uiMetadata.displayName} from inheriting ${sourceRole.uiMetadata.displayName}`
|
||||
: null
|
||||
|
||||
const getEdgeClasses = (active, iconHovered) => {
|
||||
let classes = ""
|
||||
if (active) classes += `active `
|
||||
if (iconHovered) classes += `delete `
|
||||
return classes
|
||||
}
|
||||
|
||||
const onEdgeMouseOver = () => {
|
||||
edgeHovered = true
|
||||
}
|
||||
|
||||
const onEdgeMouseOut = () => {
|
||||
edgeHovered = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const edge = document.querySelector(`.svelte-flow__edge[data-id="${id}"]`)
|
||||
if (edge) {
|
||||
edge.addEventListener("mouseover", onEdgeMouseOver)
|
||||
edge.addEventListener("mouseout", onEdgeMouseOut)
|
||||
}
|
||||
return () => {
|
||||
if (edge) {
|
||||
edge.removeEventListener("mouseover", onEdgeMouseOver)
|
||||
edge.removeEventListener("mouseout", onEdgeMouseOut)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<BaseEdge path={edgePath} class={edgeClasses} />
|
||||
<EdgeLabelRenderer>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
style:transform="translate(-50%, -50%) translate({labelX}px,{labelY}px)"
|
||||
class="edge-label nodrag nopan"
|
||||
class:active
|
||||
on:click={() => deleteEdge(id)}
|
||||
on:mouseover={() => (iconHovered = true)}
|
||||
on:mouseout={() => (iconHovered = false)}
|
||||
>
|
||||
<Icon
|
||||
name="Delete"
|
||||
size="S"
|
||||
{tooltip}
|
||||
tooltipPosition={TooltipPosition.Top}
|
||||
/>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
|
||||
<style>
|
||||
.edge-label {
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.edge-label.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
}
|
||||
.edge-label:hover :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
.edge-label :global(.spectrum-Icon) {
|
||||
background: var(--background-color);
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.edge-label :global(svg) {
|
||||
padding: 4px;
|
||||
}
|
||||
:global(.svelte-flow__edge-path.active) {
|
||||
stroke: var(--spectrum-global-color-blue-400);
|
||||
}
|
||||
:global(.svelte-flow__edge-path.active.delete) {
|
||||
stroke: var(--spectrum-global-color-red-400);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import { SvelteFlowProvider } from "@xyflow/svelte"
|
||||
import RoleFlow from "./RoleFlow.svelte"
|
||||
</script>
|
||||
|
||||
<SvelteFlowProvider>
|
||||
<RoleFlow />
|
||||
</SvelteFlowProvider>
|
|
@ -0,0 +1,234 @@
|
|||
<script>
|
||||
import { Heading, Helpers, notifications } from "@budibase/bbui"
|
||||
import { writable, derived } from "svelte/store"
|
||||
import {
|
||||
SvelteFlow,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
useSvelteFlow,
|
||||
} from "@xyflow/svelte"
|
||||
import "@xyflow/svelte/dist/style.css"
|
||||
import RoleNode from "./RoleNode.svelte"
|
||||
import EmptyStateNode from "./EmptyStateNode.svelte"
|
||||
import RoleEdge from "./RoleEdge.svelte"
|
||||
import BracketEdge from "./BracketEdge.svelte"
|
||||
import {
|
||||
autoLayout,
|
||||
getAdminPosition,
|
||||
getBasicPosition,
|
||||
rolesToLayout,
|
||||
nodeToRole,
|
||||
getBounds,
|
||||
} from "./utils"
|
||||
import { setContext, tick } from "svelte"
|
||||
import Controls from "./Controls.svelte"
|
||||
import { GridResolution, MaxAutoZoom, ZoomDuration } from "./constants"
|
||||
import { roles } from "stores/builder"
|
||||
import { Roles } from "constants/backend"
|
||||
import { getSequentialName } from "helpers/duplicate"
|
||||
import { derivedMemo } from "@budibase/frontend-core"
|
||||
|
||||
const flow = useSvelteFlow()
|
||||
const edges = writable([])
|
||||
const nodes = writable([])
|
||||
const dragging = writable(false)
|
||||
|
||||
// Derive the list of selected nodes
|
||||
const selectedNodes = derived(nodes, $nodes => {
|
||||
return $nodes.filter(node => node.selected).map(node => node.id)
|
||||
})
|
||||
|
||||
// Derive the bounds of all custom role nodes
|
||||
const bounds = derivedMemo(nodes, getBounds)
|
||||
|
||||
$: handleExternalRoleChanges($roles)
|
||||
$: updateBuiltins($bounds)
|
||||
|
||||
// Updates nodes and edges based on external changes to roles
|
||||
const handleExternalRoleChanges = roles => {
|
||||
const currentNodes = $nodes
|
||||
const newLayout = autoLayout(rolesToLayout(roles))
|
||||
edges.set(newLayout.edges)
|
||||
|
||||
// For nodes we want to persist some metadata if possible
|
||||
nodes.set(
|
||||
newLayout.nodes.map(node => {
|
||||
const currentNode = currentNodes.find(x => x.id === node.id)
|
||||
if (!currentNode) {
|
||||
return node
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
position: currentNode.position || node.position,
|
||||
selected: currentNode.selected || node.selected,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Positions the basic and admin role at either edge of the flow
|
||||
const updateBuiltins = bounds => {
|
||||
flow.updateNode(Roles.BASIC, {
|
||||
position: getBasicPosition(bounds),
|
||||
})
|
||||
flow.updateNode(Roles.ADMIN, {
|
||||
position: getAdminPosition(bounds),
|
||||
})
|
||||
}
|
||||
|
||||
// Automatically lays out all roles and edges and zooms to fit them
|
||||
const layoutAndFit = () => {
|
||||
const layout = autoLayout({ nodes: $nodes, edges: $edges })
|
||||
nodes.set(layout.nodes)
|
||||
edges.set(layout.edges)
|
||||
flow.fitView({ maxZoom: MaxAutoZoom, duration: ZoomDuration })
|
||||
}
|
||||
|
||||
const createRole = async () => {
|
||||
const roleId = Helpers.uuid()
|
||||
await roles.save({
|
||||
name: roleId,
|
||||
uiMetadata: {
|
||||
displayName: getSequentialName($roles, "New role ", {
|
||||
getName: role => role.uiMetadata.displayName,
|
||||
}),
|
||||
color: "var(--spectrum-global-color-gray-700)",
|
||||
description: "Custom role",
|
||||
},
|
||||
inherits: [Roles.BASIC],
|
||||
})
|
||||
await tick()
|
||||
layoutAndFit()
|
||||
|
||||
// Select the new node
|
||||
nodes.update($nodes => {
|
||||
return $nodes.map(node => ({
|
||||
...node,
|
||||
selected: node.id === roleId,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const updateRole = async (roleId, metadata) => {
|
||||
const node = $nodes.find(node => node.id === roleId)
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
// Update metadata immediately, before saving
|
||||
if (metadata) {
|
||||
flow.updateNodeData(roleId, metadata)
|
||||
}
|
||||
try {
|
||||
await roles.save(nodeToRole({ node, edges: $edges }))
|
||||
layoutAndFit()
|
||||
} catch (error) {
|
||||
notifications.error(error?.message || error || "Failed to update role")
|
||||
handleExternalRoleChanges($roles)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteRole = async roleId => {
|
||||
nodes.set($nodes.filter(node => node.id !== roleId))
|
||||
layoutAndFit()
|
||||
const role = $roles.find(role => role._id === roleId)
|
||||
if (role) {
|
||||
roles.delete(role)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEdge = async edgeId => {
|
||||
const edge = $edges.find(edge => edge.id === edgeId)
|
||||
edges.set($edges.filter(edge => edge.id !== edgeId))
|
||||
await updateRole(edge.target)
|
||||
}
|
||||
|
||||
const onConnect = async connection => {
|
||||
await updateRole(connection.target)
|
||||
}
|
||||
|
||||
setContext("flow", {
|
||||
nodes,
|
||||
edges,
|
||||
dragging,
|
||||
selectedNodes,
|
||||
bounds,
|
||||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
deleteEdge,
|
||||
layoutAndFit,
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="title">
|
||||
<div class="heading" />
|
||||
</div>
|
||||
<div class="flow">
|
||||
<SvelteFlow
|
||||
fitView
|
||||
{nodes}
|
||||
{edges}
|
||||
snapGrid={[GridResolution, GridResolution]}
|
||||
nodeTypes={{ role: RoleNode, empty: EmptyStateNode }}
|
||||
edgeTypes={{ role: RoleEdge, bracket: BracketEdge }}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
fitViewOptions={{ maxZoom: MaxAutoZoom }}
|
||||
defaultEdgeOptions={{ type: "role", animated: true, selectable: false }}
|
||||
onconnectstart={() => dragging.set(true)}
|
||||
onconnectend={() => dragging.set(false)}
|
||||
onconnect={onConnect}
|
||||
deleteKey={null}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Controls />
|
||||
<div class="title">
|
||||
<Heading size="S">Manage roles</Heading>
|
||||
</div>
|
||||
<div class="footer">Roles inherit permissions from each other</div>
|
||||
</SvelteFlow>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flow {
|
||||
margin: -28px -40px -40px -40px;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
--background-color: var(--spectrum-global-color-gray-50);
|
||||
--border-color: var(--spectrum-global-color-gray-300);
|
||||
--edge-color: var(--spectrum-global-color-gray-500);
|
||||
--handle-color: var(--spectrum-global-color-gray-600);
|
||||
--selected-color: var(--spectrum-global-color-blue-400);
|
||||
}
|
||||
.title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 20px;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Customise svelte-flow theme */
|
||||
.flow :global(.svelte-flow) {
|
||||
/* Panel */
|
||||
--xy-background-color: var(--background-color);
|
||||
|
||||
/* Controls */
|
||||
--xy-controls-button-border-color: var(--border-color);
|
||||
|
||||
/* Handles */
|
||||
--xy-handle-background-color: var(--handle-color);
|
||||
--xy-handle-border-color: var(--handle-color);
|
||||
|
||||
/* Edges */
|
||||
--xy-edge-stroke: var(--edge-color);
|
||||
--xy-edge-stroke-selected: var(--edge-color);
|
||||
--xy-edge-stroke-width: 2px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,231 @@
|
|||
<script>
|
||||
import { Handle, Position } from "@xyflow/svelte"
|
||||
import {
|
||||
Icon,
|
||||
Input,
|
||||
ColorPicker,
|
||||
Modal,
|
||||
ModalContent,
|
||||
FieldLabel,
|
||||
} from "@budibase/bbui"
|
||||
import { NodeWidth, NodeHeight } from "./constants"
|
||||
import { getContext } from "svelte"
|
||||
import { roles } from "stores/builder"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
||||
export let data
|
||||
export let id
|
||||
export let selected
|
||||
export let isConnectable
|
||||
|
||||
const { dragging, updateRole, deleteRole } = getContext("flow")
|
||||
|
||||
let anchor
|
||||
let modal
|
||||
let tempDisplayName
|
||||
let tempDescription
|
||||
let tempColor
|
||||
let deleteModal
|
||||
|
||||
$: nameError = validateName(tempDisplayName, $roles)
|
||||
$: descriptionError = validateDescription(tempDescription)
|
||||
$: invalid = nameError || descriptionError
|
||||
|
||||
const validateName = (name, roles) => {
|
||||
if (!name?.length) {
|
||||
return "Please enter a name"
|
||||
}
|
||||
if (roles.some(x => x.uiMetadata.displayName === name && x._id !== id)) {
|
||||
return "That name is already used by another role"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const validateDescription = description => {
|
||||
if (!description?.length) {
|
||||
return "Please enter a name"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const openPopover = e => {
|
||||
e.stopPropagation()
|
||||
tempDisplayName = data.displayName
|
||||
tempDescription = data.description
|
||||
tempColor = data.color
|
||||
modal.show()
|
||||
}
|
||||
|
||||
const saveChanges = () => {
|
||||
updateRole(id, {
|
||||
displayName: tempDisplayName,
|
||||
description: tempDescription,
|
||||
color: tempColor,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="node"
|
||||
class:dragging={$dragging}
|
||||
class:selected
|
||||
class:interactive={data.interactive}
|
||||
class:custom={data.custom}
|
||||
class:selectable={isConnectable}
|
||||
style={`--color:${data.color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`}
|
||||
bind:this={anchor}
|
||||
>
|
||||
<div class="color" />
|
||||
<div class="content">
|
||||
<div class="text">
|
||||
<div class="name">
|
||||
{data.displayName}
|
||||
</div>
|
||||
{#if data.description}
|
||||
<div class="description" title={data.description}>
|
||||
{data.description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if data.custom}
|
||||
<div class="buttons">
|
||||
<Icon size="S" name="Edit" hoverable on:click={openPopover} />
|
||||
<Icon size="S" name="Delete" hoverable on:click={deleteModal?.show} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable && $dragging && data.custom}
|
||||
/>
|
||||
<Handle type="source" position={Position.Right} {isConnectable} />
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={deleteModal}
|
||||
title={`Delete ${data.displayName}`}
|
||||
body="Are you sure you want to delete this role? This can't be undone."
|
||||
okText="Delete"
|
||||
onOk={async () => await deleteRole(id)}
|
||||
/>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
title={`Edit ${data.displayName}`}
|
||||
confirmText="Save"
|
||||
onConfirm={saveChanges}
|
||||
disabled={invalid}
|
||||
>
|
||||
<Input
|
||||
label="Name"
|
||||
value={tempDisplayName}
|
||||
error={nameError}
|
||||
on:change={e => (tempDisplayName = e.detail)}
|
||||
/>
|
||||
<Input
|
||||
label="Description"
|
||||
value={tempDescription}
|
||||
error={descriptionError}
|
||||
on:change={e => (tempDescription = e.detail)}
|
||||
/>
|
||||
<div>
|
||||
<FieldLabel label="Color" />
|
||||
<ColorPicker value={tempColor} on:change={e => (tempColor = e.detail)} />
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
/* Node styles */
|
||||
.node {
|
||||
position: relative;
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
border-radius: 4px;
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.node.selectable:hover {
|
||||
cursor: pointer;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.node.selectable.selected {
|
||||
background: var(--spectrum-global-color-blue-100);
|
||||
cursor: grab;
|
||||
}
|
||||
.color {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
flex: 0 0 10px;
|
||||
background: var(--color);
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid var(--border-color);
|
||||
border-left-width: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
padding: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
.node.selected .content {
|
||||
border-color: var(--spectrum-global-color-blue-100);
|
||||
}
|
||||
|
||||
/* Text */
|
||||
.text {
|
||||
width: 0;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
.name,
|
||||
.description {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.description {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.buttons :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
/* Handles */
|
||||
.node :global(.svelte-flow__handle) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-width: 2px;
|
||||
}
|
||||
.node :global(.svelte-flow__handle.target) {
|
||||
background: var(--background-color);
|
||||
}
|
||||
.node:not(.dragging) :global(.svelte-flow__handle.target),
|
||||
.node:not(.interactive) :global(.svelte-flow__handle),
|
||||
.node:not(.custom) :global(.svelte-flow__handle.target) {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
export const ZoomDuration = 300
|
||||
export const MaxAutoZoom = 1.2
|
||||
export const GridResolution = 20
|
||||
export const NodeHeight = GridResolution * 3
|
||||
export const NodeWidth = GridResolution * 12
|
||||
export const NodeHSpacing = GridResolution * 6
|
||||
export const NodeVSpacing = GridResolution * 2
|
||||
export const MinHeight = GridResolution * 10
|
||||
export const EmptyStateID = "empty"
|
|
@ -0,0 +1,245 @@
|
|||
import dagre from "@dagrejs/dagre"
|
||||
import {
|
||||
NodeWidth,
|
||||
NodeHeight,
|
||||
GridResolution,
|
||||
NodeHSpacing,
|
||||
NodeVSpacing,
|
||||
MinHeight,
|
||||
EmptyStateID,
|
||||
} from "./constants"
|
||||
import { getNodesBounds, Position } from "@xyflow/svelte"
|
||||
import { Roles } from "constants/backend"
|
||||
import { roles } from "stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
// Calculates the bounds of all custom nodes
|
||||
export const getBounds = nodes => {
|
||||
const interactiveNodes = nodes.filter(node => node.data.interactive)
|
||||
|
||||
// Empty state bounds which line up with bounds after adding first node
|
||||
if (!interactiveNodes.length) {
|
||||
return {
|
||||
x: 0,
|
||||
y: -3.5 * GridResolution,
|
||||
width: 12 * GridResolution,
|
||||
height: 10 * GridResolution,
|
||||
}
|
||||
}
|
||||
let bounds = getNodesBounds(interactiveNodes)
|
||||
|
||||
// Enforce a min size
|
||||
if (bounds.height < MinHeight) {
|
||||
const diff = MinHeight - bounds.height
|
||||
bounds.height = MinHeight
|
||||
bounds.y -= diff / 2
|
||||
}
|
||||
return bounds
|
||||
}
|
||||
|
||||
// Gets the position of the basic role
|
||||
export const getBasicPosition = bounds => ({
|
||||
x: bounds.x - NodeHSpacing - NodeWidth,
|
||||
y: bounds.y + bounds.height / 2 - NodeHeight / 2,
|
||||
})
|
||||
|
||||
// Gets the position of the admin role
|
||||
export const getAdminPosition = bounds => ({
|
||||
x: bounds.x + bounds.width + NodeHSpacing,
|
||||
y: bounds.y + bounds.height / 2 - NodeHeight / 2,
|
||||
})
|
||||
|
||||
// Filters out invalid nodes and edges
|
||||
const preProcessLayout = ({ nodes, edges }) => {
|
||||
const ignoredIds = [Roles.PUBLIC, Roles.BASIC, Roles.ADMIN, EmptyStateID]
|
||||
const targetlessIds = [Roles.POWER]
|
||||
return {
|
||||
nodes: nodes.filter(node => {
|
||||
// Filter out ignored IDs
|
||||
if (ignoredIds.includes(node.id)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
edges: edges.filter(edge => {
|
||||
// Filter out edges from ignored IDs
|
||||
if (
|
||||
ignoredIds.includes(edge.source) ||
|
||||
ignoredIds.includes(edge.target)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// Filter out edges which have the same source and target
|
||||
if (edge.source === edge.target) {
|
||||
return false
|
||||
}
|
||||
// Filter out edges which target targetless roles
|
||||
if (targetlessIds.includes(edge.target)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Updates positions of nodes and edges into a nice graph structure
|
||||
export const dagreLayout = ({ nodes, edges }) => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
dagreGraph.setGraph({
|
||||
rankdir: "LR",
|
||||
ranksep: NodeHSpacing,
|
||||
nodesep: NodeVSpacing,
|
||||
})
|
||||
nodes.forEach(node => {
|
||||
dagreGraph.setNode(node.id, { width: NodeWidth, height: NodeHeight })
|
||||
})
|
||||
edges.forEach(edge => {
|
||||
dagreGraph.setEdge(edge.source, edge.target)
|
||||
})
|
||||
dagre.layout(dagreGraph)
|
||||
nodes.forEach(node => {
|
||||
const pos = dagreGraph.node(node.id)
|
||||
node.targetPosition = Position.Left
|
||||
node.sourcePosition = Position.Right
|
||||
node.position = {
|
||||
x: Math.round((pos.x - NodeWidth / 2) / GridResolution) * GridResolution,
|
||||
y: Math.round((pos.y - NodeHeight / 2) / GridResolution) * GridResolution,
|
||||
}
|
||||
})
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
const postProcessLayout = ({ nodes, edges }) => {
|
||||
// Add basic and admin nodes at each edge
|
||||
const bounds = getBounds(nodes)
|
||||
const $roles = get(roles)
|
||||
nodes.push({
|
||||
...roleToNode($roles.find(role => role._id === Roles.BASIC)),
|
||||
position: getBasicPosition(bounds),
|
||||
})
|
||||
nodes.push({
|
||||
...roleToNode($roles.find(role => role._id === Roles.ADMIN)),
|
||||
position: getAdminPosition(bounds),
|
||||
})
|
||||
|
||||
// Add custom edges for basic and admin brackets
|
||||
edges.push({
|
||||
id: "basic-bracket",
|
||||
source: Roles.BASIC,
|
||||
target: Roles.ADMIN,
|
||||
type: "bracket",
|
||||
})
|
||||
edges.push({
|
||||
id: "admin-bracket",
|
||||
source: Roles.ADMIN,
|
||||
target: Roles.BASIC,
|
||||
type: "bracket",
|
||||
})
|
||||
|
||||
// Add empty state node if required
|
||||
if (!nodes.some(node => node.data.interactive)) {
|
||||
nodes.push({
|
||||
id: EmptyStateID,
|
||||
type: "empty",
|
||||
position: {
|
||||
x: bounds.x + bounds.width / 2 - NodeWidth / 2,
|
||||
y: bounds.y + bounds.height / 2 - NodeHeight / 2,
|
||||
},
|
||||
data: {},
|
||||
measured: {
|
||||
width: NodeWidth,
|
||||
height: NodeHeight,
|
||||
},
|
||||
deletable: false,
|
||||
draggable: false,
|
||||
connectable: false,
|
||||
selectable: false,
|
||||
})
|
||||
}
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
// Automatically lays out the graph, sanitising and enriching the structure
|
||||
export const autoLayout = ({ nodes, edges }) => {
|
||||
return postProcessLayout(dagreLayout(preProcessLayout({ nodes, edges })))
|
||||
}
|
||||
|
||||
// Converts a role doc into a node structure
|
||||
export const roleToNode = role => {
|
||||
const custom = ![
|
||||
Roles.PUBLIC,
|
||||
Roles.BASIC,
|
||||
Roles.POWER,
|
||||
Roles.ADMIN,
|
||||
Roles.BUILDER,
|
||||
].includes(role._id)
|
||||
const interactive = custom || role._id === Roles.POWER
|
||||
return {
|
||||
id: role._id,
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
type: "role",
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
...role.uiMetadata,
|
||||
custom,
|
||||
interactive,
|
||||
},
|
||||
measured: {
|
||||
width: NodeWidth,
|
||||
height: NodeHeight,
|
||||
},
|
||||
deletable: custom,
|
||||
draggable: interactive,
|
||||
connectable: interactive,
|
||||
selectable: interactive,
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a node structure back into a role doc
|
||||
export const nodeToRole = ({ node, edges }) => ({
|
||||
...get(roles).find(role => role._id === node.id),
|
||||
inherits: edges
|
||||
.filter(x => x.target === node.id)
|
||||
.map(x => x.source)
|
||||
.concat(Roles.BASIC),
|
||||
uiMetadata: {
|
||||
displayName: node.data.displayName,
|
||||
color: node.data.color,
|
||||
description: node.data.description,
|
||||
},
|
||||
})
|
||||
|
||||
// Builds a default layout from an array of roles
|
||||
export const rolesToLayout = roles => {
|
||||
let nodes = []
|
||||
let edges = []
|
||||
|
||||
// Add all nodes and edges
|
||||
for (let role of roles) {
|
||||
// Add node for this role
|
||||
nodes.push(roleToNode(role))
|
||||
|
||||
// Add edges for this role
|
||||
let inherits = []
|
||||
if (role.inherits) {
|
||||
inherits = Array.isArray(role.inherits) ? role.inherits : [role.inherits]
|
||||
}
|
||||
for (let sourceRole of inherits) {
|
||||
if (!roles.some(x => x._id === sourceRole)) {
|
||||
continue
|
||||
}
|
||||
edges.push({
|
||||
id: `${sourceRole}-${role._id}`,
|
||||
source: sourceRole,
|
||||
target: role._id,
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
<script>
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { StatusLight } from "@budibase/bbui"
|
||||
import { roles } from "stores/builder"
|
||||
|
||||
export let id
|
||||
export let size = "M"
|
||||
export let disabled = false
|
||||
|
||||
$: color = RoleUtils.getRoleColour(id)
|
||||
$: color =
|
||||
$roles.find(x => x._id === id)?.color ||
|
||||
"var(--spectrum-global-color-static-magenta-400)"
|
||||
</script>
|
||||
|
||||
<StatusLight square {disabled} {size} {color} />
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { roles } from "stores/builder"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
import { Constants, RoleUtils } from "@budibase/frontend-core"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
|
@ -49,7 +49,8 @@
|
|||
let options = roles
|
||||
.filter(role => allowedRoles.includes(role._id))
|
||||
.map(role => ({
|
||||
name: enrichLabel(role.name),
|
||||
color: role.uiMetadata.color,
|
||||
name: enrichLabel(role.uiMetadata.displayName),
|
||||
_id: role._id,
|
||||
}))
|
||||
if (allowedRoles.includes(Constants.Roles.CREATOR)) {
|
||||
|
@ -64,7 +65,8 @@
|
|||
|
||||
// Allow all core roles
|
||||
let options = roles.map(role => ({
|
||||
name: enrichLabel(role.name),
|
||||
color: role.uiMetadata.color,
|
||||
name: enrichLabel(role.uiMetadata.displayName),
|
||||
_id: role._id,
|
||||
}))
|
||||
|
||||
|
@ -100,7 +102,7 @@
|
|||
if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) {
|
||||
return null
|
||||
}
|
||||
return RoleUtils.getRoleColour(role._id)
|
||||
return role.color || "var(--spectrum-global-color-static-magenta-400)"
|
||||
}
|
||||
|
||||
const getIcon = role => {
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { roles } from "stores/builder"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
export let value
|
||||
export let error
|
||||
export let placeholder = null
|
||||
export let autoWidth = false
|
||||
</script>
|
||||
|
||||
<Select
|
||||
bind:value
|
||||
on:change
|
||||
options={$roles}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionLabel={role => role.uiMetadata.displayName}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
|
||||
getOptionColour={role => role.uiMetadata.color}
|
||||
{placeholder}
|
||||
{error}
|
||||
{autoWidth}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { Label, notifications, Select } from "@budibase/bbui"
|
||||
import { permissions, roles } from "stores/builder"
|
||||
import { Label, notifications } from "@budibase/bbui"
|
||||
import { permissions } from "stores/builder"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
|
||||
export let query
|
||||
export let label
|
||||
|
@ -52,12 +53,5 @@
|
|||
{#if label}
|
||||
<Label>{label}</Label>
|
||||
{/if}
|
||||
<Select
|
||||
value={roleId}
|
||||
on:change={e => updateRole(e.detail)}
|
||||
options={$roles}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
autoWidth
|
||||
/>
|
||||
<RoleSelect value={roleId} on:change={e => updateRole(e.detail)} autoWidth />
|
||||
{/if}
|
||||
|
|
|
@ -503,7 +503,7 @@
|
|||
on:save={saveQuery}
|
||||
/>
|
||||
<div class="access">
|
||||
<Label>Access level</Label>
|
||||
<Label>Access</Label>
|
||||
<AccessLevelSelect {query} {saveId} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -725,10 +725,10 @@ const getRoleBindings = () => {
|
|||
return {
|
||||
type: "context",
|
||||
runtimeBinding: `'${role._id}'`,
|
||||
readableBinding: `Role.${role.name}`,
|
||||
readableBinding: `Role.${role.uiMetadata.displayName}`,
|
||||
category: "Role",
|
||||
icon: "UserGroup",
|
||||
display: { type: "string", name: role.name },
|
||||
display: { type: "string", name: role.uiMetadata.displayName },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -228,7 +228,7 @@
|
|||
.top-nav {
|
||||
flex: 0 0 60px;
|
||||
background: var(--background);
|
||||
padding-left: var(--spacing-xl);
|
||||
padding: 0 var(--spacing-xl);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
flex-direction: row;
|
||||
|
@ -269,6 +269,7 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-right: calc(-1 * var(--spacing-xl));
|
||||
}
|
||||
|
||||
.toprightnav :global(.avatars) {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import RoleEditor from "components/backend/RoleEditor/RoleEditor.svelte"
|
||||
import { builderStore } from "stores/builder"
|
||||
|
||||
builderStore.selectResource("roles")
|
||||
</script>
|
||||
|
||||
<RoleEditor />
|
|
@ -56,15 +56,13 @@
|
|||
buttonsCollapsed
|
||||
>
|
||||
<svelte:fragment slot="controls">
|
||||
<GridManageAccessButton />
|
||||
<GridFilterButton />
|
||||
<GridSortButton />
|
||||
<GridSizeButton />
|
||||
<GridColumnsSettingButton />
|
||||
<GridManageAccessButton />
|
||||
<GridRowActionsButton />
|
||||
<GridScreensButton on:request-generate={() => generateButton?.show()} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls-right">
|
||||
<GridScreensButton on:generate={() => generateButton?.show()} />
|
||||
<GridGenerateButton bind:this={generateButton} />
|
||||
</svelte:fragment>
|
||||
<GridCreateEditRowModal />
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
integrations,
|
||||
appStore,
|
||||
rowActions,
|
||||
roles,
|
||||
} from "stores/builder"
|
||||
import { themeStore, admin, licensing } from "stores/portal"
|
||||
import { TableNames } from "constants"
|
||||
|
@ -26,16 +27,20 @@
|
|||
import GridRowActionsButton from "components/backend/DataTable/buttons/grid/GridRowActionsButton.svelte"
|
||||
import { DB_TYPE_EXTERNAL } from "constants/backend"
|
||||
|
||||
const userSchemaOverrides = {
|
||||
let generateButton
|
||||
|
||||
$: userSchemaOverrides = {
|
||||
firstName: { displayName: "First name", disabled: true },
|
||||
lastName: { displayName: "Last name", disabled: true },
|
||||
email: { displayName: "Email", disabled: true },
|
||||
roleId: { displayName: "Role", disabled: true },
|
||||
status: { displayName: "Status", disabled: true },
|
||||
roleId: {
|
||||
displayName: "Role",
|
||||
type: "role",
|
||||
disabled: true,
|
||||
roles: $roles,
|
||||
},
|
||||
}
|
||||
|
||||
let generateButton
|
||||
|
||||
$: autoColumnStatus = verifyAutocolumns($tables?.selected)
|
||||
$: duplicates = Object.values(autoColumnStatus).reduce((acc, status) => {
|
||||
if (status.length > 1) {
|
||||
|
@ -141,17 +146,11 @@
|
|||
<GridRelationshipButton />
|
||||
{/if}
|
||||
{#if !isUsersTable}
|
||||
<GridRowActionsButton />
|
||||
<GridScreensButton on:request-generate={() => generateButton?.show()} />
|
||||
<GridAutomationsButton
|
||||
on:request-generate={() => generateButton?.show()}
|
||||
/>
|
||||
<GridImportButton />
|
||||
{/if}
|
||||
<GridExportButton />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls-right">
|
||||
{#if !isUsersTable}
|
||||
<GridExportButton />
|
||||
<GridRowActionsButton />
|
||||
<GridScreensButton on:generate={() => generateButton?.show()} />
|
||||
<GridAutomationsButton on:generate={() => generateButton?.show()} />
|
||||
<GridGenerateButton bind:this={generateButton} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
export let title
|
||||
export let body
|
||||
export let icon = "HelpOutline"
|
||||
export let warning = false
|
||||
export let error = false
|
||||
</script>
|
||||
|
||||
<div class="info" class:noTitle={!title}>
|
||||
<div class="info" class:noTitle={!title} class:warning class:error>
|
||||
{#if title}
|
||||
<div class="title">
|
||||
<Icon name={icon} />
|
||||
|
@ -16,7 +18,7 @@
|
|||
{@html body}
|
||||
{:else}
|
||||
<span class="icon">
|
||||
<Icon name={icon} />
|
||||
<Icon size="S" name={icon} />
|
||||
</span>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html body}
|
||||
|
@ -24,6 +26,23 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.info {
|
||||
padding: var(--spacing-m) var(--spacing-l);
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
border-radius: var(--border-radius-s);
|
||||
font-size: 13px;
|
||||
}
|
||||
.warning {
|
||||
background: rgba(255, 200, 0, 0.2);
|
||||
}
|
||||
.error {
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
}
|
||||
.noTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
@ -39,17 +58,7 @@
|
|||
.icon {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.info {
|
||||
padding: var(--spacing-m) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
||||
background-color: var(--background-alt);
|
||||
border-radius: var(--border-radius-s);
|
||||
font-size: 13px;
|
||||
}
|
||||
.noTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.info :global(a) {
|
||||
color: inherit;
|
||||
transition: color 130ms ease-out;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { Tooltip, StatusLight } from "@budibase/bbui"
|
||||
import { roles } from "stores/builder"
|
||||
import { Roles } from "constants/backend"
|
||||
|
@ -8,12 +7,13 @@
|
|||
|
||||
let showTooltip = false
|
||||
|
||||
$: color = RoleUtils.getRoleColour(roleId)
|
||||
$: role = $roles.find(role => role._id === roleId)
|
||||
$: color =
|
||||
role?.uiMetadata.color || "var(--spectrum-global-color-static-magenta-400)"
|
||||
$: tooltip =
|
||||
roleId === Roles.PUBLIC
|
||||
? "Open to the public"
|
||||
: `Requires ${role?.name} access`
|
||||
: `Requires ${role?.uiMetadata.displayName || "Unknown role"} access`
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
|
||||
export let value
|
||||
export let row
|
||||
$: count = getCount(Object.keys(value || {}).length)
|
||||
|
||||
const getCount = () => {
|
||||
$: count = getCount(row, value)
|
||||
|
||||
const getCount = (row, value) => {
|
||||
return sdk.users.hasAppBuilderPermissions(row)
|
||||
? row.builder.apps.length +
|
||||
Object.keys(row.roles || {}).filter(appId =>
|
||||
row.builder.apps.includes(appId)
|
||||
).length
|
||||
: value?.length || 0
|
||||
: Object.keys(value || {}).length
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
<script>
|
||||
import { StatusLight } from "@budibase/bbui"
|
||||
import { RoleUtils, Constants } from "@budibase/frontend-core"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { roles } from "stores/builder"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let value
|
||||
|
||||
$: role = $roles.find(x => x._id === value)
|
||||
|
||||
const getRoleLabel = roleId => {
|
||||
const role = $roles.find(x => x._id === roleId)
|
||||
return roleId === Constants.Roles.CREATOR
|
||||
? capitalise(Constants.Roles.CREATOR.toLowerCase())
|
||||
: role?.name || "Custom role"
|
||||
: role?.uiMetadata.displayName || role?.name || "Custom role"
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value === Constants.Roles.CREATOR}
|
||||
Can edit
|
||||
{:else}
|
||||
<StatusLight square color={RoleUtils.getRoleColour(value)}>
|
||||
<StatusLight
|
||||
square
|
||||
color={role?.uiMetadata.color ||
|
||||
"var(--spectrum-global-color-static-magenta-400)"}
|
||||
>
|
||||
Can use as {getRoleLabel(value)}
|
||||
</StatusLight>
|
||||
{/if}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<script>
|
||||
import TagsTableRenderer from "./TagsTableRenderer.svelte"
|
||||
|
||||
export let value
|
||||
|
||||
$: roles = value?.filter(role => role != null).map(role => role.name) ?? []
|
||||
</script>
|
||||
|
||||
<TagsTableRenderer value={roles} />
|
|
@ -1,35 +0,0 @@
|
|||
<script>
|
||||
import { Tag, Tags } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
|
||||
const displayLimit = 5
|
||||
|
||||
$: values = value?.filter(value => value != null) ?? []
|
||||
$: tags = values.slice(0, displayLimit)
|
||||
$: leftover = values.length - tags.length
|
||||
</script>
|
||||
|
||||
<div class="tag-renderer">
|
||||
<Tags>
|
||||
{#each tags as tag}
|
||||
<Tag>
|
||||
{tag}
|
||||
</Tag>
|
||||
{/each}
|
||||
{#if leftover}
|
||||
<Tag>+{leftover} more</Tag>
|
||||
{/if}
|
||||
</Tags>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tag-renderer :global(.spectrum-Tags-item:hover) {
|
||||
color: var(--spectrum-alias-label-text-color);
|
||||
border-color: var(--spectrum-alias-border-color-darker-default);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tag-renderer :global(.spectrum-Tags-itemLabel) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -1,74 +0,0 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Body, Select, ModalContent, notifications } from "@budibase/bbui"
|
||||
import { users } from "stores/portal"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
export let app
|
||||
export let user
|
||||
|
||||
const NO_ACCESS = "NO_ACCESS"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const roles = app.roles
|
||||
let options = roles
|
||||
.filter(role => role._id !== "PUBLIC")
|
||||
.map(role => ({ value: role._id, label: role.name }))
|
||||
|
||||
if (!sdk.users.isBuilder(user, app?.appId)) {
|
||||
options.push({ value: NO_ACCESS, label: "No Access" })
|
||||
}
|
||||
let selectedRole = user?.roles?.[app?._id]
|
||||
|
||||
async function updateUserRoles() {
|
||||
try {
|
||||
if (selectedRole === NO_ACCESS) {
|
||||
// Remove the user role
|
||||
const filteredRoles = { ...user.roles }
|
||||
delete filteredRoles[app?._id]
|
||||
await users.save({
|
||||
...user,
|
||||
roles: {
|
||||
...filteredRoles,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Add the user role
|
||||
await users.save({
|
||||
...user,
|
||||
roles: {
|
||||
...user.roles,
|
||||
[app._id]: selectedRole,
|
||||
},
|
||||
})
|
||||
}
|
||||
notifications.success("Role updated")
|
||||
dispatch("update")
|
||||
} catch (error) {
|
||||
notifications.error("Failed to update role")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={updateUserRoles}
|
||||
title="Update App Role"
|
||||
confirmText="Update role"
|
||||
cancelText="Cancel"
|
||||
size="M"
|
||||
showCloseIcon={false}
|
||||
>
|
||||
<Body>
|
||||
Update {user.email}'s role for <strong>{app.name}</strong>.
|
||||
</Body>
|
||||
<Select
|
||||
placeholder={null}
|
||||
bind:value={selectedRole}
|
||||
on:change
|
||||
{options}
|
||||
label="Role"
|
||||
getOptionLabel={role => role.label}
|
||||
getOptionValue={role => role.value}
|
||||
/>
|
||||
</ModalContent>
|
|
@ -1,16 +1,34 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { derived, writable, get } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
export function createRolesStore() {
|
||||
const { subscribe, update, set } = writable([])
|
||||
const store = writable([])
|
||||
const enriched = derived(store, $store => {
|
||||
return $store.map(role => ({
|
||||
...role,
|
||||
|
||||
// Ensure we have new metadata for all roles
|
||||
uiMetadata: {
|
||||
displayName: role.uiMetadata?.displayName || role.name,
|
||||
color:
|
||||
role.uiMetadata?.color || "var(--spectrum-global-color-magenta-400)",
|
||||
description: role.uiMetadata?.description || "Custom role",
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
function setRoles(roles) {
|
||||
set(
|
||||
store.set(
|
||||
roles.sort((a, b) => {
|
||||
const priorityA = RoleUtils.getRolePriority(a._id)
|
||||
const priorityB = RoleUtils.getRolePriority(b._id)
|
||||
return priorityA > priorityB ? -1 : 1
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA > priorityB ? -1 : 1
|
||||
}
|
||||
const nameA = a.uiMetadata?.displayName || a.name
|
||||
const nameB = b.uiMetadata?.displayName || b.name
|
||||
return nameA < nameB ? -1 : 1
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -29,17 +47,43 @@ export function createRolesStore() {
|
|||
roleId: role?._id,
|
||||
roleRev: role?._rev,
|
||||
})
|
||||
update(state => state.filter(existing => existing._id !== role._id))
|
||||
await actions.fetch()
|
||||
},
|
||||
save: async role => {
|
||||
const savedRole = await API.saveRole(role)
|
||||
await actions.fetch()
|
||||
return savedRole
|
||||
},
|
||||
replace: (roleId, role) => {
|
||||
// Handles external updates of roles
|
||||
if (!roleId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle deletion
|
||||
if (!role) {
|
||||
store.update(state => state.filter(x => x._id !== roleId))
|
||||
return
|
||||
}
|
||||
|
||||
// Add new role
|
||||
const index = get(store).findIndex(x => x._id === role._id)
|
||||
if (index === -1) {
|
||||
store.update(state => [...state, role])
|
||||
}
|
||||
|
||||
// Update existing role
|
||||
else if (role) {
|
||||
store.update(state => {
|
||||
state[index] = role
|
||||
return [...state]
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
subscribe: enriched.subscribe,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
snippets,
|
||||
datasources,
|
||||
tables,
|
||||
roles,
|
||||
} from "stores/builder"
|
||||
import { get } from "svelte/store"
|
||||
import { auth, appsStore } from "stores/portal"
|
||||
|
@ -56,12 +57,18 @@ export const createBuilderWebsocket = appId => {
|
|||
datasources.replaceDatasource(id, datasource)
|
||||
})
|
||||
|
||||
// Role events
|
||||
socket.onOther(BuilderSocketEvent.RoleChange, ({ id, role }) => {
|
||||
roles.replace(id, role)
|
||||
})
|
||||
|
||||
// Design section events
|
||||
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
|
||||
screenStore.replace(id, screen)
|
||||
})
|
||||
|
||||
// App events
|
||||
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
|
||||
//Sync app metadata across the stores
|
||||
appStore.syncMetadata(metadata)
|
||||
themeStore.syncMetadata(metadata)
|
||||
navigationStore.syncMetadata(metadata)
|
||||
|
@ -79,7 +86,7 @@ export const createBuilderWebsocket = appId => {
|
|||
}
|
||||
)
|
||||
|
||||
// Automations
|
||||
// Automation events
|
||||
socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => {
|
||||
automationStore.actions.replace(id, automation)
|
||||
})
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
<script>
|
||||
import { Heading, Select, ActionButton } from "@budibase/bbui"
|
||||
import { devToolsStore, appStore, roleStore } from "../../stores"
|
||||
import { devToolsStore, appStore } from "../../stores"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
|
||||
const context = getContext("context")
|
||||
const SELF_ROLE = "self"
|
||||
|
||||
let staticRoleList
|
||||
let roles
|
||||
|
||||
$: previewOptions = buildRoleList(staticRoleList)
|
||||
$: previewOptions = buildRoleList(roles)
|
||||
|
||||
function buildRoleList(roleIds) {
|
||||
function buildRoleList(roles) {
|
||||
const list = []
|
||||
list.push({
|
||||
label: "View as yourself",
|
||||
value: SELF_ROLE,
|
||||
})
|
||||
if (!roleIds) {
|
||||
if (!roles) {
|
||||
return list
|
||||
}
|
||||
for (let roleId of roleIds) {
|
||||
for (let role of roles) {
|
||||
list.push({
|
||||
label: `View as ${roleId.toLowerCase()} user`,
|
||||
value: roleId,
|
||||
label: `View as ${role.uiMetadata?.displayName || role.name}`,
|
||||
value: role._id,
|
||||
})
|
||||
}
|
||||
return list
|
||||
|
@ -31,7 +32,7 @@
|
|||
onMount(async () => {
|
||||
// make sure correct before starting
|
||||
await devToolsStore.actions.changeRole(SELF_ROLE)
|
||||
staticRoleList = await roleStore.actions.fetchAccessibleRoles()
|
||||
roles = await API.getRoles()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
focus: () => api?.focus?.(),
|
||||
blur: () => api?.blur?.(),
|
||||
isActive: () => api?.isActive?.() ?? false,
|
||||
onKeyDown: (...params) => api?.onKeyDown(...params),
|
||||
onKeyDown: (...params) => api?.onKeyDown?.(...params),
|
||||
isReadonly: () => readonly,
|
||||
getType: () => column.schema.type,
|
||||
getValue: () => row[column.name],
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { StatusLight } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
export let schema
|
||||
|
||||
$: role = schema.roles?.find(x => x._id === value)
|
||||
</script>
|
||||
|
||||
<div class="role-cell">
|
||||
<div class="light">
|
||||
<StatusLight
|
||||
square
|
||||
size="L"
|
||||
color={role?.uiMetadata?.color ||
|
||||
"var(--spectrum-global-color-static-magenta-400)"}
|
||||
/>
|
||||
</div>
|
||||
<div class="value">
|
||||
{role?.uiMetadata?.displayName || role?.name || "Unknown role"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.role-cell {
|
||||
flex: 1 1 auto;
|
||||
padding: var(--cell-padding);
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
gap: var(--cell-padding);
|
||||
}
|
||||
.light {
|
||||
height: 20px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.value {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
|
@ -16,6 +16,7 @@ import AttachmentSingleCell from "../cells/AttachmentSingleCell.svelte"
|
|||
import BBReferenceCell from "../cells/BBReferenceCell.svelte"
|
||||
import SignatureCell from "../cells/SignatureCell.svelte"
|
||||
import BBReferenceSingleCell from "../cells/BBReferenceSingleCell.svelte"
|
||||
import RoleCell from "../cells/RoleCell.svelte"
|
||||
|
||||
const TypeComponentMap = {
|
||||
[FieldType.STRING]: TextCell,
|
||||
|
@ -35,6 +36,9 @@ const TypeComponentMap = {
|
|||
[FieldType.JSON]: JSONCell,
|
||||
[FieldType.BB_REFERENCE]: BBReferenceCell,
|
||||
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
||||
|
||||
// Custom types for UI only
|
||||
role: RoleCell,
|
||||
}
|
||||
export const getCellRenderer = column => {
|
||||
return (
|
||||
|
|
|
@ -7,20 +7,7 @@ const RolePriorities = {
|
|||
[Roles.BASIC]: 2,
|
||||
[Roles.PUBLIC]: 1,
|
||||
}
|
||||
const RoleColours = {
|
||||
[Roles.ADMIN]: "var(--spectrum-global-color-static-red-400)",
|
||||
[Roles.CREATOR]: "var(--spectrum-global-color-static-magenta-600)",
|
||||
[Roles.POWER]: "var(--spectrum-global-color-static-orange-400)",
|
||||
[Roles.BASIC]: "var(--spectrum-global-color-static-green-400)",
|
||||
[Roles.PUBLIC]: "var(--spectrum-global-color-static-blue-400)",
|
||||
}
|
||||
|
||||
export const getRolePriority = role => {
|
||||
return RolePriorities[role] ?? 0
|
||||
}
|
||||
|
||||
export const getRoleColour = roleId => {
|
||||
return (
|
||||
RoleColours[roleId] ?? "var(--spectrum-global-color-static-magenta-400)"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,9 +18,11 @@ import {
|
|||
UserCtx,
|
||||
UserMetadata,
|
||||
DocumentType,
|
||||
PermissionLevel,
|
||||
} from "@budibase/types"
|
||||
import { RoleColor, sdk as sharedSdk, helpers } from "@budibase/shared-core"
|
||||
import sdk from "../../sdk"
|
||||
import { builderSocket } from "../../websockets"
|
||||
|
||||
const UpdateRolesOptions = {
|
||||
CREATED: "created",
|
||||
|
@ -34,11 +36,11 @@ async function removeRoleFromOthers(roleId: string) {
|
|||
let changed = false
|
||||
if (Array.isArray(role.inherits)) {
|
||||
const newInherits = role.inherits.filter(
|
||||
id => !roles.compareRoleIds(id, roleId)
|
||||
id => !roles.roleIDsAreEqual(id, roleId)
|
||||
)
|
||||
changed = role.inherits.length !== newInherits.length
|
||||
role.inherits = newInherits
|
||||
} else if (role.inherits && roles.compareRoleIds(role.inherits, roleId)) {
|
||||
} else if (role.inherits && roles.roleIDsAreEqual(role.inherits, roleId)) {
|
||||
role.inherits = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||
changed = true
|
||||
}
|
||||
|
@ -124,6 +126,17 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
|||
ctx.throw(400, "Cannot change custom role name")
|
||||
}
|
||||
|
||||
// custom roles should always inherit basic - if they don't inherit anything else
|
||||
if (!inherits && roles.validInherits(allRoles, dbRole?.inherits)) {
|
||||
inherits = dbRole?.inherits
|
||||
} else if (!roles.validInherits(allRoles, inherits)) {
|
||||
inherits = [roles.BUILTIN_ROLE_IDS.BASIC]
|
||||
}
|
||||
// assume write permission level for newly created roles
|
||||
if (isCreate && !permissionId) {
|
||||
permissionId = PermissionLevel.WRITE
|
||||
}
|
||||
|
||||
const role = new roles.Role(_id, name, permissionId, {
|
||||
displayName: uiMetadata?.displayName || name,
|
||||
description: uiMetadata?.description || "Custom role",
|
||||
|
@ -177,6 +190,7 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
|
|||
},
|
||||
})
|
||||
}
|
||||
builderSocket?.emitRoleUpdate(ctx, role)
|
||||
}
|
||||
|
||||
export async function destroy(ctx: UserCtx<void, DestroyRoleResponse>) {
|
||||
|
@ -216,6 +230,7 @@ export async function destroy(ctx: UserCtx<void, DestroyRoleResponse>) {
|
|||
|
||||
ctx.message = `Role ${ctx.params.roleId} deleted successfully`
|
||||
ctx.status = 200
|
||||
builderSocket?.emitRoleDeletion(ctx, role)
|
||||
}
|
||||
|
||||
export async function accessible(ctx: UserCtx<void, AccessibleRolesResponse>) {
|
||||
|
@ -223,35 +238,23 @@ export async function accessible(ctx: UserCtx<void, AccessibleRolesResponse>) {
|
|||
if (!roleId) {
|
||||
roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||
}
|
||||
// If a custom role is provided in the header, filter out higher level roles
|
||||
const roleHeader = ctx.header[Header.PREVIEW_ROLE]
|
||||
if (Array.isArray(roleHeader)) {
|
||||
ctx.throw(400, `Too many roles specified in ${Header.PREVIEW_ROLE} header`)
|
||||
}
|
||||
const isBuilder = ctx.user && sharedSdk.users.isAdminOrBuilder(ctx.user)
|
||||
let roleIds: string[] = []
|
||||
if (ctx.user && sharedSdk.users.isAdminOrBuilder(ctx.user)) {
|
||||
if (!roleHeader && isBuilder) {
|
||||
const appId = context.getAppId()
|
||||
if (appId) {
|
||||
roleIds = await roles.getAllRoleIds(appId)
|
||||
}
|
||||
} else if (isBuilder && roleHeader) {
|
||||
roleIds = await roles.getUserRoleIdHierarchy(roleHeader)
|
||||
} else {
|
||||
roleIds = await roles.getUserRoleIdHierarchy(roleId!)
|
||||
}
|
||||
|
||||
// If a custom role is provided in the header, filter out higher level roles
|
||||
const roleHeader = ctx.header?.[Header.PREVIEW_ROLE] as string
|
||||
if (roleHeader && !Object.keys(roles.BUILTIN_ROLE_IDS).includes(roleHeader)) {
|
||||
const role = await roles.getRole(roleHeader)
|
||||
const inherits = role?.inherits
|
||||
const orderedRoles = roleIds.reverse()
|
||||
let filteredRoles = [roleHeader]
|
||||
for (let role of orderedRoles) {
|
||||
filteredRoles = [role, ...filteredRoles]
|
||||
if (
|
||||
(Array.isArray(inherits) && inherits.includes(role)) ||
|
||||
role === inherits
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
filteredRoles.pop()
|
||||
roleIds = [roleHeader, ...filteredRoles]
|
||||
}
|
||||
|
||||
ctx.body = roleIds.map(roleId => roles.getExternalRoleID(roleId))
|
||||
}
|
||||
|
|
|
@ -38,6 +38,26 @@ describe("/roles", () => {
|
|||
_id: dbCore.prefixRoleID(res._id!),
|
||||
})
|
||||
})
|
||||
|
||||
it("handle a role with invalid inherits", async () => {
|
||||
const role = basicRole()
|
||||
role.inherits = ["not_real", "some_other_not_real"]
|
||||
|
||||
const res = await config.api.roles.save(role, {
|
||||
status: 200,
|
||||
})
|
||||
expect(res.inherits).toEqual([BUILTIN_ROLE_IDS.BASIC])
|
||||
})
|
||||
|
||||
it("handle a role with no inherits", async () => {
|
||||
const role = basicRole()
|
||||
role.inherits = []
|
||||
|
||||
const res = await config.api.roles.save(role, {
|
||||
status: 200,
|
||||
})
|
||||
expect(res.inherits).toEqual([BUILTIN_ROLE_IDS.BASIC])
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
|
@ -149,6 +169,17 @@ describe("/roles", () => {
|
|||
{ status: 400, body: { message: LOOP_ERROR } }
|
||||
)
|
||||
})
|
||||
|
||||
it("handle updating a role, without its inherits", async () => {
|
||||
const res = await config.api.roles.save({
|
||||
...basicRole(),
|
||||
inherits: [BUILTIN_ROLE_IDS.ADMIN],
|
||||
})
|
||||
// remove the roles so that it will default back to DB roles, then save again
|
||||
delete res.inherits
|
||||
const updatedRes = await config.api.roles.save(res)
|
||||
expect(updatedRes.inherits).toEqual([BUILTIN_ROLE_IDS.ADMIN])
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
|
@ -298,6 +329,23 @@ describe("/roles", () => {
|
|||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("should fetch preview role correctly even without basic specified", async () => {
|
||||
const role = await config.api.roles.save(basicRole())
|
||||
// have to forcefully delete the inherits from DB - technically can't
|
||||
// happen anymore - but good test case
|
||||
await dbCore.getDB(config.appId!).put({
|
||||
...role,
|
||||
_id: dbCore.prefixRoleID(role._id!),
|
||||
inherits: [],
|
||||
})
|
||||
await config.withHeaders({ "x-budibase-role": role.name }, async () => {
|
||||
const res = await config.api.roles.accessible({
|
||||
status: 200,
|
||||
})
|
||||
expect(res).toEqual([role.name])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("accessible - multi-inheritance", () => {
|
||||
|
|
|
@ -86,7 +86,6 @@ describe("/screens", () => {
|
|||
status: 200,
|
||||
}
|
||||
)
|
||||
// basic and role1 screen
|
||||
expect(res.screens.length).toEqual(screenIds.length)
|
||||
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
|
||||
})
|
||||
|
@ -107,6 +106,25 @@ describe("/screens", () => {
|
|||
screen2._id!,
|
||||
])
|
||||
})
|
||||
|
||||
it("should be able to fetch basic and screen 1 with role1 in role header", async () => {
|
||||
await config.withHeaders(
|
||||
{
|
||||
"x-budibase-role": role1._id!,
|
||||
},
|
||||
async () => {
|
||||
const res = await config.api.application.getDefinition(
|
||||
config.prodAppId!,
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
)
|
||||
const screenIds = [screen._id!, screen1._id!]
|
||||
expect(res.screens.length).toEqual(screenIds.length)
|
||||
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort())
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("save", () => {
|
||||
|
|
|
@ -56,22 +56,9 @@ export default async (ctx: UserCtx, next: any) => {
|
|||
ctx.request &&
|
||||
(ctx.request.headers[constants.Header.PREVIEW_ROLE] as string)
|
||||
if (isBuilder && isDevApp && roleHeader) {
|
||||
// Ensure the role is valid by ensuring a definition exists
|
||||
try {
|
||||
if (roleHeader) {
|
||||
const role = await roles.getRole(roleHeader)
|
||||
if (role) {
|
||||
roleId = roleHeader
|
||||
|
||||
// Delete admin and builder flags so that the specified role is honoured
|
||||
ctx.user = users.removePortalUserPermissions(
|
||||
ctx.user
|
||||
) as ContextUser
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Swallow error and do nothing
|
||||
}
|
||||
roleId = roleHeader
|
||||
// Delete admin and builder flags so that the specified role is honoured
|
||||
ctx.user = users.removePortalUserPermissions(ctx.user) as ContextUser
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,31 +8,31 @@ import {
|
|||
} from "../../automations"
|
||||
import {
|
||||
AIOperationEnum,
|
||||
AutoFieldSubType,
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
AutomationEventType,
|
||||
AutomationResults,
|
||||
AutomationStatus,
|
||||
AutomationStep,
|
||||
AutomationStepType,
|
||||
AutomationTrigger,
|
||||
AutomationTriggerStepId,
|
||||
BBReferenceFieldSubType,
|
||||
CreateViewRequest,
|
||||
Datasource,
|
||||
FieldSchema,
|
||||
FieldType,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
JsonFieldSubType,
|
||||
LoopStepType,
|
||||
Query,
|
||||
Role,
|
||||
SourceName,
|
||||
Table,
|
||||
INTERNAL_TABLE_SOURCE_ID,
|
||||
TableSourceType,
|
||||
Query,
|
||||
Webhook,
|
||||
WebhookActionType,
|
||||
AutomationEventType,
|
||||
LoopStepType,
|
||||
FieldSchema,
|
||||
BBReferenceFieldSubType,
|
||||
JsonFieldSubType,
|
||||
AutoFieldSubType,
|
||||
Role,
|
||||
CreateViewRequest,
|
||||
} from "@budibase/types"
|
||||
import { LoopInput } from "../../definitions/automations"
|
||||
import { merge } from "lodash"
|
||||
|
@ -439,7 +439,7 @@ export function updateRowAutomationWithFilters(
|
|||
appId: string,
|
||||
tableId: string
|
||||
): Automation {
|
||||
const automation: Automation = {
|
||||
return {
|
||||
name: "updateRowWithFilters",
|
||||
type: "automation",
|
||||
appId,
|
||||
|
@ -472,7 +472,6 @@ export function updateRowAutomationWithFilters(
|
|||
},
|
||||
},
|
||||
}
|
||||
return automation
|
||||
}
|
||||
|
||||
export function basicAutomationResults(
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Screen,
|
||||
App,
|
||||
Automation,
|
||||
Role,
|
||||
} from "@budibase/types"
|
||||
import { gridSocket } from "./index"
|
||||
import { clearLock, updateLock } from "../utilities/redis"
|
||||
|
@ -100,6 +101,20 @@ export default class BuilderSocket extends BaseSocket {
|
|||
})
|
||||
}
|
||||
|
||||
emitRoleUpdate(ctx: any, role: Role) {
|
||||
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.RoleChange, {
|
||||
id: role._id,
|
||||
role,
|
||||
})
|
||||
}
|
||||
|
||||
emitRoleDeletion(ctx: any, role: Role) {
|
||||
this.emitToRoom(ctx, ctx.appId, BuilderSocketEvent.RoleChange, {
|
||||
id: role._id,
|
||||
role: null,
|
||||
})
|
||||
}
|
||||
|
||||
emitTableUpdate(ctx: any, table: Table, options?: EmitOptions) {
|
||||
if (table.sourceId == null || table.sourceId === "") {
|
||||
throw new Error("Table sourceId is not set")
|
||||
|
|
|
@ -97,6 +97,7 @@ export enum BuilderSocketEvent {
|
|||
SelectResource = "SelectResource",
|
||||
AppPublishChange = "AppPublishChange",
|
||||
AutomationChange = "AutomationChange",
|
||||
RoleChange = "RoleChange",
|
||||
}
|
||||
|
||||
export const SocketSessionTTL = 60
|
||||
|
|
145
yarn.lock
145
yarn.lock
|
@ -2343,6 +2343,18 @@
|
|||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@dagrejs/dagre@1.1.4":
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.4.tgz#66f9c0e2b558308f2c268f60e2c28f22ee17e339"
|
||||
integrity sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==
|
||||
dependencies:
|
||||
"@dagrejs/graphlib" "2.2.4"
|
||||
|
||||
"@dagrejs/graphlib@2.2.4":
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.4.tgz#d77bfa9ff49e2307c0c6e6b8b26b5dd3c05816c4"
|
||||
integrity sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==
|
||||
|
||||
"@datadog/native-appsec@7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-7.0.0.tgz#a380174dd49aef2d9bb613a0ec8ead6dc7822095"
|
||||
|
@ -5093,6 +5105,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-4.3.1.tgz#d333fa41909f691c8750b5c15ad9ba029df2248e"
|
||||
integrity sha512-rX6Iasu9BsFMVgEN0vGRPm9dmSxva+IK/uqQAa9HM0lliwqUiFrJxrFXHHpiAgNuux/U4srEJwbSpGzfF+CegQ==
|
||||
|
||||
"@svelte-put/shortcut@^3.1.0":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@svelte-put/shortcut/-/shortcut-3.1.1.tgz#aba4d7407024d5cff38727e12925c8f81e877079"
|
||||
integrity sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==
|
||||
|
||||
"@sveltejs/vite-plugin-svelte@1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.4.0.tgz#412a735de489ca731d0c780c2b410f45dd95b392"
|
||||
|
@ -5451,6 +5468,45 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/d3-color@*":
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
|
||||
integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
|
||||
|
||||
"@types/d3-drag@^3.0.7":
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
|
||||
integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
|
||||
dependencies:
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/d3-interpolate@*":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
|
||||
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
|
||||
dependencies:
|
||||
"@types/d3-color" "*"
|
||||
|
||||
"@types/d3-selection@*", "@types/d3-selection@^3.0.10":
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
|
||||
integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
|
||||
|
||||
"@types/d3-transition@^3.0.8":
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f"
|
||||
integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==
|
||||
dependencies:
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/d3-zoom@^3.0.8":
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
|
||||
integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
|
||||
dependencies:
|
||||
"@types/d3-interpolate" "*"
|
||||
"@types/d3-selection" "*"
|
||||
|
||||
"@types/debug@*":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
||||
|
@ -6578,6 +6634,28 @@
|
|||
loupe "^3.1.1"
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
"@xyflow/svelte@^0.1.18":
|
||||
version "0.1.18"
|
||||
resolved "https://registry.yarnpkg.com/@xyflow/svelte/-/svelte-0.1.18.tgz#ba2f9f72adc64ff6f71a5ad03cf759af8d7c9748"
|
||||
integrity sha512-P2td3XcvMk36pnhyRUAXtmwfd7sv1KAHVF29YZUNndYlgxG98vwj1UoyyuXwCHIiyu82GgowaTppHCNPXsvNSg==
|
||||
dependencies:
|
||||
"@svelte-put/shortcut" "^3.1.0"
|
||||
"@xyflow/system" "0.0.41"
|
||||
classcat "^5.0.4"
|
||||
|
||||
"@xyflow/system@0.0.41":
|
||||
version "0.0.41"
|
||||
resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.41.tgz#6c314b2bbca594aec4d7cdb56efb003be6727d21"
|
||||
integrity sha512-XAjs8AUA0YMfYD91cT6pLGALwbsPS64s2WBHyULqL1m0gTqXqaUSLK1P7qA/Q8HecN0RFbqlM2tPO8bmZXP0YQ==
|
||||
dependencies:
|
||||
"@types/d3-drag" "^3.0.7"
|
||||
"@types/d3-selection" "^3.0.10"
|
||||
"@types/d3-transition" "^3.0.8"
|
||||
"@types/d3-zoom" "^3.0.8"
|
||||
d3-drag "^3.0.0"
|
||||
d3-selection "^3.0.0"
|
||||
d3-zoom "^3.0.0"
|
||||
|
||||
"@yarnpkg/lockfile@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
||||
|
@ -8244,6 +8322,11 @@ cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107"
|
||||
integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==
|
||||
|
||||
classcat@^5.0.4:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77"
|
||||
integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==
|
||||
|
||||
clean-stack@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
||||
|
@ -9159,6 +9242,68 @@ curlconverter@3.21.0:
|
|||
string.prototype.startswith "^1.0.0"
|
||||
yamljs "^0.3.0"
|
||||
|
||||
"d3-color@1 - 3":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
|
||||
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
|
||||
|
||||
"d3-dispatch@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
|
||||
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
|
||||
|
||||
"d3-drag@2 - 3", d3-drag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
|
||||
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-selection "3"
|
||||
|
||||
"d3-ease@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
|
||||
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
||||
|
||||
"d3-interpolate@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
|
||||
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
|
||||
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
|
||||
|
||||
"d3-timer@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
||||
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
||||
|
||||
"d3-transition@2 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
|
||||
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
d3-dispatch "1 - 3"
|
||||
d3-ease "1 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
d3-zoom@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
|
||||
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-drag "2 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-selection "2 - 3"
|
||||
d3-transition "2 - 3"
|
||||
|
||||
dargs@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc"
|
||||
|
|
Loading…
Reference in New Issue