1
0
Fork 0
mirror of synced 2024-06-28 11:00:55 +12:00

some user table fixes

This commit is contained in:
Peter Clement 2022-07-13 15:46:10 +01:00
parent 6eb4f189ce
commit 4543b1213f
16 changed files with 268 additions and 143 deletions

View file

@ -27,6 +27,7 @@
import { AppStatus } from "constants"
import Logo from "assets/bb-space-man.svg"
import AccessFilter from "./_components/AcessFilter.svelte"
import { Constants } from "@budibase/frontend-core"
let sortBy = "name"
let template
@ -68,6 +69,8 @@
$: unlocked = lockedApps?.length === 0
$: automationErrors = getAutomationErrors(enrichedApps)
$: isProPlan = $auth.user?.license.plan.type !== Constants.PlanType.FREE
const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({
...app,
@ -355,7 +358,7 @@
</Button>
{/if}
<div class="filter">
{#if $groups.length}
{#if isProPlan && $groups.length}
<AccessFilter on:change={accessFilterAction} />
{/if}
<Select

View file

@ -9,12 +9,14 @@
Tags,
notifications,
} from "@budibase/bbui"
import { groups } from "stores/portal"
import { groups, auth } from "stores/portal"
import { onMount } from "svelte"
import { Constants } from "@budibase/frontend-core"
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
import UserGroupsRow from "./_components/UserGroupsRow.svelte"
$: isProPlan = $auth.user?.license.plan.type !== Constants.PlanType.FREE
let modal
let group = {
name: "",
@ -23,7 +25,6 @@
users: [],
apps: [],
}
let proPlan = true
async function deleteGroup(group) {
try {
@ -54,7 +55,7 @@
<Layout gap="XS" noPadding>
<div style="display: flex;">
<Heading size="M">User groups</Heading>
{#if !proPlan}
{#if !isProPlan}
<Tags>
<div class="tags">
<div class="tag">
@ -68,13 +69,15 @@
</Layout>
<div class="align-buttons">
<Button
icon={proPlan ? "UserGroup" : ""}
cta={proPlan}
newStyles
icon={isProPlan ? "UserGroup" : ""}
cta={isProPlan}
on:click={() => modal.show()}
>{proPlan ? "Create user group" : "Upgrade Account"}</Button
>{isProPlan ? "Create user group" : "Upgrade Account"}</Button
>
{#if !proPlan}
{#if !isProPlan}
<Button
newStyles
secondary
on:click={() => {
window.open("https://budibase.com/pricing/", "_blank")
@ -83,7 +86,7 @@
{/if}
</div>
{#if proPlan}
{#if isProPlan}
<div class="groupTable">
{#each $groups as group}
<div>

View file

@ -40,6 +40,8 @@
let selectedGroups = []
let allAppList = []
$: isProPlan = $auth.user?.license.plan.type !== Constants.PlanType.FREE
$: allAppList = $apps
.filter(x => {
if ($userFetch.data?.roles) {
@ -244,52 +246,53 @@
</div>
</Layout>
<!-- User groups -->
<Layout gap="XS" noPadding>
<div class="tableTitle">
<div>
<Heading size="XS">User groups</Heading>
<Body size="S">Add or remove this user from user groups</Body>
</div>
<div bind:this={popoverAnchor}>
<Button on:click={popover.show()} icon="UserGroup" cta
>Add User Group</Button
>
</div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
key={"name"}
title={"Group"}
bind:searchTerm
bind:selected={selectedGroups}
bind:filtered={filteredGroups}
{addAll}
select={addGroup}
/>
</Popover>
</div>
<List>
{#if userGroups.length}
{#each userGroups as group}
<ListItem
title={group.name}
icon={group.icon}
iconBackground={group.color}
><Icon
on:click={removeGroup(group._id)}
hoverable
size="L"
name="Close"
/></ListItem
{#if isProPlan}
<!-- User groups -->
<Layout gap="XS" noPadding>
<div class="tableTitle">
<div>
<Heading size="XS">User groups</Heading>
<Body size="S">Add or remove this user from user groups</Body>
</div>
<div bind:this={popoverAnchor}>
<Button on:click={popover.show()} icon="UserGroup" cta
>Add User Group</Button
>
{/each}
{:else}
<ListItem icon="UserGroup" title="No groups" />
{/if}
</List>
</Layout>
</div>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
key={"name"}
title={"Group"}
bind:searchTerm
bind:selected={selectedGroups}
bind:filtered={filteredGroups}
{addAll}
select={addGroup}
/>
</Popover>
</div>
<List>
{#if userGroups.length}
{#each userGroups as group}
<ListItem
title={group.name}
icon={group.icon}
iconBackground={group.color}
><Icon
on:click={removeGroup(group._id)}
hoverable
size="L"
name="Close"
/></ListItem
>
{/each}
{:else}
<ListItem icon="UserGroup" title="No groups" />
{/if}
</List>
</Layout>
{/if}
<!-- User Apps -->
<Layout gap="S" noPadding>
<div class="appsTitle">

View file

@ -7,7 +7,7 @@
InputDropdown,
Layout,
} from "@budibase/bbui"
import { groups } from "stores/portal"
import { groups, auth } from "stores/portal"
import { createEventDispatcher } from "svelte"
import { Constants } from "@budibase/frontend-core"
@ -17,6 +17,8 @@
let disabled
let userGroups = []
$: isProPlan = $auth.user?.license.plan.type !== Constants.PlanType.FREE
$: userData = [
{
email: "",
@ -67,14 +69,16 @@
</div>
</Layout>
<Multiselect
bind:value={userGroups}
placeholder="Select User Groups"
label="User Groups"
options={$groups}
getOptionLabel={option => option.name}
getOptionValue={option => option._id}
/>
{#if isProPlan}
<Multiselect
bind:value={userGroups}
placeholder="Select User Groups"
label="User Groups"
options={$groups}
getOptionLabel={option => option.name}
getOptionValue={option => option._id}
/>
{/if}
</ModalContent>
<style>

View file

@ -6,9 +6,8 @@
Multiselect,
notifications,
} from "@budibase/bbui"
import { groups } from "stores/portal"
import { groups, auth } from "stores/portal"
import { emailValidator } from "../../../../../../helpers/validation"
import { Constants } from "@budibase/frontend-core"
const BYTES_IN_MB = 1000000
@ -21,7 +20,9 @@
let userEmails = []
let userGroups = []
let usersRole = null
$: invalidEmails = []
$: isProPlan = $auth.user?.license.plan.type !== Constants.PlanType.FREE
const validEmails = userEmails => {
for (const email of userEmails) {
@ -86,14 +87,16 @@
options={Constants.BuilderRoleDescriptions}
/>
<Multiselect
bind:value={userGroups}
placeholder="Select User Groups"
label="User Groups"
options={$groups}
getOptionLabel={option => option.name}
getOptionValue={option => option._id}
/>
{#if isProPlan}
<Multiselect
bind:value={userGroups}
placeholder="Select User Groups"
label="User Groups"
options={$groups}
getOptionLabel={option => option.name}
getOptionValue={option => option._id}
/>
{/if}
</ModalContent>
<style>

View file

@ -17,7 +17,7 @@
</div>
{value}
{:else}
<div class="text">Invite pending...</div>
<div class="text">Not Available</div>
{/if}
</div>

View file

@ -15,8 +15,9 @@
Label,
} from "@budibase/bbui"
import AddUserModal from "./_components/AddUserModal.svelte"
import { users, groups } from "stores/portal"
import { users, groups, auth } from "stores/portal"
import { onMount } from "svelte"
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
import NameTableRenderer from "./_components/NameTableRenderer.svelte"
@ -27,25 +28,7 @@
import PasswordModal from "./_components/PasswordModal.svelte"
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
import { createPaginationStore } from "helpers/pagination"
const schema = {
name: {},
email: {},
role: {
noPropagation: true,
sortable: false,
},
userGroups: { sortable: false, displayName: "User groups" },
apps: { width: "120px" },
settings: {
sortable: false,
width: "60px",
displayName: "",
align: "Right",
},
}
$: userData = []
import { Constants } from "@budibase/frontend-core"
const accessTypes = [
{
@ -74,6 +57,38 @@
let prevEmail = undefined,
searchEmail = undefined
let selectedRows = []
let customRenderers = [
{ column: "userGroups", component: GroupsTableRenderer },
{ column: "apps", component: AppsTableRenderer },
{ column: "name", component: NameTableRenderer },
{ column: "settings", component: SettingsTableRenderer },
{ column: "role", component: RoleTableRenderer },
]
$: isProPlan = $auth.user?.license.plan.type !== Constants.PlanType.FREE
$: schema = {
name: {},
email: {},
role: {
noPropagation: true,
sortable: false,
},
...(isProPlan && {
userGroups: { sortable: false, displayName: "User groups" },
}),
apps: { width: "120px" },
settings: {
sortable: false,
width: "60px",
displayName: "",
align: "Right",
},
}
$: userData = []
$: page = $pageInfo.page
$: fetchUsers(page, searchEmail)
@ -164,6 +179,19 @@
}
})
const deleteRows = async () => {
try {
let ids = selectedRows.map(user => user._id)
await users.bulkDelete(ids)
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = []
await fetchUsers(page, searchEmail)
} catch (error) {
console.log(error)
notifications.error("Error deleting rows")
}
}
async function fetchUsers(page, email) {
if ($pageInfo.loading) {
return
@ -211,26 +239,25 @@
<Button on:click={importUsersModal.show} icon="Import" primary
>Import Users</Button
>
<div class="field">
<Label size="L">Search email</Label>
<Search bind:value={searchEmail} placeholder="" />
</div>
{#if selectedRows.length > 0}
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
{/if}
</ButtonGroup>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema}
bind:selectedRows
data={enrichedUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={true}
showHeaderBorder={false}
customRenderers={[
{ column: "userGroups", component: GroupsTableRenderer },
{ column: "apps", component: AppsTableRenderer },
{ column: "name", component: NameTableRenderer },
{ column: "settings", component: SettingsTableRenderer },
{ column: "role", component: RoleTableRenderer },
]}
{customRenderers}
/>
<div class="pagination">
<Pagination

View file

@ -8,13 +8,15 @@
ListItem,
Modal,
notifications,
Pagination,
} from "@budibase/bbui"
import { onMount } from "svelte"
import RoleSelect from "components/common/RoleSelect.svelte"
import { users, groups, apps } from "stores/portal"
import { users, groups, apps, auth } from "stores/portal"
import AssignmentModal from "./AssignmentModal.svelte"
import { createPaginationStore } from "helpers/pagination"
import { Constants } from "@budibase/frontend-core"
export let app
@ -22,11 +24,11 @@
let appGroups = []
let appUsers = []
let pageInfo = createPaginationStore()
let prevSearch = undefined,
search = undefined
$: page = $pageInfo.page
$: fetchUsers(page, search)
$: fetchUsers(page)
$: isProPlan = $auth.user?.license.plan.type === Constants.PlanType.FREE
$: appUsers =
$users.data?.filter(x => {
@ -41,6 +43,19 @@
})
})
$: filteredUsers =
$users.data?.filter(x => {
return !Object.keys(x.roles).find(y => {
return extractAppId(y) === extractAppId(app.appId)
})
}) || []
$: filteredGroups = $groups.filter(element => {
return !element.apps.find(y => {
return y.appId === app.appId
})
})
function extractAppId(id) {
const split = id?.split("_") || []
return split.length ? split[split.length - 1] : null
@ -70,26 +85,27 @@
await users.save(newUser)
}
})
await pageInfo.reset()
await groups.actions.init()
await users.search({ page, appId: app.appId })
}
/*
async function updateRole(user) {
console.log(user)
async function updateUserRole(role, user) {
user.roles[app.appId] = role
users.save(user)
}
*/
async function fetchUsers(page, search) {
async function updateGroupRole(role, group) {
group.role = role
groups.actions.save(group)
}
async function fetchUsers(page) {
if ($pageInfo.loading) {
return
}
// need to remove the page if they've started searching
if (search && !prevSearch) {
pageInfo.reset()
page = undefined
}
prevSearch = search
try {
pageInfo.loading()
await users.search({ page, search })
await users.search({ page, appId: app.appId })
pageInfo.fetched($users.hasNextPage, $users.nextPage)
} catch (error) {
notifications.error("Error getting user list")
@ -120,21 +136,29 @@
>
</div>
</div>
<List title="User Groups">
{#each appGroups as group}
<ListItem
title={group.name}
icon={group.icon}
iconBackground={group.color}
>
<RoleSelect autoWidth quiet value={group.role} />
</ListItem>
{/each}
</List>
{#if isProPlan}
<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.role}
/>
</ListItem>
{/each}
</List>
{/if}
<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[
@ -146,6 +170,15 @@
</ListItem>
{/each}
</List>
<div class="pagination">
<Pagination
page={$pageInfo.pageNumber}
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
goToPrevPage={pageInfo.prevPage}
goToNextPage={pageInfo.nextPage}
/>
</div>
{:else}
<div class="align">
<Layout gap="S">
@ -167,7 +200,11 @@
</div>
<Modal bind:this={assignmentModal}>
<AssignmentModal userData={$users.data} {addData} />
<AssignmentModal
userData={filteredUsers.length ? filteredUsers : $users.data}
groups={isProPlan ? filteredGroups : []}
{addData}
/>
</Modal>
<style>

View file

@ -1,19 +1,22 @@
<script>
import { ModalContent, PickerDropdown, ActionButton } from "@budibase/bbui"
import { groups } from "stores/portal"
import { roles } from "stores/backend"
import { RoleUtils } from "@budibase/frontend-core"
export let addData
export let userData = []
export let groups = []
$: optionSections = {
groups: {
data: $groups,
getLabel: group => group.name,
getValue: group => group._id,
getIcon: group => group.icon,
getColour: group => group.color,
},
...(groups.length && {
groups: {
data: groups,
getLabel: group => group.name,
getValue: group => group._id,
getIcon: group => group.icon,
getColour: group => group.color,
},
}),
users: {
data: userData,
getLabel: user => user.email,

View file

@ -75,6 +75,10 @@ export function createUsersStore() {
update(users => users.filter(user => user._id !== id))
}
async function bulkDelete(userIds) {
await API.deleteUsers(userIds)
}
async function save(user) {
return await API.saveUser(user)
}
@ -87,6 +91,7 @@ export function createUsersStore() {
acceptInvite,
create,
save,
bulkDelete,
delete: del,
}
}

View file

@ -107,6 +107,19 @@ export const buildUserEndpoints = API => ({
})
},
/**
* Deletes multiple users
* @param userId the ID of the user to delete
*/
deleteUsers: async userIds => {
return await API.post({
url: `/api/global/users/bulkDelete`,
body: {
userIds,
},
})
},
/**
* Invites a user to the current tenant.
* @param email the email address to send the invitation to

View file

@ -84,6 +84,13 @@ export const BuilderRoleDescriptions = [
},
]
export const PlanType = {
FREE: "free",
TEAM: "team",
BUSINESS: "business",
ENTERPRISE: "enterprise",
}
/**
* API version header attached to all requests.
* Version changelog:

View file

@ -73,6 +73,7 @@ const checkAuthorizedResource = async (
export = (permType: any, permLevel: any = null, opts = { schema: false }) =>
async (ctx: any, next: any) => {
console.log(ctx)
// webhooks don't need authentication, each webhook unique
// also internal requests (between services) don't need authorized
if (isWebhookEndpoint(ctx) || ctx.internal) {

View file

@ -31,7 +31,7 @@ export const bulkSave = async (ctx: any) => {
newUsers.forEach((user: any) => {
usersToSave.push(
users.save(user, {
hashPassword: false,
hashPassword: true,
requirePassword: user.requirePassword,
bulkCreate: true,
})
@ -53,10 +53,11 @@ export const bulkSave = async (ctx: any) => {
delete user.password
})
groupsToSave.forEach(async group => {
group.users = [...group.users, ...allUsers]
await db.put(group)
})
if (groupsToSave.length)
groupsToSave.forEach(async group => {
group.users = [...group.users, ...allUsers]
await db.put(group)
})
ctx.body = response
} catch (err: any) {
@ -130,6 +131,19 @@ export const destroy = async (ctx: any) => {
}
}
export const bulkDelete = async (ctx: any) => {
const { userIds } = ctx.request.body
let deleted = 0
userIds.forEach(async (id: any) => {
await users.destroy(id, ctx.user)
deleted++
})
ctx.body = {
message: `${deleted} user(s) deleted`,
}
}
export const search = async (ctx: any) => {
const paginated = await users.paginatedUsers(ctx.request.body)
// user hashed password shouldn't ever be returned

View file

@ -14,6 +14,7 @@ function buildGroupSaveValidation() {
color: Joi.string().required(),
icon: Joi.string().required(),
name: Joi.string().required(),
role: Joi.string().optional(),
users: Joi.array().optional(),
apps: Joi.array().optional(),
createdAt: Joi.string().optional(),

View file

@ -63,6 +63,7 @@ router
.get("/api/global/users", builderOrAdmin, controller.fetch)
.post("/api/global/users/search", builderOrAdmin, controller.search)
.delete("/api/global/users/:id", adminOnly, controller.destroy)
.post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
.get("/api/global/roles/:appId")
.post(
"/api/global/users/invite",