Add new access selection UI for tables and views

This commit is contained in:
Andrew Kingston 2024-09-09 17:04:02 +01:00
parent 714d05a9d2
commit 4fd74c3a19
No known key found for this signature in database
7 changed files with 239 additions and 152 deletions

View File

@ -8,6 +8,7 @@
export let onConfirm = undefined
export let buttonText = ""
export let cta = false
$: icon = selectIcon(type)
// if newlines used, convert them to different elements
$: split = message.split("\n")

View File

@ -8,6 +8,7 @@
export let url = null
export let hoverable = false
export let showArrow = false
export let selected = false
</script>
<a
@ -15,6 +16,7 @@
class="list-item"
class:hoverable={hoverable || url != null}
on:click
class:selected
>
<div class="left">
{#if icon}
@ -43,7 +45,7 @@
<style>
.list-item {
padding: var(--spacing-m);
padding: var(--spacing-m) var(--spacing-l);
background: var(--spectrum-global-color-gray-75);
display: flex;
flex-direction: row;
@ -66,8 +68,13 @@
}
.hoverable:hover {
cursor: pointer;
}
.hoverable:not(.selected):hover {
background: var(--spectrum-global-color-gray-200);
}
.selected {
background: var(--spectrum-global-color-blue-100);
}
.left,
.right {

View File

@ -1,31 +1,208 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { permissions } from "stores/builder"
import ManageAccessModal from "../modals/ManageAccessModal.svelte"
import {
ActionButton,
Icon,
Body,
Input,
Select,
Label,
List,
ListItem,
notifications,
} from "@budibase/bbui"
import { permissions as permissionsStore, roles } from "stores/builder"
import DetailPopover from "components/common/DetailPopover.svelte"
import EditRolesButton from "./EditRolesButton.svelte"
import { PermissionSource } from "@budibase/types"
import { capitalise } from "helpers"
import InfoDisplay from "pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
import { Roles } from "constants/backend"
export let resourceId
let resourcePermissions
const inheritedRoleId = "inherited"
const builtins = [Roles.ADMIN, Roles.POWER, Roles.BASIC, Roles.PUBLIC]
let permissions
let showPopover = true
let dependantsInfoMessage
$: fetchPermissions(resourceId)
$: loadDependantInfo(resourceId)
$: roleMismatch = checkRoleMismatch(permissions)
$: selectedRole = roleMismatch ? null : permissions?.[0]?.value
$: readableRole = selectedRole
? $roles.find(x => x._id === selectedRole)?.name
: null
$: buttonLabel = readableRole ? `Access: ${readableRole}` : "Access"
$: customRoles = $roles.filter(x => !builtins.includes(x._id))
$: highlight = roleMismatch || selectedRole === Roles.PUBLIC
const fetchPermissions = async id => {
resourcePermissions = await permissions.forResourceDetailed(id)
const res = await permissionsStore.forResourceDetailed(id)
permissions = Object.entries(res?.permissions || {}).map(([perm, info]) => {
let enriched = {
permission: perm,
value:
info.permissionType === PermissionSource.INHERITED
? inheritedRoleId
: info.role,
options: [...$roles],
}
if (info.inheritablePermission) {
enriched.options.unshift({
_id: inheritedRoleId,
name: `Inherit (${
$roles.find(x => x._id === info.inheritablePermission).name
})`,
})
}
return enriched
})
}
const checkRoleMismatch = permissions => {
if (!permissions || permissions.length < 2) {
return false
}
return (
permissions[0].value !== permissions[1].value ||
permissions[0].value === inheritedRoleId
)
}
const loadDependantInfo = async resourceId => {
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
const resourceByType = dependantsInfo?.resourceByType
if (resourceByType) {
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
let resourceDisplay =
Object.keys(resourceByType).length === 1 && resourceByType.view
? "view"
: "resource"
if (total === 1) {
dependantsInfoMessage = `1 ${resourceDisplay} is inheriting this access`
} else if (total > 1) {
dependantsInfoMessage = `${total} ${resourceDisplay}s are inheriting this access`
} else {
dependantsInfoMessage = null
}
} else {
dependantsInfoMessage = null
}
}
const changePermission = async role => {
try {
await permissionsStore.save({
level: "read",
role,
resource: resourceId,
})
await permissionsStore.save({
level: "write",
role,
resource: resourceId,
})
await fetchPermissions(resourceId)
notifications.success("Updated permissions")
} catch (error) {
console.error(error)
notifications.error("Error updating permissions")
}
}
</script>
<DetailPopover title="Manage access" {showPopover}>
<DetailPopover title="Select access role" {showPopover}>
<svelte:fragment slot="anchor" let:open>
<ActionButton icon="LockClosed" selected={open} quiet>Access</ActionButton>
<ActionButton
icon="LockClosed"
selected={open || highlight}
quiet
accentColor={highlight ? "#ff0000" : null}
>
{buttonLabel}
</ActionButton>
</svelte:fragment>
{#if resourcePermissions}
<ManageAccessModal {resourceId} permissions={resourcePermissions} />
{#if roleMismatch}
<div class="row">
<Label extraSmall grey>Level</Label>
<Label extraSmall grey>Role</Label>
{#each permissions as permission}
<Input value={capitalise(permission.permission)} disabled />
<Select
placeholder={false}
value={permission.value}
on:change={e => changePermission(e.detail)}
disabled
options={permission.options}
getOptionLabel={x => x.name}
getOptionValue={x => x._id}
/>
{/each}
</div>
<InfoDisplay
error
icon="Alert"
body="Your previous configuration is shown above.<br/> Please choose a new role for both read and write access."
/>
{/if}
<List>
<ListItem
title="App admin"
subtitle="Only app admins can access this data"
hoverable
selected={selectedRole === Roles.ADMIN}
on:click={() => changePermission(Roles.ADMIN)}
/>
<ListItem
title="App power user"
subtitle="Only app power users can access this data"
hoverable
selected={selectedRole === Roles.POWER}
on:click={() => changePermission(Roles.POWER)}
/>
<ListItem
title="App user"
subtitle="Only logged in users can access this data"
hoverable
selected={selectedRole === Roles.BASIC}
on:click={() => changePermission(Roles.BASIC)}
/>
<ListItem
title="Public user"
subtitle="Users are not required to log in to access this data"
hoverable
selected={selectedRole === Roles.PUBLIC}
on:click={() => changePermission(Roles.PUBLIC)}
/>
{#each customRoles as role}
<ListItem
title={role.name}
hoverable
selected={selectedRole === role._id}
on:click={() => changePermission(role._id)}
/>
{/each}
</List>
{#if dependantsInfoMessage}
<InfoDisplay body={dependantsInfoMessage} />
{/if}
<EditRolesButton
on:show={() => (showPopover = false)}
on:hide={() => (showPopover = true)}
/>
</DetailPopover>
<style>
.row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-s);
}
</style>

View File

@ -17,7 +17,7 @@
let basePermissions = []
let selectedRole = BASE_ROLE
let errors = []
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
let builtInRoles = ["App admin", "App power user", "App user", "Public user"]
let validRegex = /^[a-zA-Z0-9_]*$/
// Don't allow editing of public role
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
@ -108,6 +108,9 @@
}
const getRoleNameError = name => {
if (builtInRoles.includes(name)) {
return null
}
const hasUniqueRoleName = !otherRoles
?.map(role => role.name)
?.includes(name)

View File

@ -1,127 +0,0 @@
<script>
import { PermissionSource } from "@budibase/types"
import { roles, permissions as permissionsStore } from "stores/builder"
import {
Label,
Input,
Select,
notifications,
Body,
Icon,
} from "@budibase/bbui"
import { capitalise } from "helpers"
import { get } from "svelte/store"
export let resourceId
export let permissions
const inheritedRoleId = "inherited"
let dependantsInfoMessage
$: loadDependantInfo(resourceId)
$: computedPermissions = Object.entries(permissions.permissions).reduce(
(p, [level, roleInfo]) => {
p[level] = {
selectedValue:
roleInfo.permissionType === PermissionSource.INHERITED
? inheritedRoleId
: roleInfo.role,
options: [...$roles],
}
if (roleInfo.inheritablePermission) {
p[level].inheritOption = roleInfo.inheritablePermission
p[level].options.unshift({
_id: inheritedRoleId,
name: `Inherit (${
get(roles).find(x => x._id === roleInfo.inheritablePermission).name
})`,
})
}
return p
},
{}
)
async function changePermission(level, role) {
try {
if (role === inheritedRoleId) {
await permissionsStore.remove({
level,
role,
resource: resourceId,
})
} else {
await permissionsStore.save({
level,
role,
resource: resourceId,
})
}
// Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResourceDetailed(resourceId)
notifications.success("Updated permissions")
} catch (error) {
notifications.error("Error updating permissions")
}
}
async function loadDependantInfo(resourceId) {
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
const resourceByType = dependantsInfo?.resourceByType
if (resourceByType) {
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
let resourceDisplay =
Object.keys(resourceByType).length === 1 && resourceByType.view
? "view"
: "resource"
if (total === 1) {
dependantsInfoMessage = `1 ${resourceDisplay} is inheriting this access.`
} else if (total > 1) {
dependantsInfoMessage = `${total} ${resourceDisplay}s are inheriting this access.`
}
}
}
</script>
<Body size="S">Specify the minimum access level role for this data.</Body>
<div class="row">
<Label extraSmall grey>Level</Label>
<Label extraSmall grey>Role</Label>
{#each Object.keys(computedPermissions) as level}
<Input value={capitalise(level)} disabled />
<Select
placeholder={false}
value={computedPermissions[level].selectedValue}
on:change={e => changePermission(level, e.detail)}
options={computedPermissions[level].options}
getOptionLabel={x => x.name}
getOptionValue={x => x._id}
/>
{/each}
</div>
{#if dependantsInfoMessage}
<div class="inheriting-resources">
<Icon name="Alert" />
<Body size="S">
<i>
{dependantsInfoMessage}
</i>
</Body>
</div>
{/if}
<style>
.row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-s);
}
.inheriting-resources {
display: flex;
gap: var(--spacing-s);
}
</style>

View File

@ -4,9 +4,11 @@
export let title
export let body
export let icon = "HelpOutline"
export let warning = false
export let error = false
</script>
<div class="info" class:noTitle={!title}>
<div class="info" class:noTitle={!title} class:warning class:error>
{#if title}
<div class="title">
<Icon name={icon} />
@ -16,7 +18,7 @@
{@html body}
{:else}
<span class="icon">
<Icon name={icon} />
<Icon size="S" name={icon} />
</span>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html body}
@ -24,6 +26,23 @@
</div>
<style>
.info {
padding: var(--spacing-m) var(--spacing-l);
background-color: var(--spectrum-global-color-gray-200);
border-radius: var(--border-radius-s);
font-size: 13px;
}
.warning {
background: rgba(255, 200, 0, 0.2);
}
.error {
background: rgba(255, 0, 0, 0.2);
}
.noTitle {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.title {
font-size: 12px;
font-weight: 600;
@ -39,17 +58,7 @@
.icon {
color: var(--spectrum-global-color-gray-600);
}
.info {
padding: var(--spacing-m) var(--spacing-l) var(--spacing-l) var(--spacing-l);
background-color: var(--background-alt);
border-radius: var(--border-radius-s);
font-size: 13px;
}
.noTitle {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.info :global(a) {
color: inherit;
transition: color 130ms ease-out;

View File

@ -1,6 +1,14 @@
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([])
@ -17,7 +25,16 @@ export function createRolesStore() {
const actions = {
fetch: async () => {
const roles = await API.getRoles()
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
}
}
setRoles(roles)
},
fetchByAppId: async appId => {