Add deleted permissions file, update auto layout

This commit is contained in:
Andrew Kingston 2024-09-11 13:06:07 +01:00
parent f1aca4c7df
commit 9f0c160bfa
No known key found for this signature in database
6 changed files with 198 additions and 67 deletions

View File

@ -1,12 +1,14 @@
<script>
import { Button, Helpers, ActionButton } from "@budibase/bbui"
import { useSvelteFlow, Position } from "@xyflow/svelte"
import { getContext } from "svelte"
import { getContext, tick } from "svelte"
import { autoLayout } from "./layout"
import { ZoomDuration } from "./constants"
const { nodes, autoLayout } = getContext("flow")
const { nodes, edges } = getContext("flow")
const flow = useSvelteFlow()
const addRole = () => {
const addRole = async () => {
nodes.update(state => [
...state,
{
@ -22,19 +24,42 @@
position: { x: 0, y: 0 },
},
])
autoLayout()
await doAutoLayout()
}
const doAutoLayout = async () => {
const layout = autoLayout({ nodes: $nodes, edges: $edges })
nodes.set(layout.nodes)
edges.set(layout.edges)
await tick()
flow.fitView({
maxZoom: 1,
duration: ZoomDuration,
includeHiddenNodes: true,
})
}
</script>
<div class="control top-left">
<div class="group">
<ActionButton icon="Add" quiet on:click={flow.zoomIn} />
<ActionButton icon="Remove" quiet on:click={flow.zoomOut} />
<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={() => flow.fitView({ maxZoom: 1 })}>
<Button
secondary
on:click={() => flow.fitView({ maxZoom: 1, duration: ZoomDuration })}
>
Zoom to fit
</Button>
<Button secondary on:click={autoLayout}>Auto layout</Button>
<Button secondary on:click={doAutoLayout}>Auto layout</Button>
</div>
<div class="control bottom-right">
<Button icon="Add" cta on:click={addRole}>Add role</Button>

View File

@ -1,57 +1,20 @@
<script>
import { Heading, Helpers } from "@budibase/bbui"
import { derived, writable } from "svelte/store"
import { Heading } from "@budibase/bbui"
import { writable } from "svelte/store"
import { SvelteFlow, Background, BackgroundVariant } from "@xyflow/svelte"
import "@xyflow/svelte/dist/style.css"
import RoleNode from "./RoleNode.svelte"
import { rolesToLayout, dagreLayout, layoutToRoles } from "./layout"
import { onMount, setContext, tick } from "svelte"
import { defaultLayout, autoLayout } from "./layout"
import { onMount, setContext } from "svelte"
import Controls from "./Controls.svelte"
import { Roles } from "constants/backend"
import { derivedMemo } from "@budibase/frontend-core"
const nodes = writable([])
const edgeStore = writable([])
const enrichedEdges = derived([edgeStore, nodes], ([$edgeStore, $nodes]) => {
let additions = []
for (let node of $nodes) {
// If a certain node does not inherit anything, make it inherit basic
if (
!$edgeStore.some(x => x.target === node.id) &&
node.id !== Roles.BASIC
) {
additions.push({
id: Helpers.uuid(),
source: Roles.BASIC,
target: node.id,
animated: true,
})
}
}
return [...$edgeStore, ...additions]
})
const edges = {
...edgeStore,
subscribe: enrichedEdges.subscribe,
}
const edges = writable([])
const roles = derivedMemo([nodes, edges], ([$nodes, $edges]) => {
return layoutToRoles({ nodes: $nodes, edges: $edges })
})
$: console.log("new roles", $roles)
setContext("flow", {
nodes,
edges,
autoLayout: async () => {
const layout = dagreLayout({ nodes: $nodes, edges: $edges })
nodes.set(layout.nodes)
edges.set(layout.edges)
},
})
setContext("flow", { nodes, edges })
onMount(() => {
const layout = dagreLayout(rolesToLayout())
const layout = autoLayout(defaultLayout())
nodes.set(layout.nodes)
edges.set(layout.edges)
})
@ -120,15 +83,5 @@
/* Edges */
--xy-edge-stroke: var(--edge-color);
--xy-edge-stroke-selected: var(--selected-color);
/* Minimap */
/* --xy-minimap-background-color-props: var(--node-background);
--xy-minimap-mask-background-color-props: var(--translucent-grey); */
}
/* Arrow edge markers */
/* .flow :global(.svelte-flow__arrowhead > polyline) {
fill: var(--edge-color);
stroke: var(--edge-color);
} */
</style>

View File

@ -12,13 +12,13 @@
import { Roles } from "constants/backend"
import { NodeWidth, NodeHeight } from "./constants"
import { getContext, tick } from "svelte"
import { template } from "lodash"
import { autoLayout } from "./layout"
export let data
export let isConnectable
export let id
const { autoLayout } = getContext("flow")
const { nodes, edges } = getContext("flow")
const flow = useSvelteFlow()
let anchor
@ -33,7 +33,7 @@
nodes: [{ id }],
})
await tick()
autoLayout()
doAutoLayout()
}
const openPopover = () => {
@ -48,6 +48,13 @@
color: tempColor,
})
}
const doAutoLayout = () => {
const layout = autoLayout({ nodes: $nodes, edges: $edges })
nodes.set(layout.nodes)
edges.set(layout.edges)
flow.fitView({ maxZoom: 1, duration: 300 })
}
</script>
<div

