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/state": "^6.2.0",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@codemirror/view": "^6.11.2",
|
"@codemirror/view": "^6.11.2",
|
||||||
|
"@dagrejs/dagre": "1.1.4",
|
||||||
"@fontsource/source-sans-pro": "^5.0.3",
|
"@fontsource/source-sans-pro": "^5.0.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
"@xyflow/svelte": "^0.1.18",
|
||||||
"@zerodevx/svelte-json-view": "^1.0.7",
|
"@zerodevx/svelte-json-view": "^1.0.7",
|
||||||
"codemirror": "^5.65.16",
|
"codemirror": "^5.65.16",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||||
import { Roles } from "constants/backend"
|
import { Roles } from "constants/backend"
|
||||||
import RoleEditor from "./RoleEditor.svelte"
|
|
||||||
|
|
||||||
export let resourceId
|
export let resourceId
|
||||||
|
|
||||||
|
@ -197,10 +196,6 @@
|
||||||
on:show={() => (showPopover = false)}
|
on:show={() => (showPopover = false)}
|
||||||
on:hide={() => (showPopover = true)}
|
on:hide={() => (showPopover = true)}
|
||||||
/>
|
/>
|
||||||
<RoleEditor
|
|
||||||
on:drawerShow={() => (showPopover = false)}
|
|
||||||
on:drawerHide={() => (showPopover = true)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</DetailPopover>
|
</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]}
|
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
<NavItem
|
||||||
|
icon="UserAdmin"
|
||||||
|
text="Manage roles"
|
||||||
|
selected={$isActive("./roles")}
|
||||||
|
on:click={() => $goto("./roles")}
|
||||||
|
/>
|
||||||
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
|
{#each enrichedDataSources.filter(ds => ds.show) as datasource}
|
||||||
<DatasourceNavItem
|
<DatasourceNavItem
|
||||||
{datasource}
|
{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