Rewrite app overview access tab
This commit is contained in:
parent
2506d018f9
commit
7ca61c8bca
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue