updated store and finished groups tab

This commit is contained in:
Peter Clement 2022-06-22 13:55:31 +01:00
parent 4ab7e8cd11
commit 8d264fe983
17 changed files with 425 additions and 207 deletions

View File

@ -15,7 +15,7 @@ exports.DocumentTypes = {
ROLE: "role", ROLE: "role",
MIGRATIONS: "migrations", MIGRATIONS: "migrations",
DEV_INFO: "devinfo", DEV_INFO: "devinfo",
GROUP: "gr" GROUP: "gr",
} }
exports.StaticDatabases = { exports.StaticDatabases = {

View File

@ -170,7 +170,6 @@ exports.getUserGroupsParams = (groupId, otherProps = {}) => {
} }
} }
/** /**
* Generates a new role ID. * Generates a new role ID.
* @returns {string} The new role ID which the role doc can be stored under. * @returns {string} The new role ID which the role doc can be stored under.

View File

@ -4,6 +4,7 @@
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import { createEventDispatcher } from "svelte"
export let value = "Anchor" export let value = "Anchor"
export let size = "M" export let size = "M"
@ -11,7 +12,7 @@
let open = false let open = false
// const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const iconList = [ const iconList = [
{ {
@ -44,12 +45,11 @@
], ],
}, },
] ]
/*
const onChange = value => { const onChange = value => {
dispatch("change", value) dispatch("change", value)
open = false open = false
} }
*/
</script> </script>
<div class="container"> <div class="container">
@ -76,7 +76,7 @@
{#each icon.icons as icon} {#each icon.icons as icon}
<div <div
on:click={() => { on:click={() => {
value = icon onChange(icon)
}} }}
> >
<Icon name={icon} /> <Icon name={icon} />

View File

@ -11,50 +11,66 @@
Search, Search,
Divider, Divider,
Detail, Detail,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import UserRow from "./_components/UserRow.svelte" import UserRow from "./_components/UserRow.svelte"
import { users } from "stores/portal" import { users, apps, groups } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import GroupAppsRow from "./_components/GroupAppsRow.svelte"
export let groupId
let popoverAnchor let popoverAnchor
let popover let popover
$: group = $groups.find(x => x._id === groupId)
let searchTerm = "" let searchTerm = ""
let selectedUsers = [] let selectedUsers = []
$: filteredUsers = $users.filter(user => $: filteredUsers = $users.filter(
user?.email?.toLowerCase().includes(searchTerm.toLowerCase()) user =>
selectedUsers &&
user?.email?.toLowerCase().includes(searchTerm.toLowerCase())
) )
let app_list = [
let group = {
_id: "gr_123456",
color: "green",
icon: "Anchor",
name: "Core Team",
userCount: 5,
appCount: 2,
}
let groupUsers = [
{ {
email: "peter@budibase.com",
access: "Developer", access: "Developer",
name: "test app",
icon: "Anchor",
color: "blue",
}, },
] ]
/* async function addAll() {
function getGroup() { selectedUsers = [...selectedUsers, ...filteredUsers]
return group.users = selectedUsers
await groups.actions.save(group)
} }
*/
function selectUser(id) { async function selectUser(id) {
let user = selectedUsers.find(user_id => user_id === id) let selectedUser = selectedUsers.find(user_id => user_id === id)
if (user) { let enrichedUser = $users.find(user => user._id === id)
selectedUsers = selectedUsers.filter(id => id !== user) if (selectedUser) {
selectedUsers = selectedUsers.filter(id => id !== selectedUser)
let newUsers = group.users.filter(user => user._id !== id)
group.users = newUsers
} else { } else {
selectedUsers = [...selectedUsers, id] selectedUsers = [...selectedUsers, id]
group.users.push(enrichedUser)
} }
await groups.actions.save(group)
} }
onMount(() => {
console.log($users) async function removeUser(id) {
let newUsers = group.users.filter(user => user._id !== id)
group.users = newUsers
await groups.actions.save(group)
}
onMount(async () => {
try {
await groups.actions.init()
await users.init()
await apps.load()
} catch (error) {
notifications.error("Error fetching User Group data")
}
}) })
</script> </script>
@ -66,13 +82,13 @@
</div> </div>
<div class="header"> <div class="header">
<div class="title"> <div class="title">
<div style="background: {group.color};" class="circle"> <div style="background: {group?.color};" class="circle">
<div> <div>
<Icon size="M" name={group.icon} /> <Icon size="M" name={group?.icon} />
</div> </div>
</div> </div>
<div class="text-padding"> <div class="text-padding">
<Heading>{group.name}</Heading> <Heading>{group?.name}</Heading>
</div> </div>
</div> </div>
<div bind:this={popoverAnchor}> <div bind:this={popoverAnchor}>
@ -90,7 +106,9 @@
> >
</div> </div>
<div> <div>
<ActionButton emphasized size="S">Add all</ActionButton> <ActionButton on:click={addAll} emphasized size="S"
>Add all</ActionButton
>
</div> </div>
</div> </div>
<Divider noMargin /> <Divider noMargin />
@ -121,10 +139,39 @@
</div> </div>
<div class="usersTable"> <div class="usersTable">
{#if groupUsers.length} {#if group?.users.length}
{#each groupUsers as user} {#each group.users as user}
<div> <div>
<UserRow {user} /> <UserRow {removeUser} {user} />
</div>
{/each}
{:else}
<div>
<div class="title header text-padding">
<Icon name="UserGroup" />
<div class="text-padding">
<Body size="S">You have no users in this team</Body>
</div>
</div>
</div>
{/if}
</div>
<div
style="flex-direction: column; margin-top: var(--spacing-m)"
class="title"
>
<Heading weight="light" size="XS">Apps</Heading>
<div style="margin-top: var(--spacing-xs)">
<Body size="S">Manage apps that this User group has been assigned to</Body
>
</div>
</div>
<div style="" class="usersTable">
{#if app_list.length}
{#each app_list as app}
<div>
<GroupAppsRow {app} />
</div> </div>
{/each} {/each}
{:else} {:else}

View File

@ -0,0 +1,58 @@
<script>
import {
ColorPicker,
Body,
ModalContent,
Input,
IconPicker,
} from "@budibase/bbui"
export let group
export let saveGroup
</script>
<ModalContent
onConfirm={() => saveGroup(group)}
size="M"
title="Create User Group"
confirmText="Save"
>
<Input bind:value={group.name} label="Team name" />
<div class="modal-format">
<div class="modal-inner">
<Body size="XS">Icon</Body>
<div class="modal-spacing">
<IconPicker
on:change={e => (group.icon = e.detail)}
bind:value={group.icon}
/>
</div>
</div>
<div class="modal-inner">
<Body size="XS">Color</Body>
<div class="modal-spacing">
<ColorPicker
bind:value={group.color}
on:change={e => (group.color = e.detail)}
/>
</div>
</div>
</div>
</ModalContent>
<style>
.modal-format {
display: flex;
justify-content: space-between;
width: 40%;
}
.modal-inner {
display: flex;
align-items: center;
}
.modal-spacing {
margin-left: var(--spacing-l);
}
</style>

View File

@ -0,0 +1,58 @@
<script>
import { Icon, Body, StatusLight } from "@budibase/bbui"
export let app
</script>
<div class="title">
<div class="name" style="display: flex; margin-left: var(--spacing-xl)">
<div class="app-icon" style="color: {app.icon?.color || ''}">
<Icon size="XL" name={app.icon?.name || "Apps"} />
</div>
</div>
</div>
<div class="desktop">
<div style="display: flex; align-items: center;">
<Body size="M">{app.name}</Body>
<div style="opacity: 0.5; margin: var(--spacing-xs) 0 0 var(--spacing-m)">
<Body size="XS">{app.access}</Body>
</div>
</div>
</div>
<div class="desktop" />
<div
style="display: flex; align-items: baseline; margin-right: var(--spacing-xl);"
>
<StatusLight purple />
<Body size="XS">{app.access}</Body>
</div>
<style>
.name {
grid-gap: var(--spacing-xl);
grid-template-columns: 75px 75px;
align-items: center;
}
.name {
text-decoration: none;
overflow: hidden;
}
.name :global(.spectrum-Heading) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-left: calc(1.5 * var(--spacing-xl));
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
@media (max-width: 640px) {
.desktop {
display: none !important;
}
}
</style>

View File

@ -1,50 +1,87 @@
<script> <script>
import { Button, Icon, Body } from "@budibase/bbui" import {
Button,
Icon,
Body,
ActionMenu,
MenuItem,
Modal,
} from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import CreateEditGroupModal from "./CreateEditGroupModal.svelte"
export let group export let group
export let deleteGroup
export let saveGroup
let modal
let { icon, color, name, userCount, appCount } = group function editGroup() {
modal.show()
}
</script> </script>
<div class="title"> <div class="title">
<div class="name" style="display: flex; margin-left: var(--spacing-xl)"> <div class="name" style="display: flex; margin-left: var(--spacing-xl)">
<div style="background: {color};" class="circle"> <div style="background: {group.color};" class="circle">
<div> <div>
<Icon size="M" name={icon} /> <Icon size="M" name={group.icon} />
</div> </div>
</div> </div>
<div class="name" data-cy="app-name-link"><Body size="S">{name}</Body></div> <div class="name" data-cy="app-name-link">
<Body size="S">{group.name}</Body>
</div>
</div> </div>
</div> </div>
<div class="desktop tableElement"> <div class="desktop tableElement">
<Icon name="User" /> <Icon name="User" />
<div style="margin-left: var(--spacing-l"> <div style="margin-left: var(--spacing-l">
{parseInt(userCount)} user{parseInt(userCount) === 1 ? "" : "s"} {parseInt(group.userCount) || 0} user{parseInt(group.userCount) === 1
? ""
: "s"}
</div> </div>
</div> </div>
<div class="desktop tableElement"> <div class="desktop tableElement">
<Icon name="WebPage" /> <Icon name="WebPage" />
<div style="margin-left: var(--spacing-l"> <div style="margin-left: var(--spacing-l)">
{parseInt(appCount)} app{parseInt(appCount) === 1 ? "" : "s"} {parseInt(group.appCount) || 0} app{parseInt(group.appCount) === 1
? ""
: "s"}
</div> </div>
</div> </div>
<div> <div>
<div class="app-row-actions"> <div class="group-row-actions">
<div> <div>
<Button on:click={() => $goto(`./${group._id}`)} size="S" cta <Button on:click={() => $goto(`./${group._id}`)} size="S" cta
>Manage</Button >Manage</Button
> >
</div> </div>
<div class="">
<ActionMenu align="right">
<span slot="control">
<Icon hoverable name="More" />
</span>
<MenuItem on:click={() => deleteGroup(group)} icon="Delete"
>Delete</MenuItem
>
<MenuItem on:click={() => editGroup(group)} icon="Delete">Edit</MenuItem
>
</ActionMenu>
</div>
</div> </div>
</div> </div>
<Modal bind:this={modal}>
<CreateEditGroupModal {group} {saveGroup} />
</Modal>
<style> <style>
.app-row-actions { .group-row-actions {
display: flex; display: flex;
float: right; float: right;
margin-right: var(--spacing-xl); margin-right: var(--spacing-xl);
grid-template-columns: 75px 75px;
grid-gap: var(--spacing-xl);
} }
.name { .name {
grid-gap: var(--spacing-xl); grid-gap: var(--spacing-xl);

View File

@ -2,8 +2,8 @@
import { Icon, Body, Avatar } from "@budibase/bbui" import { Icon, Body, Avatar } from "@budibase/bbui"
export let user export let user
export let removeUser
function removeFromGroup() {} $: console.log(user)
</script> </script>
<div class="title"> <div class="title">
@ -22,7 +22,9 @@
</div> </div>
</div> </div>
<div class="desktop" /> <div class="desktop" />
<div><Icon on:click={removeFromGroup} hoverable size="L" name="Close" /></div> <div>
<Icon on:click={() => removeUser(user._id)} hoverable size="L" name="Close" />
</div>
<style> <style>
.name { .name {

View File

@ -2,35 +2,34 @@
import { import {
Layout, Layout,
Heading, Heading,
ColorPicker,
Body, Body,
Button, Button,
Modal, Modal,
ModalContent,
Input,
Tag, Tag,
Tags, Tags,
IconPicker, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { API } from "api"
import { groups } from "stores/portal" import { groups } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
import UserGroupsRow from "./_components/UserGroupsRow.svelte" import UserGroupsRow from "./_components/UserGroupsRow.svelte"
let modal let modal
let selectedColor let group = {}
let selectedIcon
let groupName
let proPlan = true let proPlan = true
async function saveConfig() { async function deleteGroup(group) {
try { try {
API.saveGroup({ groups.actions.delete(group)
color: selectedColor, } catch (error) {
icon: selectedIcon, notifications.error(`Failed to delete group`)
name: groupName, }
}) }
async function saveGroup(group) {
try {
await groups.actions.save(group)
} catch (error) { } catch (error) {
notifications.error(`Failed to save group`) notifications.error(`Failed to save group`)
} }
@ -38,7 +37,7 @@
onMount(async () => { onMount(async () => {
try { try {
await groups.init() await groups.actions.init()
} catch (error) { } catch (error) {
notifications.error("Error getting User groups") notifications.error("Error getting User groups")
} }
@ -81,56 +80,17 @@
<div class="groupTable"> <div class="groupTable">
{#each $groups as group} {#each $groups as group}
<div> <div>
<UserGroupsRow {group} /> <UserGroupsRow {saveGroup} {deleteGroup} {group} />
</div> </div>
{/each} {/each}
</div> </div>
</Layout> </Layout>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<ModalContent <CreateEditGroupModal {group} {saveGroup} />
onConfirm={saveConfig}
size="M"
title="Create User Group"
confirmText="Save"
>
<Input bind:value={groupName} label="Team name" />
<div class="modal-format">
<div class="modal-inner">
<Body size="XS">Icon</Body>
<div class="modal-spacing">
<IconPicker bind:value={selectedIcon} />
</div>
</div>
<div class="modal-inner">
<Body size="XS">Color</Body>
<div class="modal-spacing">
<ColorPicker
bind:value={selectedColor}
on:change={e => (selectedColor = e.detail)}
/>
</div>
</div>
</div>
</ModalContent>
</Modal> </Modal>
<style> <style>
.modal-format {
display: flex;
justify-content: space-between;
width: 40%;
}
.modal-inner {
display: flex;
align-items: center;
}
.modal-spacing {
margin-left: var(--spacing-l);
}
.align-buttons { .align-buttons {
display: flex; display: flex;
column-gap: var(--spacing-xl); column-gap: var(--spacing-xl);

View File

@ -1,24 +1,46 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { API } from "api" import { API } from "api"
import { update } from "lodash"
export function createGroupsStore() { export function createGroupsStore() {
const { subscribe, set } = writable([]) const store = writable([])
async function init() { const actions = {
const users = await API.getGroups() init: async () => {
set(users) const users = await API.getGroups()
} store.set(users)
},
async function save(data) { save: async group => {
await API.saveGroup(data) const response = await API.saveGroup(group)
} group._id = response._id
group._rev = response._rev
store.update(state => {
const currentIdx = state.findIndex(gr => gr._id === response._id)
if (currentIdx >= 0) {
state.splice(currentIdx, 1, group)
} else {
state.push(group)
}
return state
})
},
return { delete: async group => {
subscribe, await API.deleteGroup({
init, id: group._id,
save, rev: group._rev,
} })
store.update(state => {
state = state.filter(state => state._id !== group._id)
return state
})
},
}
return {
subscribe: store.subscribe,
actions,
}
} }
export const groups = createGroupsStore() export const groups = createGroupsStore()

View File

@ -1,18 +1,40 @@
export const buildGroupsEndpoints = API => ({ export const buildGroupsEndpoints = API => ({
/** /**
* Creates or updates a user in the current tenant. * Creates a user group.
* @param user the new user to create * @param user the new group to create
*/ */
saveGroup: async group => { saveGroup: async group => {
return await API.post({ return await API.post({
url: "/api/global/groups", url: "/api/global/groups",
body: group, body: group,
}) })
}, },
getGroups: async () => { /**
return await API.get({ * Gets all of the user groups
url: "/api/global/groups", */
}) getGroups: async () => {
return await API.get({
url: "/api/global/groups",
})
},
} /**
* Gets a group by ID
*/
getGroup: async id => {
return await API.get({
url: `/api/global/groups/${id}`,
})
},
/**
* Deletes a user group
* @param id the id of the config to delete
* @param rev the revision of the config to delete
*/
deleteGroup: async ({ id, rev }) => {
return await API.delete({
url: `/api/global/groups/${id}/${rev}`,
})
},
}) })

View File

@ -236,6 +236,6 @@ export const createAPIClient = config => {
...buildViewEndpoints(API), ...buildViewEndpoints(API),
...buildSelfEndpoints(API), ...buildSelfEndpoints(API),
...buildLicensingEndpoints(API), ...buildLicensingEndpoints(API),
...buildGroupsEndpoints(API) ...buildGroupsEndpoints(API),
} }
} }

View File

@ -1,58 +0,0 @@
const {
generateUserGroupID,
getUserGroupsParams
} = require("@budibase/backend-core/db")
const { Configs } = require("../../../constants")
const email = require("../../../utilities/email")
const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const env = require("../../../environment")
const {
withCache,
CacheKeys,
bustCache,
} = require("@budibase/backend-core/cache")
exports.save = async function (ctx) {
const db = getGlobalDB()
// Config does not exist yet
if (!ctx.request.body._id) {
ctx.request.body._id = generateUserGroupID(ctx.request.body.name)
}
try {
const response = await db.put(ctx.request.body)
ctx.body = {
_id: response.id,
_rev: response.rev,
}
} catch (err) {
ctx.throw(400, err)
}
}
exports.fetch = async function (ctx) {
const db = getGlobalDB()
console.log('in here')
const response = await db.allDocs(
getUserGroupsParams(null, {
include_docs: true,
})
)
ctx.body = response.rows.map(row => row.doc)
}
exports.destroy = async function (ctx) {
const db = getGlobalDB()
const { id, rev } = ctx.params
try {
await db.remove(id, rev)
ctx.body = { message: "Group deleted successfully" }
} catch (err) {
ctx.throw(err.status, err)
}
}

View File

@ -0,0 +1,66 @@
const {
generateUserGroupID,
getUserGroupsParams,
} = require("@budibase/backend-core/db")
const { Configs } = require("../../../constants")
const email = require("../../../utilities/email")
const { getGlobalDB, getTenantId } = require("@budibase/backend-core/tenancy")
const env = require("../../../environment")
const {
withCache,
CacheKeys,
bustCache,
} = require("@budibase/backend-core/cache")
exports.save = async function (ctx: any) {
const db = getGlobalDB()
// Config does not exist yet
if (!ctx.request.body._id) {
ctx.request.body._id = generateUserGroupID(ctx.request.body.name)
}
try {
const response = await db.put(ctx.request.body)
ctx.body = {
_id: response.id,
_rev: response.rev,
}
} catch (err) {
ctx.throw(400, err)
}
}
exports.fetch = async function (ctx: any) {
const db = getGlobalDB()
const response = await db.allDocs(
getUserGroupsParams(null, {
include_docs: true,
})
)
ctx.body = response.rows.map((row: any) => row.doc)
}
exports.destroy = async function (ctx: any) {
const db = getGlobalDB()
const { id, rev } = ctx.params
try {
await db.remove(id, rev)
ctx.body = { message: "Group deleted successfully" }
} catch (err: any) {
ctx.throw(err.status, err)
}
}
/**
* Gets a group by ID from the global database.
*/
exports.find = async function (ctx: any) {
const db = getGlobalDB()
try {
ctx.body = await db.get(ctx.params.id)
} catch (err: any) {
ctx.throw(err.status, err)
}
}

View File

@ -7,21 +7,26 @@ const Joi = require("joi")
const router = Router() const router = Router()
function buildGroupSaveValidation() { function buildGroupSaveValidation() {
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
color: Joi.string().required(), _id: Joi.string().optional(),
icon: Joi.string().required(), _rev: Joi.string().optional(),
name: Joi.string().required() color: Joi.string().required(),
}).required()) icon: Joi.string().required(),
name: Joi.string().required(),
users: Joi.array().optional()
}).required())
} }
router.post( router
.post(
"/api/global/groups", "/api/global/groups",
adminOnly, adminOnly,
buildGroupSaveValidation(), buildGroupSaveValidation(),
controller.save controller.save
) )
.get("/api/global/groups", controller.fetch) .get("/api/global/groups", controller.fetch)
.delete("/api/global/groups/:id/:rev", adminOnly, controller.destroy)
.get("/api/global/groups/:id", adminOnly, controller.find)
module.exports = router module.exports = router

View File

@ -26,5 +26,5 @@ exports.routes = [
statusRoutes, statusRoutes,
selfRoutes, selfRoutes,
licenseRoutes, licenseRoutes,
userGroupRoutes userGroupRoutes,
] ]