Add initial version of new RBAC editor
This commit is contained in:
parent
9da84b19f9
commit
3c158c5357
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
export const NodeWidth = 220
|
||||
export const NodeHeight = 66
|
|
@ -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 }
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import RoleEditor from "components/backend/RoleEditor/RoleEditor.svelte"
|
||||
</script>
|
||||
|
||||
<RoleEditor />
|
Loading…
Reference in New Issue