Add custom RBAC edges with inline deletion icon

This commit is contained in:
Andrew Kingston 2024-09-12 12:23:27 +01:00
parent 35bdc998ca
commit 6f9175168b
No known key found for this signature in database
4 changed files with 139 additions and 10 deletions

View File

@ -0,0 +1,114 @@
<script>
import {
getBezierPath,
BaseEdge,
EdgeLabelRenderer,
useSvelteFlow,
} from "@xyflow/svelte"
import { Icon } from "@budibase/bbui"
import { onMount } from "svelte"
export let sourceX
export let sourceY
export let sourcePosition
export let targetX
export let targetY
export let targetPosition
export let id
const flow = useSvelteFlow()
let edgeHovered = false
let labelHovered = false
$: hovered = edgeHovered || labelHovered
$: edgeClasses = getEdgeClasses(hovered, labelHovered)
$: [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
})
const getEdgeClasses = (hovered, labelHovered) => {
let classes = ""
if (hovered) classes += `hovered `
if (labelHovered) classes += `delete `
return classes
}
const onEdgeMouseOver = e => {
edgeHovered = true
}
const onEdgeMouseOut = e => {
edgeHovered = false
}
const deleteEdge = () => {
flow.deleteElements({
edges: [{ id }],
})
}
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:hovered
on:click={deleteEdge}
on:mouseover={() => (labelHovered = true)}
on:mouseout={() => (labelHovered = false)}
>
<Icon name="Delete" />
</div>
</EdgeLabelRenderer>
<style>
.edge-label {
position: absolute;
padding: 8px;
opacity: 0;
pointer-events: all;
}
.edge-label:hover,
.edge-label.hovered {
opacity: 1;
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);
}
:global(.svelte-flow__edge:hover .svelte-flow__edge-path),
:global(.svelte-flow__edge-path.hovered) {
stroke: var(--spectrum-global-color-blue-400);
}
:global(.svelte-flow__edge-path.hovered.delete) {
stroke: var(--spectrum-global-color-red-400);
}
</style>

View File

@ -4,14 +4,16 @@
import { SvelteFlow, Background, BackgroundVariant } from "@xyflow/svelte" import { SvelteFlow, Background, BackgroundVariant } from "@xyflow/svelte"
import "@xyflow/svelte/dist/style.css" import "@xyflow/svelte/dist/style.css"
import RoleNode from "./RoleNode.svelte" import RoleNode from "./RoleNode.svelte"
import RoleEdge from "./RoleEdge.svelte"
import { rolesToNodes, autoLayout } from "./layout" import { rolesToNodes, autoLayout } from "./layout"
import { onMount, setContext } from "svelte" import { onMount, setContext } from "svelte"
import Controls from "./Controls.svelte" import Controls from "./Controls.svelte"
const nodes = writable([]) const nodes = writable([])
const edges = writable([]) const edges = writable([])
const dragging = writable(false)
setContext("flow", { nodes, edges }) setContext("flow", { nodes, edges, dragging })
onMount(() => { onMount(() => {
const layout = autoLayout(rolesToNodes()) const layout = autoLayout(rolesToNodes())
@ -33,9 +35,12 @@
{edges} {edges}
snapGrid={[25, 25]} snapGrid={[25, 25]}
nodeTypes={{ role: RoleNode }} nodeTypes={{ role: RoleNode }}
edgeTypes={{ role: RoleEdge }}
proOptions={{ hideAttribution: true }} proOptions={{ hideAttribution: true }}
fitViewOptions={{ maxZoom: 1 }} fitViewOptions={{ maxZoom: 1 }}
defaultEdgeOptions={{ type: "bezier", animated: true }} defaultEdgeOptions={{ type: "role", animated: true, selectable: false }}
onconnectstart={() => dragging.set(true)}
onconnectend={() => dragging.set(false)}
> >
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<Controls /> <Controls />
@ -81,6 +86,6 @@
/* Edges */ /* Edges */
--xy-edge-stroke: var(--edge-color); --xy-edge-stroke: var(--edge-color);
--xy-edge-stroke-selected: var(--selected-color); --xy-edge-stroke-selected: var(--edge-color);
} }
</style> </style>

View File

@ -16,10 +16,9 @@
import { roles } from "stores/builder" import { roles } from "stores/builder"
export let data export let data
export let isConnectable
export let id export let id
const { nodes, edges } = getContext("flow") const { nodes, edges, dragging } = getContext("flow")
const flow = useSvelteFlow() const flow = useSvelteFlow()
let anchor let anchor
@ -31,6 +30,7 @@
$: nameError = validateName(tempDisplayName, $roles) $: nameError = validateName(tempDisplayName, $roles)
$: descriptionError = validateDescription(tempDescription) $: descriptionError = validateDescription(tempDescription)
$: invalid = nameError || descriptionError $: invalid = nameError || descriptionError
$: targetClasses = `target${$dragging ? "" : " hidden"}`
const validateName = (name, roles) => { const validateName = (name, roles) => {
if (!name?.length) { if (!name?.length) {
@ -109,10 +109,15 @@
{/if} {/if}
</div> </div>
{#if id !== Roles.BASIC} {#if id !== Roles.BASIC}
<Handle type="target" position={Position.Left} {isConnectable} /> <Handle
type="target"
position={Position.Left}
class={targetClasses}
isConnectable={$dragging}
/>
{/if} {/if}
{#if id !== Roles.ADMIN} {#if id !== Roles.ADMIN}
<Handle type="source" position={Position.Right} {isConnectable} /> <Handle type="source" position={Position.Right} />
{/if} {/if}
</div> </div>
@ -207,4 +212,11 @@
.node:hover .buttons { .node:hover .buttons {
display: flex; display: flex;
} }
.node :global(.svelte-flow__handle.target) {
background: var(--background-color);
}
.node :global(.svelte-flow__handle.hidden) {
opacity: 0;
pointer-events: none;
}
</style> </style>

View File

@ -62,7 +62,6 @@ export const rolesToNodes = () => {
id: `${sourceRole}-${role._id}`, id: `${sourceRole}-${role._id}`,
source: sourceRole, source: sourceRole,
target: role._id, target: role._id,
animated: true,
}) })
} }
} }
@ -79,7 +78,7 @@ const dagreLayout = ({ nodes, edges }) => {
dagreGraph.setDefaultEdgeLabel(() => ({})) dagreGraph.setDefaultEdgeLabel(() => ({}))
dagreGraph.setGraph({ dagreGraph.setGraph({
rankdir: "LR", rankdir: "LR",
ranksep: 100, ranksep: 200,
nodesep: 100, nodesep: 100,
}) })
nodes.forEach(node => { nodes.forEach(node => {
@ -122,7 +121,6 @@ const sanitiseLayout = ({ nodes, edges }) => {
id: Helpers.uuid(), id: Helpers.uuid(),
source: node.id, source: node.id,
target: Roles.ADMIN, target: Roles.ADMIN,
animated: true,
}) })
} }
} }