View File

@ -1,2 +1,3 @@
export const NodeWidth = 220
export const NodeHeight = 66
export const ZoomDuration = 300

View File

@ -6,7 +6,8 @@ import { Roles } from "constants/backend"
import { get } from "svelte/store"
import { Helpers } from "@budibase/bbui"
export const rolesToLayout = () => {
// Generates a flow compatible structure of nodes and edges from the current roles
export const defaultLayout = () => {
const ignoredRoles = [Roles.PUBLIC]
const $roles = get(roles)
const descriptions = {
@ -59,6 +60,7 @@ export const rolesToLayout = () => {
}
}
// Converts the flow structure of ndes and edges back into an array of roles
export const layoutToRoles = ({ nodes, edges }) => {
// Clone and wipe existing inheritance
let newRoles = Helpers.cloneDeep(get(roles)).map(role => {
@ -92,7 +94,8 @@ export const layoutToRoles = ({ nodes, edges }) => {
return newRoles
}
export const dagreLayout = ({ nodes, edges }) => {
// Updates positions of nodes and edges into a nice graph structure
const dagreLayout = ({ nodes, edges }) => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
dagreGraph.setGraph({
@ -118,3 +121,40 @@ export const dagreLayout = ({ nodes, edges }) => {
})
return { nodes, edges }
}
// Adds additional edges as needed to the flow structure to ensure compatibility with BB role logic
const sanitiseLayout = ({ nodes, edges }) => {
let additions = []
for (let node of nodes) {
// If a node does not inherit anything, let it inherit basic
if (!edges.some(x => x.target === node.id) && node.id !== Roles.BASIC) {
additions.push({
id: Helpers.uuid(),
source: Roles.BASIC,
target: node.id,
animated: true,
})
}
// If a node is not inherited by anything, let it be inherited by admin
if (!edges.some(x => x.source === node.id) && node.id !== Roles.ADMIN) {
additions.push({
id: Helpers.uuid(),
source: node.id,
target: Roles.ADMIN,
animated: true,
})
}
}
return {
nodes,
edges: [...edges, ...additions],
}
}
// Automatically lays out the graph, sanitising and enriching the structure
export const autoLayout = ({ nodes, edges }) => {
return dagreLayout(sanitiseLayout({ nodes, edges }))
}

View File

@ -0,0 +1,105 @@
import { db, roles } from "@budibase/backend-core"
import { features } from "@budibase/pro"
import {
DocumentType,
PermissionLevel,
PermissionSource,
PlanType,
VirtualDocumentType,
} from "@budibase/types"
import { extractViewInfoFromID, isViewID } from "../../../db/utils"
import {
CURRENTLY_SUPPORTED_LEVELS,
getBasePermissions,
} from "../../../utilities/security"
import sdk from "../../../sdk"
import { isV2 } from "../views"
type ResourcePermissions = Record<
string,
{ role: string; type: PermissionSource }
>
export async function getInheritablePermissions(
resourceId: string
): Promise<ResourcePermissions | undefined> {
if (isViewID(resourceId)) {
return await getResourcePerms(extractViewInfoFromID(resourceId).tableId)
}
}
export async function getResourcePerms(
resourceId: string
): Promise<ResourcePermissions> {
const rolesList = await roles.getAllRoles()
let permissions: ResourcePermissions = {}
const permsToInherit = await getInheritablePermissions(resourceId)
for (let level of CURRENTLY_SUPPORTED_LEVELS) {
// update the various roleIds in the resource permissions
for (let role of rolesList) {
const rolePerms = roles.checkForRoleResourceArray(
role.permissions || {},
resourceId
)
if (rolePerms[resourceId]?.indexOf(level) > -1) {
permissions[level] = {
role: roles.getExternalRoleID(role._id!, role.version),
type: PermissionSource.EXPLICIT,
}
} else if (
!permissions[level] &&
permsToInherit &&
permsToInherit[level]
) {
permissions[level] = {
role: permsToInherit[level].role,
type: PermissionSource.INHERITED,
}
}
}
}
const basePermissions = Object.entries(
getBasePermissions(resourceId)
).reduce<ResourcePermissions>((p, [level, role]) => {
p[level] = { role, type: PermissionSource.BASE }
return p
}, {})
const result = Object.assign(basePermissions, permissions)
return result
}
export async function getDependantResources(
resourceId: string
): Promise<Record<string, number> | undefined> {
if (db.isTableId(resourceId)) {
const dependants: Record<string, Set<string>> = {}
const table = await sdk.tables.getTable(resourceId)
const views = Object.values(table.views || {})
for (const view of views) {
if (!isV2(view)) {
continue
}
const permissions = await getResourcePerms(view.id)
for (const [, roleInfo] of Object.entries(permissions)) {
if (roleInfo.type === PermissionSource.INHERITED) {
dependants[VirtualDocumentType.VIEW] ??= new Set()
dependants[VirtualDocumentType.VIEW].add(view.id)
}
}
}
return Object.entries(dependants).reduce((p, [type, resources]) => {
p[type] = resources.size
return p
}, {} as Record<string, number>)
}
return
}