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
version?: string
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.name = name
this.name = id
this.displayName = displayName
this.color = color
this.description = description
this.permissionId = permissionId
// version for managing the ID - removing the role_ when responding
this.version = RoleIDVersion.NAME
@ -63,21 +75,39 @@ export class Role implements RoleDoc {
const BUILTIN_ROLES = {
ADMIN: new Role(
BUILTIN_IDS.ADMIN,
"Admin",
"App admin",
"Can do everything",
"var(--spectrum-global-color-static-red-400)",
BuiltinPermissionID.ADMIN
).addInheritance(BUILTIN_IDS.POWER),
POWER: new Role(
BUILTIN_IDS.POWER,
"Power",
"App power user",
"An app user with more access",
"var(--spectrum-global-color-static-orange-400)",
BuiltinPermissionID.POWER
).addInheritance(BUILTIN_IDS.BASIC),
BASIC: new Role(
BUILTIN_IDS.BASIC,
"Basic",
"App user",
"Any logged in user",
"var(--spectrum-global-color-static-green-400)",
BuiltinPermissionID.WRITE
).addInheritance(BUILTIN_IDS.PUBLIC),
PUBLIC: new Role(BUILTIN_IDS.PUBLIC, "Public", BuiltinPermissionID.PUBLIC),
BUILDER: new Role(BUILTIN_IDS.BUILDER, "Builder", BuiltinPermissionID.ADMIN),
PUBLIC: new Role(
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 } {

View File

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

View File

@ -4,7 +4,7 @@
import { SvelteFlow, Background, BackgroundVariant } from "@xyflow/svelte"
import "@xyflow/svelte/dist/style.css"
import RoleNode from "./RoleNode.svelte"
import { defaultLayout, autoLayout } from "./layout"
import { rolesToNodes, autoLayout } from "./layout"
import { onMount, setContext } from "svelte"
import Controls from "./Controls.svelte"
@ -14,7 +14,7 @@
setContext("flow", { nodes, edges })
onMount(() => {
const layout = autoLayout(defaultLayout())
const layout = autoLayout(rolesToNodes())
nodes.set(layout.nodes)
edges.set(layout.edges)
})

View File

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

View File

@ -6,15 +6,36 @@ import { Roles } from "constants/backend"
import { get } from "svelte/store"
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
export const defaultLayout = () => {
export const rolesToNodes = () => {
const ignoredRoles = [Roles.PUBLIC]
const $roles = get(roles)
const descriptions = {
[Roles.BASIC]: "Basic user",
[Roles.POWER]: "Power user",
[Roles.ADMIN]: "Can do everything",
}
let nodes = []
let edges = []
@ -23,19 +44,11 @@ export const defaultLayout = () => {
if (ignoredRoles.includes(role._id)) {
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 = []
if (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
const dagreLayout = ({ nodes, edges }) => {
const dagreGraph = new dagre.graphlib.Graph()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,6 @@
import { writable } from "svelte/store"
import { API } from "api"
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() {
const { subscribe, update, set } = writable([])
@ -25,16 +17,7 @@ export function createRolesStore() {
const actions = {
fetch: async () => {
let 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
}
}
const roles = await API.getRoles()
setRoles(roles)
},
fetchByAppId: async appId => {
@ -51,7 +34,13 @@ export function createRolesStore() {
save: async role => {
const savedRole = await API.saveRole(role)
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.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 => {
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>) {
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
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")
}
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) {
role._rev = ctx.request.body._rev
}

View File

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

View File

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

View File

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