diff --git a/packages/backend-core/src/db/constants.js b/packages/backend-core/src/db/constants.js index 10c6e174d7..12626fb90e 100644 --- a/packages/backend-core/src/db/constants.js +++ b/packages/backend-core/src/db/constants.js @@ -1,4 +1,5 @@ exports.SEPARATOR = "_" +exports.UNICODE_MAX = "\ufff0" const PRE_APP = "app" const PRE_DEV = "dev" diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index dc7a0454c3..54af4fc7a2 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -1,7 +1,7 @@ import { newid } from "../hashing" import { DEFAULT_TENANT_ID, Configs } from "../constants" import env from "../environment" -import { SEPARATOR, DocumentTypes } from "./constants" +import { SEPARATOR, DocumentTypes, UNICODE_MAX } from "./constants" import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy" import fetch from "node-fetch" import { doWithDB, allDbs } from "./index" @@ -12,8 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" -const UNICODE_MAX = "\ufff0" - export const ViewNames = { USER_BY_EMAIL: "by_email", BY_API_KEY: "by_api_key", @@ -93,13 +91,17 @@ export function generateGlobalUserID(id?: any) { /** * Gets parameters for retrieving users. */ -export function getGlobalUserParams(globalId: any, otherProps = {}) { +export function getGlobalUserParams(globalId: any, otherProps: any = {}) { if (!globalId) { globalId = "" } + const startkey = otherProps?.startkey return { ...otherProps, - startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`, + // need to include this incase pagination + startkey: startkey + ? startkey + : `${DocumentTypes.USER}${SEPARATOR}${globalId}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, } } @@ -434,6 +436,26 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => { return platformUrl } +export function pagination( + data: any[], + pageSize: number, + { paginate, property } = { paginate: true, property: "_id" } +) { + if (!paginate) { + return { data, hasNextPage: false } + } + const hasNextPage = data.length > pageSize + let nextPage = undefined + if (hasNextPage) { + nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id + } + return { + data: data.slice(0, pageSize), + hasNextPage, + nextPage, + } +} + export async function getScopedConfig(db: any, params: any) { const configDoc = await getScopedFullConfig(db, params) return configDoc && configDoc.config ? configDoc.config : configDoc diff --git a/packages/backend-core/src/users.js b/packages/backend-core/src/users.js index 4acccda2a0..0c1350a674 100644 --- a/packages/backend-core/src/users.js +++ b/packages/backend-core/src/users.js @@ -1,5 +1,6 @@ const { ViewNames } = require("./db/utils") const { queryGlobalView } = require("./db/views") +const { UNICODE_MAX } = require("./db/constants") /** * Given an email address this will use a view to search through @@ -19,3 +20,24 @@ exports.getGlobalUserByEmail = async email => { 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] +} diff --git a/packages/builder/src/helpers/pagination.js b/packages/builder/src/helpers/pagination.js new file mode 100644 index 0000000000..122973f1a1 --- /dev/null +++ b/packages/builder/src/helpers/pagination.js @@ -0,0 +1,66 @@ +import { writable } from "svelte/store" + +function defaultValue() { + return { + nextPage: null, + page: undefined, + hasPrevPage: false, + hasNextPage: false, + loading: false, + pageNumber: 1, + pages: [], + } +} + +export function createPaginationStore() { + const { subscribe, set, update } = writable(defaultValue()) + + function prevPage() { + update(state => { + state.pageNumber-- + state.nextPage = state.pages.pop() + state.page = state.pages[state.pages.length - 1] + state.hasPrevPage = state.pageNumber > 1 + return state + }) + } + + 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 + state.loading = false + return state + }) + } + + function loading(loading = true) { + update(state => { + state.loading = loading + return state + }) + } + + function reset() { + set(defaultValue()) + } + + return { + subscribe, + prevPage, + nextPage, + fetched, + loading, + reset, + } +} diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte index 5583a48b7d..88a8fb6c5d 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte @@ -10,7 +10,9 @@ } from "@budibase/bbui" import { createValidationStore, emailValidator } from "helpers/validation" import { users } from "stores/portal" + import { createEventDispatcher } from "svelte" + const dispatch = createEventDispatcher() const password = Math.random().toString(36).substring(2, 22) const options = ["Email onboarding", "Basic onboarding"] const [email, error, touched] = createValidationStore("", emailValidator) @@ -39,6 +41,7 @@ forceResetPassword: true, }) notifications.success("Successfully created user") + dispatch("created") } catch (error) { notifications.error("Error creating user") } diff --git a/packages/builder/src/pages/builder/portal/manage/users/index.svelte b/packages/builder/src/pages/builder/portal/manage/users/index.svelte index 1d9c245480..0da8c1345a 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/index.svelte @@ -12,41 +12,36 @@ Layout, Modal, notifications, + Pagination, } from "@budibase/bbui" import TagsRenderer from "./_components/TagsTableRenderer.svelte" import AddUserModal from "./_components/AddUserModal.svelte" import { users } from "stores/portal" - import { onMount } from "svelte" + import { createPaginationStore } from "helpers/pagination" const schema = { email: {}, developmentAccess: { displayName: "Development Access", type: "boolean" }, adminAccess: { displayName: "Admin Access", type: "boolean" }, - // role: { type: "options" }, group: {}, - // access: {}, - // group: {} } - let search - $: filteredUsers = $users - .filter(user => user.email.includes(search || "")) - .map(user => ({ - ...user, - group: ["All users"], - developmentAccess: !!user.builder?.global, - adminAccess: !!user.admin?.global, - })) + let pageInfo = createPaginationStore() + let search = undefined + $: page = $pageInfo.page + $: fetchUsers(page, search) let createUserModal - onMount(async () => { + async function fetchUsers(page, search) { try { - await users.init() + pageInfo.loading() + await users.search({ page, search }) + pageInfo.fetched($users.hasNextPage, $users.nextPage) } catch (error) { notifications.error("Error getting user list") } - }) + } @@ -75,17 +70,31 @@ $goto(`./${detail._id}`)} {schema} - data={filteredUsers || $users} + data={$users.data} allowEditColumns={false} allowEditRows={false} allowSelectRows={false} customRenderers={[{ column: "group", component: TagsRenderer }]} /> + - + { + pageInfo.reset() + await fetchUsers() + }} + />