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

View file

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

View file

@ -40,6 +40,8 @@
let selectedGroups = [] let selectedGroups = []
let allAppList = [] let allAppList = []
$: isProPlan = $auth.user?.license.plan.type !== Constants.PlanType.FREE
$: allAppList = $apps $: allAppList = $apps
.filter(x => { .filter(x => {
if ($userFetch.data?.roles) { if ($userFetch.data?.roles) {
@ -244,6 +246,7 @@
</div> </div>
</Layout> </Layout>
{#if isProPlan}
<!-- User groups --> <!-- User groups -->
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<div class="tableTitle"> <div class="tableTitle">
@ -289,7 +292,7 @@
{/if} {/if}
</List> </List>
</Layout> </Layout>
{/if}
<!-- User Apps --> <!-- User Apps -->
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
<div class="appsTitle"> <div class="appsTitle">

View file

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

View file

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

View file

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

View file

@ -15,8 +15,9 @@
Label, Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import AddUserModal from "./_components/AddUserModal.svelte" import AddUserModal from "./_components/AddUserModal.svelte"
import { users, groups } from "stores/portal" import { users, groups, auth } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte" import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
import AppsTableRenderer from "./_components/AppsTableRenderer.svelte" import AppsTableRenderer from "./_components/AppsTableRenderer.svelte"
import NameTableRenderer from "./_components/NameTableRenderer.svelte" import NameTableRenderer from "./_components/NameTableRenderer.svelte"
@ -27,25 +28,7 @@
import PasswordModal from "./_components/PasswordModal.svelte" import PasswordModal from "./_components/PasswordModal.svelte"
import ImportUsersModal from "./_components/ImportUsersModal.svelte" import ImportUsersModal from "./_components/ImportUsersModal.svelte"
import { createPaginationStore } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { Constants } from "@budibase/frontend-core"
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 = []
const accessTypes = [ const accessTypes = [
{ {
@ -74,6 +57,38 @@
let prevEmail = undefined, let prevEmail = undefined,
searchEmail = 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 $: page = $pageInfo.page
$: fetchUsers(page, searchEmail) $: 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) { async function fetchUsers(page, email) {
if ($pageInfo.loading) { if ($pageInfo.loading) {
return return
@ -211,26 +239,25 @@
<Button on:click={importUsersModal.show} icon="Import" primary <Button on:click={importUsersModal.show} icon="Import" primary
>Import Users</Button >Import Users</Button
> >
<div class="field"> <div class="field">
<Label size="L">Search email</Label> <Label size="L">Search email</Label>
<Search bind:value={searchEmail} placeholder="" /> <Search bind:value={searchEmail} placeholder="" />
</div> </div>
{#if selectedRows.length > 0}
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
{/if}
</ButtonGroup> </ButtonGroup>
<Table <Table
on:click={({ detail }) => $goto(`./${detail._id}`)} on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema} {schema}
bind:selectedRows
data={enrichedUsers} data={enrichedUsers}
allowEditColumns={false} allowEditColumns={false}
allowEditRows={false} allowEditRows={false}
allowSelectRows={true} allowSelectRows={true}
showHeaderBorder={false} showHeaderBorder={false}
customRenderers={[ {customRenderers}
{ column: "userGroups", component: GroupsTableRenderer },
{ column: "apps", component: AppsTableRenderer },
{ column: "name", component: NameTableRenderer },
{ column: "settings", component: SettingsTableRenderer },
{ column: "role", component: RoleTableRenderer },
]}
/> />
<div class="pagination"> <div class="pagination">
<Pagination <Pagination

View file

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

View file

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

View file

@ -75,6 +75,10 @@ export function createUsersStore() {
update(users => users.filter(user => user._id !== id)) update(users => users.filter(user => user._id !== id))
} }
async function bulkDelete(userIds) {
await API.deleteUsers(userIds)
}
async function save(user) { async function save(user) {
return await API.saveUser(user) return await API.saveUser(user)
} }
@ -87,6 +91,7 @@ export function createUsersStore() {
acceptInvite, acceptInvite,
create, create,
save, save,
bulkDelete,
delete: del, 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. * Invites a user to the current tenant.
* @param email the email address to send the invitation to * @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. * API version header attached to all requests.
* Version changelog: * Version changelog:

View file

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

View file

@ -31,7 +31,7 @@ export const bulkSave = async (ctx: any) => {
newUsers.forEach((user: any) => { newUsers.forEach((user: any) => {
usersToSave.push( usersToSave.push(
users.save(user, { users.save(user, {
hashPassword: false, hashPassword: true,
requirePassword: user.requirePassword, requirePassword: user.requirePassword,
bulkCreate: true, bulkCreate: true,
}) })
@ -53,6 +53,7 @@ export const bulkSave = async (ctx: any) => {
delete user.password delete user.password
}) })
if (groupsToSave.length)
groupsToSave.forEach(async group => { groupsToSave.forEach(async group => {
group.users = [...group.users, ...allUsers] group.users = [...group.users, ...allUsers]
await db.put(group) await db.put(group)
@ -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) => { export const search = async (ctx: any) => {
const paginated = await users.paginatedUsers(ctx.request.body) const paginated = await users.paginatedUsers(ctx.request.body)
// user hashed password shouldn't ever be returned // user hashed password shouldn't ever be returned

View file

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

View file

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