Updating user page to search through the backend and building a basic pagination store that can be used for it.

This commit is contained in:
mike12345567 2022-06-30 15:39:26 +01:00
parent 63646b0c38
commit 062d834950
11 changed files with 165 additions and 109 deletions

View File

@ -1,4 +1,5 @@
exports.SEPARATOR = "_" exports.SEPARATOR = "_"
exports.UNICODE_MAX = "\ufff0"
const PRE_APP = "app" const PRE_APP = "app"
const PRE_DEV = "dev" const PRE_DEV = "dev"

View File

@ -1,7 +1,7 @@
import { newid } from "../hashing" import { newid } from "../hashing"
import { DEFAULT_TENANT_ID, Configs } from "../constants" import { DEFAULT_TENANT_ID, Configs } from "../constants"
import env from "../environment" import env from "../environment"
import { SEPARATOR, DocumentTypes } from "./constants" import { SEPARATOR, DocumentTypes, UNICODE_MAX } from "./constants"
import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy" import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy"
import fetch from "node-fetch" import fetch from "node-fetch"
import { doWithDB, allDbs } from "./index" import { doWithDB, allDbs } from "./index"
@ -12,8 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions"
import { APP_PREFIX } from "./constants" import { APP_PREFIX } from "./constants"
import * as events from "../events" import * as events from "../events"
const UNICODE_MAX = "\ufff0"
export const ViewNames = { export const ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
BY_API_KEY: "by_api_key", BY_API_KEY: "by_api_key",
@ -439,21 +437,22 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => {
} }
export function pagination( export function pagination(
response: any, data: any[],
pageSize: number, pageSize: number,
paginate: boolean = true { paginate, property } = { paginate: true, property: "_id" }
) { ) {
const data = response.rows.map((row: any) => {
return row.doc ? row.doc : row
})
if (!paginate) { if (!paginate) {
return { data, hasNextPage: false } return { data, hasNextPage: false }
} }
const hasNextPage = data.length > pageSize const hasNextPage = data.length > pageSize
let nextPage = undefined
if (hasNextPage) {
nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id
}
return { return {
data: data.slice(0, pageSize), data: data.slice(0, pageSize),
hasNextPage, hasNextPage,
nextPage: hasNextPage ? data[pageSize]?._id : undefined, nextPage,
} }
} }

View File

@ -1,5 +1,6 @@
const { ViewNames } = require("./db/utils") const { ViewNames } = require("./db/utils")
const { queryGlobalView } = require("./db/views") const { queryGlobalView } = require("./db/views")
const { UNICODE_MAX } = require("./db/constants")
/** /**
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
@ -19,3 +20,24 @@ exports.getGlobalUserByEmail = async email => {
return response return response
} }
/**
* Performs a starts with search on the global email view.
*/
exports.searchGlobalUsersByEmail = async (email, opts) => {
if (typeof email !== "string") {
throw new Error("Must provide a string to search by")
}
const lcEmail = email.toLowerCase()
// handle if passing up startkey for pagination
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
let response = await queryGlobalView(ViewNames.USER_BY_EMAIL, {
...opts,
startkey,
endkey: `${lcEmail}${UNICODE_MAX}`,
})
if (!response) {
response = []
}
return Array.isArray(response) ? response : [response]
}

View File

@ -1,31 +1,56 @@
export class PageInfo { import { writable } from "svelte/store"
constructor(fetch) {
this.reset()
this.fetch = fetch
}
async goToNextPage() { function defaultValue() {
this.pageNumber++ return {
this.prevPage = this.page nextPage: null,
this.page = this.nextPage page: undefined,
this.hasPrevPage = this.pageNumber > 1 hasPrevPage: false,
await this.fetch(this.page) hasNextPage: false,
} pageNumber: 1,
pages: [],
async goToPrevPage() { }
this.pageNumber-- }
this.nextPage = this.page
this.page = this.prevPage export function createPaginationStore() {
this.hasPrevPage = this.pageNumber > 1 const { subscribe, set, update } = writable(defaultValue())
await this.fetch(this.page)
} function prevPage() {
update(state => {
reset() { state.pageNumber--
this.prevPage = null state.nextPage = state.pages.pop()
this.nextPage = null state.page = state.pages[state.pages.length - 1]
this.page = undefined state.hasPrevPage = state.pageNumber > 1
this.hasPrevPage = false return state
this.hasNextPage = false })
this.pageNumber = 1 }
function nextPage() {
update(state => {
state.pageNumber++
state.page = state.nextPage
state.pages.push(state.page)
state.hasPrevPage = state.pageNumber > 1
return state
})
}
function fetched(hasNextPage, nextPage) {
update(state => {
state.hasNextPage = hasNextPage
state.nextPage = nextPage
return state
})
}
function reset() {
set(defaultValue())
}
return {
subscribe,
prevPage,
nextPage,
fetched,
reset,
} }
} }

