Add new access selection UI for tables and views
This commit is contained in:
parent
714d05a9d2
commit
4fd74c3a19
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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 => {
|
||||
|
|
Loading…
Reference in New Issue