Add new UserFetch for searching users table and use in users page. Add debounce utility

This commit is contained in:
Andrew Kingston 2022-08-05 15:57:21 +01:00
parent f188472533
commit 5acce00d14
5 changed files with 176 additions and 95 deletions

View File

@ -24,41 +24,53 @@
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte" import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
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 { get } from "svelte/store" import { get } from "svelte/store"
import { Constants } from "@budibase/frontend-core" import { Constants, Utils, fetchData } from "@budibase/frontend-core"
import { API } from "api"
const fetch = fetchData({
API,
datasource: {
type: "user",
},
})
let loaded = false
let enrichedUsers = [] let enrichedUsers = []
let createUserModal, let createUserModal,
inviteConfirmationModal, inviteConfirmationModal,
onboardingTypeModal, onboardingTypeModal,
passwordModal, passwordModal,
importUsersModal importUsersModal
let pageInfo = createPaginationStore() let searchEmail = undefined
let prevEmail = undefined,
searchEmail = undefined
let selectedRows = [] let selectedRows = []
let customRenderers = [ let customRenderers = [
{ column: "userGroups", component: GroupsTableRenderer }, { column: "userGroups", component: GroupsTableRenderer },
{ column: "apps", component: AppsTableRenderer }, { column: "apps", component: AppsTableRenderer },
{ column: "role", component: RoleTableRenderer }, { column: "role", component: RoleTableRenderer },
] ]
let userData = []
$: debouncedUpdateFetch(searchEmail)
$: schema = { $: schema = {
email: {}, email: {
sortable: false,
},
role: { role: {
sortable: false, sortable: false,
}, },
...($auth.groupsEnabled && { ...($auth.groupsEnabled && {
userGroups: { sortable: false, displayName: "User groups" }, userGroups: {
sortable: false,
displayName: "User groups",
},
}), }),
apps: {}, apps: {
sortable: false,
},
} }
$: userData = []
$: page = $pageInfo.page
$: fetchUsers(page, searchEmail)
$: { $: {
enrichedUsers = $users.data?.map(user => { enrichedUsers = $fetch.rows?.map(user => {
let userGroups = [] let userGroups = []
$groups.forEach(group => { $groups.forEach(group => {
if (group.users) { if (group.users) {
@ -78,6 +90,18 @@
}) })
} }
const updateFetch = email => {
if (email == null && $fetch.query.email == null) {
return
}
fetch.update({
query: {
email,
},
})
}
const debouncedUpdateFetch = Utils.debounce(updateFetch, 250)
const showOnboardingTypeModal = async addUsersData => { const showOnboardingTypeModal = async addUsersData => {
userData = await removingDuplicities(addUsersData) userData = await removingDuplicities(addUsersData)
if (!userData?.users?.length) return if (!userData?.users?.length) return
@ -161,14 +185,6 @@
} }
} }
onMount(async () => {
try {
await groups.actions.init()
} catch (error) {
notifications.error("Error fetching User Group data")
}
})
const deleteRows = async () => { const deleteRows = async () => {
try { try {
let ids = selectedRows.map(user => user._id) let ids = selectedRows.map(user => user._id)
@ -179,88 +195,81 @@
await users.bulkDelete(ids) await users.bulkDelete(ids)
notifications.success(`Successfully deleted ${selectedRows.length} rows`) notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = [] selectedRows = []
await fetchUsers(page, searchEmail) await fetch.refresh()
} catch (error) { } catch (error) {
notifications.error("Error deleting rows") notifications.error("Error deleting rows")
} }
} }
async function fetchUsers(page, email) { onMount(async () => {
if ($pageInfo.loading) {
return
}
// need to remove the page if they've started searching
if (email && !prevEmail) {
pageInfo.reset()
page = undefined
}
prevEmail = email
try { try {
pageInfo.loading() loaded = false
await users.search({ page, email }) await groups.actions.init()
pageInfo.fetched($users.hasNextPage, $users.nextPage) loaded = true
} catch (error) { } catch (error) {
notifications.error("Error getting user list") notifications.error("Error fetching User Group data")
} }
} })
</script> </script>
<Layout noPadding gap="M"> {#if loaded && $fetch.loaded}
<Layout gap="XS" noPadding> <Layout noPadding gap="M">
<Heading>Users</Heading> <Layout gap="XS" noPadding>
<Body>Add users and control who gets access to your published apps</Body> <Heading>Users</Heading>
</Layout> <Body>Add users and control who gets access to your published apps</Body>
<Divider /> </Layout>
<div class="controls"> <Divider />
<ButtonGroup> <div class="controls">
<Button <ButtonGroup>
dataCy="add-user" <Button
on:click={createUserModal.show} dataCy="add-user"
icon="UserAdd" on:click={createUserModal.show}
cta>Add users</Button icon="UserAdd"
> cta>Add users</Button
<Button >
on:click={importUsersModal.show} <Button
icon="Import" on:click={importUsersModal.show}
secondary icon="Import"
newStyles secondary
> newStyles
Import users >
</Button> Import users
</ButtonGroup> </Button>
<div class="controls-right"> </ButtonGroup>
<Search bind:value={searchEmail} placeholder="Search" /> <div class="controls-right">
{#if selectedRows.length > 0} <Search bind:value={searchEmail} placeholder="Search" />
<DeleteRowsButton {#if selectedRows.length > 0}
item="user" <DeleteRowsButton
on:updaterows item="user"
{selectedRows} on:updaterows
{deleteRows} {selectedRows}
/> {deleteRows}
{/if} />
{/if}
</div>
</div> </div>
</div> <Table
<Table on:click={({ detail }) => $goto(`./${detail._id}`)}
on:click={({ detail }) => $goto(`./${detail._id}`)} {schema}
{schema} bind:selectedRows
bind:selectedRows data={enrichedUsers}
data={enrichedUsers} allowEditColumns={false}
allowEditColumns={false} allowEditRows={false}
allowEditRows={false} allowSelectRows={true}
allowSelectRows={true} showHeaderBorder={false}
showHeaderBorder={false} {customRenderers}
{customRenderers}
/>
<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> <div class="pagination">
</Layout> <Pagination
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.loading ? false : $fetch.hasPrevPage}
hasNextPage={$fetch.loading ? false : $fetch.hasNextPage}
goToPrevPage={fetch.prevPage}
goToNextPage={fetch.nextPage}
/>
</div>
</Layout>
{/if}
<Modal bind:this={createUserModal}> <Modal bind:this={createUserModal}>
<AddUserModal {showOnboardingTypeModal} /> <AddUserModal {showOnboardingTypeModal} />
@ -272,11 +281,11 @@
title="Invites sent!" title="Invites sent!"
confirmText="Done" confirmText="Done"
> >
<Body size="S" <Body size="S">
>Your users should now recieve an email invite to get access to their Your users should now recieve an email invite to get access to their
Budibase account</Body Budibase account
></ModalContent </Body>
> </ModalContent>
</Modal> </Modal>
<Modal bind:this={onboardingTypeModal}> <Modal bind:this={onboardingTypeModal}>

View File

@ -158,6 +158,8 @@ export default class DataFetch {
schema, schema,
query, query,
loading: true, loading: true,
cursors: [],
cursor: null,
})) }))
// Actually fetch data // Actually fetch data

View File

@ -0,0 +1,46 @@
import { get } from "svelte/store"
import DataFetch from "./DataFetch.js"
import { TableNames } from "../constants"
export default class UserFetch extends DataFetch {
constructor(opts) {
super({
...opts,
datasource: {
tableId: TableNames.USERS,
},
})
}
determineFeatureFlags() {
return {
supportsSearch: true,
supportsSort: false,
supportsPagination: true,
}
}
async getData() {
const { cursor, query } = get(this.store)
try {
// "query" normally contains a lucene query, but users uses a non-standard
// search endpoint so we use query uniquely here
const res = await this.API.searchUsers({
page: cursor,
email: query.email,
appId: query.appId,
})
return {
rows: res?.data || [],
hasNextPage: res?.hasNextPage || false,
cursor: res?.nextPage || null,
}
} catch (error) {
return {
rows: [],
hasNextPage: false,
error,
}
}
}
}

View File

@ -5,12 +5,14 @@ import RelationshipFetch from "./RelationshipFetch.js"
import NestedProviderFetch from "./NestedProviderFetch.js" import NestedProviderFetch from "./NestedProviderFetch.js"
import FieldFetch from "./FieldFetch.js" import FieldFetch from "./FieldFetch.js"
import JSONArrayFetch from "./JSONArrayFetch.js" import JSONArrayFetch from "./JSONArrayFetch.js"
import UserFetch from "./UserFetch.js"
const DataFetchMap = { const DataFetchMap = {
table: TableFetch, table: TableFetch,
view: ViewFetch, view: ViewFetch,
query: QueryFetch, query: QueryFetch,
link: RelationshipFetch, link: RelationshipFetch,
user: UserFetch,
// Client specific datasource types // Client specific datasource types
provider: NestedProviderFetch, provider: NestedProviderFetch,

View File

@ -19,3 +19,25 @@ export const sequential = fn => {
} }
} }
} }
/**
* Utility to debounce an async function and ensure a minimum delay between
* invocations is enforced.
* @param callback an async function to run
* @param minDelay the minimum delay between invocations
* @returns {Promise} a debounced version of the callback
*/
export const debounce = (callback, minDelay = 1000) => {
let timeout
return async (...params) => {
return new Promise(resolve => {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(async () => {
console.log("timeout reached!")
resolve(await callback(...params))
}, minDelay)
})
}
}