View File

@ -10,7 +10,9 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation" import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal" import { users } from "stores/portal"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
const password = Math.random().toString(36).substring(2, 22) const password = Math.random().toString(36).substring(2, 22)
const options = ["Email onboarding", "Basic onboarding"] const options = ["Email onboarding", "Basic onboarding"]
const [email, error, touched] = createValidationStore("", emailValidator) const [email, error, touched] = createValidationStore("", emailValidator)
@ -39,6 +41,7 @@
forceResetPassword: true, forceResetPassword: true,
}) })
notifications.success("Successfully created user") notifications.success("Successfully created user")
dispatch("created")
} catch (error) { } catch (error) {
notifications.error("Error creating user") notifications.error("Error creating user")
} }

View File

@ -17,8 +17,7 @@
import TagsRenderer from "./_components/TagsTableRenderer.svelte" import TagsRenderer from "./_components/TagsTableRenderer.svelte"
import AddUserModal from "./_components/AddUserModal.svelte" import AddUserModal from "./_components/AddUserModal.svelte"
import { users } from "stores/portal" import { users } from "stores/portal"
import { PageInfo } from "helpers/pagination" import { createPaginationStore } from "helpers/pagination"
import { onMount } from "svelte"
const schema = { const schema = {
email: {}, email: {},
@ -27,43 +26,21 @@
group: {}, group: {},
} }
let pageInfo = new PageInfo(fetchUsers) let pageInfo = createPaginationStore()
let search let search = undefined
$: checkRefreshed($users.page) $: page = $pageInfo.page
$: filteredUsers = $users.data $: fetchUsers(page, search)
?.filter(user => user?.email?.includes(search || ""))
.map(user => ({
...user,
group: ["All users"],
developmentAccess: !!user.builder?.global,
adminAccess: !!user.admin?.global,
}))
let createUserModal let createUserModal
async function checkRefreshed(page) { async function fetchUsers(page, search) {
// the users have been reset, go back to first page
if (!page && pageInfo.page) {
pageInfo.reset()
pageInfo.pageNumber = pageInfo.pageNumber
pageInfo.hasNextPage = $users.hasNextPage
pageInfo.nextPage = $users.nextPage
}
}
async function fetchUsers(page) {
try { try {
await users.fetch(page) await users.fetch({ page, search })
pageInfo.hasNextPage = $users.hasNextPage pageInfo.fetched($users.hasNextPage, $users.nextPage)
pageInfo.nextPage = $users.nextPage
} catch (error) { } catch (error) {
notifications.error("Error getting user list") notifications.error("Error getting user list")
} }
} }
onMount(async () => {
await fetchUsers()
})
</script> </script>
<Layout noPadding> <Layout noPadding>
@ -92,7 +69,7 @@
<Table <Table
on:click={({ detail }) => $goto(`./${detail._id}`)} on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema} {schema}
data={filteredUsers || $users.data} data={$users.data}
allowEditColumns={false} allowEditColumns={false}
allowEditRows={false} allowEditRows={false}
allowSelectRows={false} allowSelectRows={false}
@ -100,18 +77,23 @@
/> />
<div class="pagination"> <div class="pagination">
<Pagination <Pagination
page={pageInfo.pageNumber} page={$pageInfo.pageNumber}
hasPrevPage={pageInfo.hasPrevPage} hasPrevPage={$pageInfo.hasPrevPage}
hasNextPage={pageInfo.hasNextPage} hasNextPage={$pageInfo.hasNextPage}
goToPrevPage={() => pageInfo.goToPrevPage()} goToPrevPage={pageInfo.prevPage}
goToNextPage={() => pageInfo.goToNextPage()} goToNextPage={pageInfo.nextPage}
/> />
</div> </div>
</Layout> </Layout>
</Layout> </Layout>
<Modal bind:this={createUserModal}> <Modal bind:this={createUserModal}>
<AddUserModal /> <AddUserModal
on:created={async () => {
pageInfo.reset()
await fetchUsers()
}}
/>
</Modal> </Modal>
<style> <style>

View File

