Add initial version of new RBAC editor

This commit is contained in:
Andrew Kingston 2024-09-10 13:30:49 +01:00
parent 9da84b19f9
commit 3c158c5357
No known key found for this signature in database
10 changed files with 447 additions and 37 deletions

View File

@ -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",
"dayjs": "^1.10.8",

View File

@ -15,7 +15,6 @@
import { capitalise } from "helpers"
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
import { Roles } from "constants/backend"
import RoleEditor from "./RoleEditor.svelte"
export let resourceId
@ -197,10 +196,6 @@
on:show={() => (showPopover = false)}
on:hide={() => (showPopover = true)}
/>
<RoleEditor
on:drawerShow={() => (showPopover = false)}
on:drawerHide={() => (showPopover = true)}
/>
</div>
</DetailPopover>

View File

@ -1,32 +0,0 @@
<script>
import {
Button,
Modal,
ModalContent,
Drawer,
DrawerContent,
} from "@budibase/bbui"
let drawer
$: invalid = false
const save = async () => {
drawer.hide()
}
</script>
<Button secondary icon="UsersLock" on:click on:click={drawer.show}>
Role editor
</Button>
<Drawer
bind:this={drawer}
title="Role editor"
on:drawerHide
on:drawerShow
forceModal
>
<Button disabled={invalid} cta slot="buttons" on:click={save}>Save</Button>
<DrawerContent slot="body">asdasdasd</DrawerContent>
</Drawer>

View File

@ -76,6 +76,12 @@
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
/>
{/if}
<NavItem
icon="UserAdmin"
text="Manage roles"
selected={$isActive("./roles")}
on:click={() => $goto("./roles")}
/>
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
<DatasourceNavItem
{datasource}

View File

