Rewrite app overview access tab

This commit is contained in:
Andrew Kingston 2022-12-20 17:01:50 +00:00
parent 2506d018f9
commit 7ca61c8bca
8 changed files with 354 additions and 214 deletions

View File

@ -88,8 +88,8 @@
on:click={onClick}
>
{#if fieldIcon}
<span class="option-extra">
<Icon name={fieldIcon} />
<span class="option-extra icon">
<Icon size="S" name={fieldIcon} />
</span>
{/if}
{#if fieldColour}
@ -168,8 +168,8 @@
class:is-disabled={!isOptionEnabled(option)}
>
{#if getOptionIcon(option, idx)}
<span class="option-extra">
<Icon name={getOptionIcon(option, idx)} />
<span class="option-extra icon">
<Icon size="S" name={getOptionIcon(option, idx)} />
</span>
{/if}
{#if getOptionColour(option, idx)}
@ -241,6 +241,9 @@
.option-extra {
padding-right: 8px;
}
.option-extra.icon {
margin: 0 -1px;
}
.spectrum-Popover :global(.spectrum-Search) {
margin-top: -1px;

View File

@ -21,6 +21,8 @@
* template: a HBS or JS binding to use as the value
* background: the background color
* color: the text color
* borderLeft: show a left border
* borderRight: show a right border
*/
export let data = []
export let schema = {}
@ -270,6 +272,14 @@
if (schema[field].align === "Right") {
styles[field] += "justify-content: flex-end; text-align: right;"
}
if (schema[field].borderLeft) {
styles[field] +=
"border-left: 1px solid var(--spectrum-global-color-gray-200);"
}
if (schema[field].borderLeft) {
styles[field] +=
"border-right: 1px solid var(--spectrum-global-color-gray-200);"
}
})
return styles
}

View File

@ -2,6 +2,7 @@
import { Select } from "@budibase/bbui"
import { roles } from "stores/backend"
import { Constants, RoleUtils } from "@budibase/frontend-core"
import { createEventDispatcher } from "svelte"
export let value
export let error
@ -9,26 +10,62 @@
export let autoWidth = false
export let quiet = false
export let allowPublic = true
export let allowRemove = false
$: options = getOptions($roles, allowPublic)
const dispatch = createEventDispatcher()
const RemoveID = "remove"
$: options = getOptions($roles, allowPublic, allowRemove)
const getOptions = (roles, allowPublic) => {
if (allowRemove) {
roles = [
...roles,
{
_id: RemoveID,
name: "Remove",
},
]
}
if (allowPublic) {
return roles
}
return roles.filter(role => role._id !== Constants.Roles.PUBLIC)
}
const getColor = role => {
if (allowRemove && role._id === RemoveID) {
return null
}
return RoleUtils.getRoleColour(role._id)
}
const getIcon = role => {
if (allowRemove && role._id === RemoveID) {
return "Close"
}
return null
}
const onChange = e => {
if (allowRemove && e.detail === RemoveID) {
dispatch("remove")
} else {
dispatch("change", e.detail)
}
}
</script>
<Select
{autoWidth}
{quiet}
bind:value
on:change
on:change={onChange}
{options}
getOptionLabel={role => role.name}
getOptionValue={role => role._id}
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
getOptionColour={getColor}
getOptionIcon={getIcon}
{placeholder}
{error}
/>

View File

@ -1,207 +0,0 @@
<script>
import {
Layout,
Heading,
Body,
Button,
List,
ListItem,
Modal,
notifications,
Pagination,
Icon,
} from "@budibase/bbui"
import { onMount } from "svelte"
import RoleSelect from "components/common/RoleSelect.svelte"
import { users, groups, apps, licensing, overview } from "stores/portal"
import AssignmentModal from "./_components/AssignmentModal.svelte"
import { roles } from "stores/backend"
import { API } from "api"
import { fetchData } from "@budibase/frontend-core"
let assignmentModal
let appGroups
let appUsers
$: app = $overview.selectedApp
$: devAppId = app.devId
$: prodAppId = apps.getProdAppID(app.devId)
$: usersFetch = fetchData({
API,
datasource: {
type: "user",
},
options: {
query: {
appId: apps.getProdAppID(devAppId),
},
},
})
$: appUsers = $usersFetch.rows
$: appGroups = $groups.filter(group => {
if (!group.roles) {
return false
}
return groups.actions.getGroupAppIds(group).includes(prodAppId)
})
async function removeUser(user) {
// Remove the user role
const filteredRoles = { ...user.roles }
delete filteredRoles[prodAppId]
await users.save({
...user,
roles: {
...filteredRoles,
},
})
await usersFetch.refresh()
}
async function removeGroup(group) {
await groups.actions.removeApp(group._id, prodAppId)
await groups.actions.init()
await usersFetch.refresh()
}
async function updateUserRole(role, user) {
user.roles[prodAppId] = role
await users.save(user)
}
async function updateGroupRole(role, group) {
await groups.actions.addApp(group._id, prodAppId, role)
await usersFetch.refresh()
}
onMount(async () => {
try {
await roles.fetch()
} catch (error) {
notifications.error(error)
}
})
</script>
<Layout noPadding>
{#if appGroups.length || appUsers.length}
<div>
<Heading>Access</Heading>
<div class="subtitle">
<Body size="S">
Assign users and groups to your app and define their access here
</Body>
<Button on:click={assignmentModal.show} icon="User" cta>
Assign access
</Button>
</div>
</div>
{#if $licensing.groupsEnabled && appGroups.length}
<List title="User Groups">
{#each appGroups as group}
<ListItem
title={group.name}
icon={group.icon}
iconBackground={group.color}
>
<RoleSelect
on:change={e => updateGroupRole(e.detail, group)}
autoWidth
quiet
value={group.roles[
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
]}
allowPublic={false}
/>
<Icon
on:click={() => removeGroup(group)}
hoverable
size="S"
name="Close"
/>
</ListItem>
{/each}
</List>
{/if}
{#if appUsers.length}
<div>
<List title="Users">
{#each appUsers as user}
<ListItem title={user.email} avatar>
<RoleSelect
on:change={e => updateUserRole(e.detail, user)}
autoWidth
quiet
value={user.roles[
Object.keys(user.roles).find(x => x === prodAppId)
]}
allowPublic={false}
/>
<Icon
on:click={() => removeUser(user)}
hoverable
size="S"
name="Close"
/>
</ListItem>
{/each}
</List>
<div class="pagination">
<Pagination
page={$usersFetch.pageNumber + 1}
hasPrevPage={$usersFetch.hasPrevPage}
hasNextPage={$usersFetch.hasNextPage}
goToPrevPage={$usersFetch.loading ? null : usersFetch.prevPage}
goToNextPage={$usersFetch.loading ? null : usersFetch.nextPage}
/>
</div>
</div>
{/if}
{:else}
<div class="align">
<Layout gap="S">
<Heading>No users assigned</Heading>
<div class="opacity">
<Body size="S">
Assign users/groups to your app and set their access here
</Body>
</div>
<div class="padding">
<Button on:click={() => assignmentModal.show()} cta icon="UserArrow">
Assign access
</Button>
</div>
</Layout>
</div>
{/if}
</Layout>
<Modal bind:this={assignmentModal}>
<AssignmentModal {app} {appUsers} on:update={usersFetch.refresh} />
</Modal>
<style>
.padding {
margin-top: var(--spacing-m);
}
.opacity {
opacity: 0.8;
}
.align {
text-align: center;
}
.subtitle {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.pagination {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: var(--spacing-xl);
}
</style>

View File

@ -0,0 +1,26 @@
<script>
import RoleSelect from "components/common/RoleSelect.svelte"
import { getContext } from "svelte"
const rolesContext = getContext("roles")
export let value
export let row
</script>
<div>
<RoleSelect
{value}
quiet
allowRemove
allowPublic={false}
on:change={e => rolesContext.updateGroupRole(e.detail, row._id)}
on:remove={() => rolesContext.removeGroupRole(row._id)}
/>
</div>
<style>
div {
width: 100%;
}
</style>

View File

@ -0,0 +1,26 @@
<script>
import RoleSelect from "components/common/RoleSelect.svelte"
import { getContext } from "svelte"
const rolesContext = getContext("roles")
export let value
export let row
</script>
<div>
<RoleSelect
{value}
quiet
allowRemove
allowPublic={false}
on:change={e => rolesContext.updateUserRole(e.detail, row._id)}
on:remove={() => rolesContext.removeUserRole(row._id)}
/>
</div>
<style>
div {
width: 100%;
}
</style>

View File

@ -0,0 +1,245 @@
<script>
import {
Layout,
Heading,
Body,
Button,
List,
ListItem,
Modal,
notifications,
Pagination,
Divider,
Icon,
Table,
} from "@budibase/bbui"
import { onMount, setContext } from "svelte"
import RoleSelect from "components/common/RoleSelect.svelte"
import { users, groups, apps, licensing, overview } from "stores/portal"
import AssignmentModal from "./_components/AssignmentModal.svelte"
import { roles } from "stores/backend"
import { API } from "api"
import { fetchData } from "@budibase/frontend-core"
import UserRoleRenderer from "./_components/UserRoleRenderer.svelte"
import GroupRoleRenderer from "./_components/GroupRoleRenderer.svelte"
const userSchema = {
email: {
type: "string",
},
userAppRole: {
displayName: "Access",
width: "150px",
borderLeft: true,
},
}
const groupSchema = {
name: {
type: "string",
},
groupAppRole: {
displayName: "Access",
width: "150px",
borderLeft: true,
},
}
const customRenderers = [
{
column: "userAppRole",
component: UserRoleRenderer,
},
{
column: "groupAppRole",
component: GroupRoleRenderer,
},
]
let assignmentModal
let appGroups
let appUsers
$: app = $overview.selectedApp
$: devAppId = app.devId
$: prodAppId = apps.getProdAppID(app.devId)
$: usersFetch = fetchData({
API,
datasource: {
type: "user",
},
options: {
query: {
appId: apps.getProdAppID(devAppId),
},
},
})
$: appUsers = getAppUsers($usersFetch.rows, prodAppId)
$: appGroups = getAppGroups($groups, prodAppId)
const getAppUsers = (users, appId) => {
return users.map(user => ({
...user,
userAppRole: user.roles[Object.keys(user.roles).find(x => x === appId)],
}))
}
const getAppGroups = (allGroups, appId) => {
return allGroups
.filter(group => {
if (!group.roles) {
return false
}
return groups.actions.getGroupAppIds(group).includes(appId)
})
.map(group => ({
...group,
groupAppRole:
group.roles[
groups.actions.getGroupAppIds(group).find(x => x === appId)
],
}))
}
const updateUserRole = async (role, userId) => {
const user = $usersFetch.rows.find(user => user._id === userId)
if (!user) {
return
}
user.roles[prodAppId] = role
await users.save(user)
await usersFetch.refresh()
}
const removeUserRole = async userId => {
const user = $usersFetch.rows.find(user => user._id === userId)
if (!user) {
return
}
const filteredRoles = { ...user.roles }
delete filteredRoles[prodAppId]
await users.save({
...user,
roles: {
...filteredRoles,
},
})
await usersFetch.refresh()
}
const updateGroupRole = async (role, groupId) => {
const group = $groups.find(group => group._id === groupId)
if (!group) {
return
}
await groups.actions.addApp(group._id, prodAppId, role)
await usersFetch.refresh()
}
const removeGroupRole = async groupId => {
const group = $groups.find(group => group._id === groupId)
if (!group) {
return
}
await groups.actions.removeApp(group._id, prodAppId)
await usersFetch.refresh()
}
setContext("roles", {
updateUserRole,
removeUserRole,
updateGroupRole,
removeGroupRole,
})
onMount(async () => {
try {
await roles.fetch()
} catch (error) {
notifications.error(error)
}
})
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading>Access</Heading>
<Body>Assign users to your app and set their access</Body>
</Layout>
<Divider />
<Layout noPadding gap="L">
<Layout noPadding gap="S">
<div class="title">
<Heading size="S">Users</Heading>
<Button secondary on:click={assignmentModal.show}>Assign user</Button>
</div>
<Table
customPlaceholder
data={appUsers}
schema={userSchema}
allowEditRows={false}
{customRenderers}
>
<div class="placeholder" slot="placeholder">
<Heading size="S">You have no users assigned yet</Heading>
</div>
</Table>
{#if $usersFetch.hasPrevPage || $usersFetch.hasNextPage}
<div class="pagination">
<Pagination
page={$usersFetch.pageNumber + 1}
hasPrevPage={$usersFetch.hasPrevPage}
hasNextPage={$usersFetch.hasNextPage}
goToPrevPage={$usersFetch.loading ? null : usersFetch.prevPage}
goToNextPage={$usersFetch.loading ? null : usersFetch.nextPage}
/>
</div>
{/if}
</Layout>
{#if $licensing.groupsEnabled && appGroups.length}
<Layout noPadding gap="S">
<div class="title">
<Heading size="S">Groups</Heading>
<Button secondary on:click={assignmentModal.show}>
Assign group
</Button>
</div>
<Table
customPlaceholder
data={appGroups}
schema={groupSchema}
allowEditRows={false}
{customRenderers}
>
<div class="placeholder" slot="placeholder">
<Heading size="S">You have no groups assigned yet</Heading>
</div>
</Table>
</Layout>
{/if}
</Layout>
</Layout>
<Modal bind:this={assignmentModal}>
<AssignmentModal {app} {appUsers} on:update={usersFetch.refresh} />
</Modal>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
}
.placeholder {
flex: 1 1 auto;
display: grid;
place-items: center;
text-align: center;
}
.pagination {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: calc(-1 * var(--spacing-s));
}
</style>