@ -5,11 +5,12 @@ import { update } from "lodash"
export function createUsersStore() { export function createUsersStore() {
const { subscribe, set } = writable({}) const { subscribe, set } = writable({})
async function fetch(page) { // opts can contain page and search params
const paged = await API.getUsers(page) async function fetch(opts = {}) {
const paged = await API.getUsers(opts)
set({ set({
...paged, ...paged,
page, ...opts,
}) })
return paged return paged
} }

View File

@ -1,10 +1,18 @@
export const buildUserEndpoints = API => ({ export const buildUserEndpoints = API => ({
/** /**
* Gets a list of users in the current tenant. * Gets a list of users in the current tenant.
* @param {string} page The page to retrieve
* @param {string} search The starts with string to search username/email by.
*/ */
getUsers: async page => { getUsers: async ({ page, search } = {}) => {
const input = page ? { page } : {} const opts = {}
const params = new URLSearchParams(input) if (page) {
opts.page = page
}
if (search) {
opts.search = search
}
const params = new URLSearchParams(opts)
return await API.get({ return await API.get({
url: `/api/global/users?${params.toString()}`, url: `/api/global/users?${params.toString()}`,
}) })

View File

@ -3,11 +3,22 @@ const {
getAllApps, getAllApps,
getProdAppID, getProdAppID,
DocumentTypes, DocumentTypes,
getGlobalUserParams,
} = require("@budibase/backend-core/db") } = require("@budibase/backend-core/db")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const { user: userCache } = require("@budibase/backend-core/cache") const { user: userCache } = require("@budibase/backend-core/cache")
const { getGlobalDB } = require("@budibase/backend-core/tenancy") const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const { users } = require("../../../sdk")
// TODO: this function needs to be removed and replaced
export const allUsers = async () => {
const db = getGlobalDB()
const response = await db.allDocs(
getGlobalUserParams(null, {
include_docs: true,
})
)
return response.rows.map(row => row.doc)
}
exports.fetch = async ctx => { exports.fetch = async ctx => {
const tenantId = ctx.user.tenantId const tenantId = ctx.user.tenantId
@ -49,10 +60,10 @@ exports.find = async ctx => {
exports.removeAppRole = async ctx => { exports.removeAppRole = async ctx => {
const { appId } = ctx.params const { appId } = ctx.params
const db = getGlobalDB() const db = getGlobalDB()
const allUsers = await users.allUsers(ctx) const users = await allUsers(ctx)
const bulk = [] const bulk = []
const cacheInvalidations = [] const cacheInvalidations = []
for (let user of allUsers) { for (let user of users) {
if (user.roles[appId]) { if (user.roles[appId]) {
cacheInvalidations.push(userCache.invalidateUser(user._id)) cacheInvalidations.push(userCache.invalidateUser(user._id))
delete user.roles[appId] delete user.roles[appId]

View File

@ -90,8 +90,7 @@ export const destroy = async (ctx: any) => {
// called internally by app server user fetch // called internally by app server user fetch
export const fetch = async (ctx: any) => { export const fetch = async (ctx: any) => {
const { page } = ctx.request.query const paginated = await users.paginatedUsers(ctx.request.query)
const paginated = await users.paginatedUsers(page)
// user hashed password shouldn't ever be returned // user hashed password shouldn't ever be returned
for (let user of paginated.data) { for (let user of paginated.data) {
if (user) { if (user) {

View File

@ -17,32 +17,37 @@ import {
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { MigrationType } from "@budibase/types" import { MigrationType } from "@budibase/types"
const PAGE_LIMIT = 10 const PAGE_LIMIT = 8
/** export const paginatedUsers = async ({
* Retrieves all users from the current tenancy. page,
*/ search,
export const allUsers = async () => { }: { page?: string; search?: string } = {}) => {
const db = tenancy.getGlobalDB()
const response = await db.allDocs(
dbUtils.getGlobalUserParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
export const paginatedUsers = async (page?: string) => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
// get one extra document, to have the next page // get one extra document, to have the next page
const response = await db.allDocs( const opts: any = {
dbUtils.getGlobalUserParams(null, { include_docs: true,
include_docs: true, limit: PAGE_LIMIT + 1,
limit: PAGE_LIMIT + 1, }
startkey: page, // add a startkey if the page was specified (anchor)
}) if (page) {
) opts.startkey = page
return dbUtils.pagination(response, PAGE_LIMIT) }
// property specifies what to use for the page/anchor
let userList, property
// no search, query allDocs
if (!search) {
const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
userList = response.rows.map((row: any) => row.doc)
property = "_id"
} else {
userList = await usersCore.searchGlobalUsersByEmail(search, opts)
property = "email"
}
return dbUtils.pagination(userList, PAGE_LIMIT, {
paginate: true,
property,
})
} }
/** /**