Add display name, color and descriptions to roles. Allow row CRUD via new UI

This commit is contained in:
Andrew Kingston 2024-09-11 15:42:05 +01:00
parent a5520a973c
commit a81d9c6dd1
No known key found for this signature in database
16 changed files with 141 additions and 125 deletions

View File

@ -45,10 +45,22 @@ export class Role implements RoleDoc {
inherits?: string inherits?: string
version?: string version?: string
permissions = {} permissions = {}
displayName?: string
color?: string
description?: string
constructor(id: string, name: string, permissionId: string) { constructor(
id: string,
displayName: string,
description: string,
color: string,
permissionId: string
) {
this._id = id this._id = id
this.name = name this.name = id
this.displayName = displayName
this.color = color
this.description = description
this.permissionId = permissionId this.permissionId = permissionId
// version for managing the ID - removing the role_ when responding // version for managing the ID - removing the role_ when responding
this.version = RoleIDVersion.NAME this.version = RoleIDVersion.NAME
@ -63,21 +75,39 @@ export class Role implements RoleDoc {
const BUILTIN_ROLES = { const BUILTIN_ROLES = {
ADMIN: new Role( ADMIN: new Role(
BUILTIN_IDS.ADMIN, BUILTIN_IDS.ADMIN,
"Admin", "App admin",
"Can do everything",
"var(--spectrum-global-color-static-red-400)",
BuiltinPermissionID.ADMIN BuiltinPermissionID.ADMIN
).addInheritance(BUILTIN_IDS.POWER), ).addInheritance(BUILTIN_IDS.POWER),
POWER: new Role( POWER: new Role(
BUILTIN_IDS.POWER, BUILTIN_IDS.POWER,
"Power", "App power user",
"An app user with more access",
"var(--spectrum-global-color-static-orange-400)",
BuiltinPermissionID.POWER BuiltinPermissionID.POWER
).addInheritance(BUILTIN_IDS.BASIC), ).addInheritance(BUILTIN_IDS.BASIC),
BASIC: new Role( BASIC: new Role(
BUILTIN_IDS.BASIC, BUILTIN_IDS.BASIC,
"Basic", "App user",
"Any logged in user",
"var(--spectrum-global-color-static-green-400)",
BuiltinPermissionID.WRITE BuiltinPermissionID.WRITE
).addInheritance(BUILTIN_IDS.PUBLIC), ).addInheritance(BUILTIN_IDS.PUBLIC),
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public", BuiltinPermissionID.PUBLIC), PUBLIC: new Role(
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder", BuiltinPermissionID.ADMIN), BUILTIN_IDS.PUBLIC,
"Public user",
"Accessible to anyone",
"var(--spectrum-global-color-static-blue-400)",
BuiltinPermissionID.PUBLIC
),
BUILDER: new Role(
BUILTIN_IDS.BUILDER,
"Builder user",
"Users that can edit this app",
"var(--spectrum-global-color-static-magenta-600)",
BuiltinPermissionID.ADMIN
),
} }
export function getBuiltinRoles(): { [key: string]: RoleDoc } { export function getBuiltinRoles(): { [key: string]: RoleDoc } {

View File

@ -2,29 +2,28 @@
import { Button, Helpers, ActionButton } from "@budibase/bbui" import { Button, Helpers, ActionButton } from "@budibase/bbui"
import { useSvelteFlow, Position } from "@xyflow/svelte" import { useSvelteFlow, Position } from "@xyflow/svelte"
import { getContext, tick } from "svelte" import { getContext, tick } from "svelte"
import { autoLayout } from "./layout" import { autoLayout, roleToNode } from "./layout"
import { ZoomDuration } from "./constants" import { ZoomDuration } from "./constants"
import { getSequentialName } from "helpers/duplicate"
import { roles } from "stores/builder"
import { Roles } from "constants/backend"
const { nodes, edges } = getContext("flow") const { nodes, edges } = getContext("flow")
const flow = useSvelteFlow() const flow = useSvelteFlow()
const addRole = async () => { const addRole = async () => {
nodes.update(state => [ const role = {
...state, name: Helpers.uuid(),
{ displayName: getSequentialName($nodes, "New role ", {
id: Helpers.uuid(), getName: x => x.data.displayName,
sourcePosition: Position.Right, }),
targetPosition: Position.Left,
type: "role",
data: {
displayName: "New role",
description: "Custom role",
custom: true,
color: "var(--spectrum-global-color-gray-700)", color: "var(--spectrum-global-color-gray-700)",
}, description: "Custom role",
position: { x: 0, y: 0 }, permissionId: "write",
}, inherits: Roles.BASIC,
]) }
const savedRole = await roles.save(role)
nodes.update(state => [...state, roleToNode(savedRole)])
await doAutoLayout() await doAutoLayout()
} }
@ -36,7 +35,6 @@
flow.fitView({ flow.fitView({
maxZoom: 1, maxZoom: 1,
duration: ZoomDuration, duration: ZoomDuration,
includeHiddenNodes: true,
}) })
} }
</script> </script>

View File

@ -4,7 +4,7 @@
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 { defaultLayout, 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"
@ -14,7 +14,7 @@
setContext("flow", { nodes, edges }) setContext("flow", { nodes, edges })
onMount(() => { onMount(() => {
const layout = autoLayout(defaultLayout()) const layout = autoLayout(rolesToNodes())
nodes.set(layout.nodes) nodes.set(layout.nodes)
edges.set(layout.edges) edges.set(layout.edges)
}) })

View File

@ -12,7 +12,8 @@
import { Roles } from "constants/backend" import { Roles } from "constants/backend"
import { NodeWidth, NodeHeight } from "./constants" import { NodeWidth, NodeHeight } from "./constants"
import { getContext, tick } from "svelte" import { getContext, tick } from "svelte"
import { autoLayout } from "./layout" import { autoLayout, nodeToRole } from "./layout"
import { roles } from "stores/builder"
export let data export let data
export let isConnectable export let isConnectable
@ -27,9 +28,8 @@
let tempDescription let tempDescription
let tempColor let tempColor
$: color = data.color || RoleUtils.getRoleColour(id)
const deleteNode = async () => { const deleteNode = async () => {
await roles.delete(nodeToRole({ id, data }))
flow.deleteElements({ flow.deleteElements({
nodes: [{ id }], nodes: [{ id }],
}) })
@ -40,11 +40,12 @@
const openPopover = () => { const openPopover = () => {
tempDisplayName = data.displayName tempDisplayName = data.displayName
tempDescription = data.description tempDescription = data.description
tempColor = color tempColor = data.color
modal.show() modal.show()
} }
const saveChanges = () => { const saveChanges = async () => {
await roles.save(nodeToRole({ id, data }))
flow.updateNodeData(id, { flow.updateNodeData(id, {
displayName: tempDisplayName, displayName: tempDisplayName,
description: tempDescription, description: tempDescription,
@ -63,7 +64,7 @@
<div <div
class="node" class="node"
class:selected={false} class:selected={false}
style={`--color:${color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`} style={`--color:${data.color}; --width:${NodeWidth}px; --height:${NodeHeight}px;`}
bind:this={anchor} bind:this={anchor}
> >
<div class="color" /> <div class="color" />

View File

@ -6,15 +6,36 @@ import { Roles } from "constants/backend"
import { get } from "svelte/store" import { get } from "svelte/store"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
// Converts a role doc into a node structure
export const roleToNode = role => ({
id: role._id,
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: "role",
position: { x: 0, y: 0 },
data: {
displayName: role.displayName || role.name,
description: role.description || "Custom role",
color: role.color || "var(--spectrum-global-color-static-magenta-400)",
custom: !role._id.match(/[A-Z]+/),
},
})
// Converts a node structure back into a role doc
export const nodeToRole = node => {
const role = get(roles).find(x => x._id === node.id)
return {
...role,
displayName: node.data.displayName,
color: node.data.color,
description: node.data.description,
}
}
// Generates a flow compatible structure of nodes and edges from the current roles // Generates a flow compatible structure of nodes and edges from the current roles
export const defaultLayout = () => { export const rolesToNodes = () => {
const ignoredRoles = [Roles.PUBLIC] const ignoredRoles = [Roles.PUBLIC]
const $roles = get(roles) const $roles = get(roles)
const descriptions = {
[Roles.BASIC]: "Basic user",
[Roles.POWER]: "Power user",
[Roles.ADMIN]: "Can do everything",
}
let nodes = [] let nodes = []
let edges = [] let edges = []
@ -23,19 +44,11 @@ export const defaultLayout = () => {
if (ignoredRoles.includes(role._id)) { if (ignoredRoles.includes(role._id)) {
continue continue
} }
nodes.push({
id: role._id,
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: "role",
data: {
displayName: role.displayName || role.name || "",
description: descriptions[role._id] || "Custom role",
color: role.color,
custom: !role._id.match(/[A-Z]+/),
},
})
// Add node for this role
nodes.push(roleToNode(role))
// Add edges for this role
let inherits = [] let inherits = []
if (role.inherits) { if (role.inherits) {
inherits = Array.isArray(role.inherits) ? role.inherits : [role.inherits] inherits = Array.isArray(role.inherits) ? role.inherits : [role.inherits]
@ -60,40 +73,6 @@ export const defaultLayout = () => {
} }
} }
// 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 => {
return { ...role, inherits: [] }
})
// Copy over names and colours
for (let node of nodes) {
let role = newRoles.find(x => x._id === node.id)
if (role) {
role.name = node.data.label
role.color = node.data.color
} else {
// New role
}
}
// Build inheritance
for (let edge of edges) {
let role = newRoles.find(x => x._id === edge.target)
if (role) {
role.inherits.push(edge.source)
} else {
// New role
}
}
// Ensure basic is correct
newRoles.find(x => x._id === Roles.BASIC).inherits = [Roles.BASIC]
return newRoles
}
// Updates positions of nodes and edges into a nice graph structure // Updates positions of nodes and edges into a nice graph structure
const dagreLayout = ({ nodes, edges }) => { const dagreLayout = ({ nodes, edges }) => {
const dagreGraph = new dagre.graphlib.Graph() const dagreGraph = new dagre.graphlib.Graph()

View File

@ -1,12 +1,15 @@
<script> <script>
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { StatusLight } from "@budibase/bbui" import { StatusLight } from "@budibase/bbui"
import { roles } from "stores/builder"
export let id export let id
export let size = "M" export let size = "M"
export let disabled = false export let disabled = false
$: color = RoleUtils.getRoleColour(id) $: color =
$roles.find(x => x._id === id)?.color ||
"var(--spectrum-global-color-static-magenta-400)"
</script> </script>
<StatusLight square {disabled} {size} {color} /> <StatusLight square {disabled} {size} {color} />

View File

@ -99,7 +99,7 @@
if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) { if (role._id === Constants.Roles.CREATOR || role._id === RemoveID) {
return null return null
} }
return RoleUtils.getRoleColour(role._id) return role.color || "var(--spectrum-global-color-static-magenta-400)"
} }
const getIcon = role => { const getIcon = role => {

View File

@ -14,7 +14,8 @@
options={$roles} options={$roles}
getOptionLabel={role => role.name} getOptionLabel={role => role.name}
getOptionValue={role => role._id} getOptionValue={role => role._id}
getOptionColour={role => RoleUtils.getRoleColour(role._id)} getOptionColour={role =>
role.color || "var(--spectrum-global-color-static-magenta-400)"}
{placeholder} {placeholder}
{error} {error}
/> />

View File

@ -8,7 +8,7 @@
let showTooltip = false let showTooltip = false
$: color = RoleUtils.getRoleColour(roleId) $: color = role.color || "var(--spectrum-global-color-static-magenta-400)"
$: role = $roles.find(role => role._id === roleId) $: role = $roles.find(role => role._id === roleId)
$: tooltip = $: tooltip =
roleId === Roles.PUBLIC roleId === Roles.PUBLIC

View File

@ -6,8 +6,9 @@
export let value export let value
$: role = $roles.find(x => x._id === roleId)
const getRoleLabel = roleId => { const getRoleLabel = roleId => {
const role = $roles.find(x => x._id === roleId)
return roleId === Constants.Roles.CREATOR return roleId === Constants.Roles.CREATOR
? capitalise(Constants.Roles.CREATOR.toLowerCase()) ? capitalise(Constants.Roles.CREATOR.toLowerCase())
: role?.name || "Custom role" : role?.name || "Custom role"
@ -17,7 +18,10 @@
{#if value === Constants.Roles.CREATOR} {#if value === Constants.Roles.CREATOR}
Can edit Can edit
{:else} {:else}
<StatusLight square color={RoleUtils.getRoleColour(value)}> <StatusLight
square
color={role?.color || "var(--spectrum-global-color-static-magenta-400)"}
>
Can use as {getRoleLabel(value)} Can use as {getRoleLabel(value)}
</StatusLight> </StatusLight>
{/if} {/if}

View File

@ -1,14 +1,6 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { API } from "api" import { API } from "api"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { Roles } from "constants/backend"
const ROLE_NAMES = {
[Roles.ADMIN]: "App admin",
[Roles.POWER]: "App power user",
[Roles.BASIC]: "App user",
[Roles.PUBLIC]: "Public user",
}
export function createRolesStore() { export function createRolesStore() {
const { subscribe, update, set } = writable([]) const { subscribe, update, set } = writable([])
@ -25,16 +17,7 @@ export function createRolesStore() {
const actions = { const actions = {
fetch: async () => { fetch: async () => {
let roles = await API.getRoles() const roles = await API.getRoles()
// Update labels
for (let [roleId, name] of Object.entries(ROLE_NAMES)) {
const idx = roles.findIndex(x => x._id === roleId)
if (idx !== -1) {
roles[idx].name = name
}
}
setRoles(roles) setRoles(roles)
}, },
fetchByAppId: async appId => { fetchByAppId: async appId => {
@ -51,7 +34,13 @@ export function createRolesStore() {
save: async role => { save: async role => {
const savedRole = await API.saveRole(role) const savedRole = await API.saveRole(role)
await actions.fetch() await actions.fetch()
return savedRole
// When saving a role we get back an _id prefixed by role_, but the API does not want this
// in future requests
return {
...savedRole,
_id: savedRole._id.replace("role_", ""),
}
}, },
} }

View File

@ -7,20 +7,7 @@ const RolePriorities = {
[Roles.BASIC]: 2, [Roles.BASIC]: 2,
[Roles.PUBLIC]: 1, [Roles.PUBLIC]: 1,
} }
const RoleColours = {
[Roles.ADMIN]: "var(--spectrum-global-color-static-red-400)",
[Roles.CREATOR]: "var(--spectrum-global-color-static-magenta-600)",
[Roles.POWER]: "var(--spectrum-global-color-static-orange-400)",
[Roles.BASIC]: "var(--spectrum-global-color-static-green-400)",
[Roles.PUBLIC]: "var(--spectrum-global-color-static-blue-400)",
}
export const getRolePriority = role => { export const getRolePriority = role => {
return RolePriorities[role] ?? 0 return RolePriorities[role] ?? 0
} }
export const getRoleColour = roleId => {
return (
RoleColours[roleId] ?? "var(--spectrum-global-color-static-magenta-400)"
)
}

View File

@ -62,7 +62,16 @@ export async function find(ctx: UserCtx<void, FindRoleResponse>) {
export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) { export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
const db = context.getAppDB() const db = context.getAppDB()
let { _id, name, inherits, permissionId, version } = ctx.request.body let {
_id,
name,
displayName,
description,
color,
inherits,
permissionId,
version,
} = ctx.request.body
let isCreate = false let isCreate = false
const isNewVersion = version === roles.RoleIDVersion.NAME const isNewVersion = version === roles.RoleIDVersion.NAME
@ -88,7 +97,13 @@ export async function save(ctx: UserCtx<SaveRoleRequest, SaveRoleResponse>) {
ctx.throw(400, "Cannot change custom role name") ctx.throw(400, "Cannot change custom role name")
} }
const role = new roles.Role(_id, name, permissionId).addInheritance(inherits) const role = new roles.Role(
_id,
displayName || name,
description || "Custom role",
color || "var(--spectrum-global-color-static-magenta-400)",
permissionId
).addInheritance(inherits)
if (ctx.request.body._rev) { if (ctx.request.body._rev) {
role._rev = ctx.request.body._rev role._rev = ctx.request.body._rev
} }

View File

@ -208,6 +208,9 @@ export function roleValidator() {
name: Joi.string() name: Joi.string()
.regex(/^[a-zA-Z0-9_]*$/) .regex(/^[a-zA-Z0-9_]*$/)
.required(), .required(),
displayName: Joi.string().optional(),
color: Joi.string().optional(),
description: Joi.string().optional(),
// this is the base permission ID (for now a built in) // this is the base permission ID (for now a built in)
permissionId: Joi.string() permissionId: Joi.string()
.valid(...Object.values(permissions.BuiltinPermissionID)) .valid(...Object.values(permissions.BuiltinPermissionID))

View File

@ -4,6 +4,9 @@ export interface SaveRoleRequest {
_id?: string _id?: string
_rev?: string _rev?: string
name: string name: string
displayName?: string
color?: string
description?: string
inherits: string inherits: string
permissionId: string permissionId: string
version: string version: string

View File

@ -6,4 +6,7 @@ export interface Role extends Document {
permissions: { [key: string]: string[] } permissions: { [key: string]: string[] }
version?: string version?: string
name: string name: string
displayName?: string
color?: string
description?: string
} }