@ -0,0 +1,95 @@
<script>
import { Button, Helpers, ActionButton } from "@budibase/bbui"
import { useSvelteFlow, Position } from "@xyflow/svelte"
import { getContext } from "svelte"
import { dagreLayout } from "./layout"
const { nodes, edges } = getContext("flow")
const flow = useSvelteFlow()
const addRole = () => {
nodes.update(state => [
...state,
{
id: Helpers.uuid(),
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: "role",
data: {
label: "New role",
description: "Custom role",
custom: true,
},
position: { x: 0, y: 0 },
},
])
autoLayout()
}
const autoLayout = () => {
const layout = dagreLayout({ nodes: $nodes, edges: $edges })
nodes.set(layout.nodes)
edges.set(layout.edges)
flow.fitView({ maxZoom: 1 })
}
</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} />
</div>
<Button secondary on:click={() => flow.fitView({ maxZoom: 1 })}>
Zoom to fit
</Button>
<Button secondary on:click={autoLayout}>Auto layout</Button>
</div>
<div class="control bottom-right">
<Button icon="Add" cta on:click={addRole}>Add role</Button>
</div>
<style>
.control {
position: absolute;
z-index: 999;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.top-left {
top: 20px;
left: 20px;
}
.bottom-right {
bottom: 20px;
right: 20px;
}
.top-left :global(.spectrum-Button),
.top-left :global(.spectrum-ActionButton),
.top-left :global(.spectrum-Icon) {
color: var(--spectrum-global-color-gray-900) !important;
}
.top-left :global(.spectrum-Button),
.top-left :global(.spectrum-ActionButton) {
background: var(--spectrum-global-color-gray-200) !important;
}
.top-left :global(.spectrum-Button:hover),
.top-left :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>

View File

@ -0,0 +1,97 @@
<script>
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 { initialLayout, dagreLayout } from "./layout"
import { onMount, setContext } from "svelte"
import Controls from "./Controls.svelte"
const nodes = writable([])
const edges = writable([])
setContext("flow", { nodes, edges })
onMount(() => {
const layout = dagreLayout(initialLayout())
nodes.set(layout.nodes)
edges.set(layout.edges)
})
</script>
<div class="title">
<div class="heading">
<Heading size="S">Manage roles</Heading>
</div>
<div class="description">Roles inherit permissions from each other.</div>
</div>
<div class="flow">
<SvelteFlow
fitView
{nodes}
{edges}
snapGrid={[25, 25]}
nodeTypes={{ role: RoleNode }}
proOptions={{ hideAttribution: true }}
fitViewOptions={{ maxZoom: 1 }}
defaultEdgeOptions={{ type: "bezier", animated: true }}
on:nodeclick={event => console.log("on node click", event.detail.node)}
>
<Background variant={BackgroundVariant.Dots} />
<Controls />
</SvelteFlow>
</div>
<style>
.heading {
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
margin-bottom: 20px;
padding-bottom: 12px;
}
.description {
color: var(--spectrum-global-color-gray-600);
margin-bottom: calc(20px - var(--spacing-l));
}
.flow {
flex: 1 1 auto;
border-radius: 8px;
overflow: hidden;
position: relative;
--background-color: var(--spectrum-global-color-gray-50);
--node-background: var(--spectrum-global-color-gray-100);
--node-background-hover: var(--spectrum-global-color-gray-300);
--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);
}
.flow :global(.svelte-flow) {
/* Panel */
--xy-background-color: var(--background-color);
--xy-background-pattern-color-props: transparent;
/* Controls */
--xy-controls-button-background-color: var(--node-background);
--xy-controls-button-background-color-hover: var(--node-background-hover);
--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(--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

@ -0,0 +1,120 @@
<script>
import { RoleUtils } from "@budibase/frontend-core"
import { Handle, Position, useSvelteFlow } from "@xyflow/svelte"
import { Icon } from "@budibase/bbui"
import { Roles } from "constants/backend"
import { NodeWidth, NodeHeight } from "./constants"
export let data
export let isConnectable
export let id
const flow = useSvelteFlow()
const { label, description, custom } = data
$: color = RoleUtils.getRoleColour(id)
const deleteNode = () => {
flow.deleteElements({
nodes: [{ id }],
})
}
</script>
<div
class="node"
class:selected={false}
style={`--color:${color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`}
>
<div class="color" />
<div class="content">
<div class="title">
<div class="label">
{label}
</div>
{#if custom}
<div class="buttons">
<Icon size="S" name="Edit" hoverable />
<Icon size="S" name="Delete" hoverable on:click={deleteNode} />
</div>
{/if}
</div>
{#if description}
<div class="description">
{description}
</div>
{/if}
</div>
{#if id !== Roles.BASIC}
<Handle type="target" position={Position.Left} {isConnectable} />
{/if}
{#if id !== Roles.ADMIN}
<Handle type="source" position={Position.Right} {isConnectable} />
{/if}
</div>
<style>
.node {
position: relative;
background: var(--node-background);
border-radius: 4px;
border: 1px solid transparent;
width: var(--width);
height: var(--height);
display: flex;
flex-direction: column;
}
.node.selected {
background: var(--spectrum-global-color-blue-100);
}
.color {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
height: 8px;
width: 100%;
background: var(--color);
}
.content {
flex: 1 1 auto;
padding: 0 14px 0 14px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
gap: 2px;
border: 1px solid var(--border-color);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.node.selected .content {
border-color: transparent;
}
.title,
.buttons {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
}
.title {
justify-content: space-between;
}
.buttons {
display: none;
}
.title :global(.spectrum-Icon) {
color: var(--spectrum-global-color-gray-600);
}
.label {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.description {
color: var(--spectrum-global-color-gray-600);
font-size: 12px;
}
.node:hover .buttons {
display: flex;
}
</style>

View File

@ -0,0 +1,2 @@
export const NodeWidth = 220
export const NodeHeight = 66

View File

@ -0,0 +1,120 @@
import dagre from "@dagrejs/dagre"
import { NodeWidth, NodeHeight } from "./constants"
import { Position } from "@xyflow/svelte"
import { roles } from "stores/builder"
import { Roles } from "constants/backend"
import { get } from "svelte/store"
export const initialLayout = () => {
const builtins = [Roles.BASIC, Roles.POWER, Roles.ADMIN]
const descriptions = {
[Roles.BASIC]: "Basic user",
[Roles.POWER]: "Power user",
[Roles.ADMIN]: "Can do everything",
}
const $roles = get(roles)
const nodes = builtins
.map(roleId => {
return {
id: roleId,
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: "role",
data: {
label: $roles.find(x => x._id === roleId)?.name,
description: descriptions[roleId],
},
}
})
.concat([
{
id: "management",
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: "role",
data: {
label: "Management",
description: "Custom role",
custom: true,
},
},
{
id: "approver",
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: "role",
data: {
label: "Approver",
description: "Custom role",
custom: true,
},
},
{
id: "engineer",
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: "role",
data: {
label: "Engineer",
description: "Custom role",
custom: true,
},
},
])
let edges = []
const link = (source, target) => {
edges.push({
id: `${source}-${target}`,
source,
target,
animated: true,
// markerEnd: {
// type: MarkerType.ArrowClosed,
// width: 16,
// height: 16,
// },
})
}
link(Roles.BASIC, "engineer")
link(Roles.BASIC, "approver")
link("engineer", Roles.POWER)
link("approver", "management")
link(Roles.POWER, Roles.ADMIN)
link("management", Roles.ADMIN)
return {
nodes,
edges,
}
}
export const dagreLayout = ({ nodes, edges }) => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
dagreGraph.setGraph({
rankdir: "LR",
ranksep: 100,
nodesep: 100,
})
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 nodeWithPosition = dagreGraph.node(node.id)
node.targetPosition = Position.Left
node.sourcePosition = Position.Right
node.position = {
x: nodeWithPosition.x - NodeWidth / 2,
y: nodeWithPosition.y - NodeHeight / 2,
}
})
return { nodes, edges }
}

View File

@ -0,0 +1,5 @@
<script>
import RoleEditor from "components/backend/RoleEditor/RoleEditor.svelte"
</script>
<RoleEditor />