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:
parent
63646b0c38
commit
062d834950
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
|
@ -1,31 +1,56 @@
|
||||||
export class PageInfo {
|
import { writable } from "svelte/store"
|
||||||
constructor(fetch) {
|
|
||||||
this.reset()
|
function defaultValue() {
|
||||||
this.fetch = fetch
|
return {
|
||||||
|
nextPage: null,
|
||||||
|
page: undefined,
|
||||||
|
hasPrevPage: false,
|
||||||
|
hasNextPage: false,
|
||||||
|
pageNumber: 1,
|
||||||
|
pages: [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async goToNextPage() {
|
export function createPaginationStore() {
|
||||||
this.pageNumber++
|
const { subscribe, set, update } = writable(defaultValue())
|
||||||
this.prevPage = this.page
|
|
||||||
this.page = this.nextPage
|
function prevPage() {
|
||||||
this.hasPrevPage = this.pageNumber > 1
|
update(state => {
|
||||||
await this.fetch(this.page)
|
state.pageNumber--
|
||||||
|
state.nextPage = state.pages.pop()
|
||||||
|
state.page = state.pages[state.pages.length - 1]
|
||||||
|
state.hasPrevPage = state.pageNumber > 1
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async goToPrevPage() {
|
function nextPage() {
|
||||||
this.pageNumber--
|
update(state => {
|
||||||
this.nextPage = this.page
|
state.pageNumber++
|
||||||
this.page = this.prevPage
|
state.page = state.nextPage
|
||||||
this.hasPrevPage = this.pageNumber > 1
|
state.pages.push(state.page)
|
||||||
await this.fetch(this.page)
|
state.hasPrevPage = state.pageNumber > 1
|
||||||
|
return state
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
function fetched(hasNextPage, nextPage) {
|
||||||
this.prevPage = null
|
update(state => {
|
||||||
this.nextPage = null
|
state.hasNextPage = hasNextPage
|
||||||
this.page = undefined
|
state.nextPage = nextPage
|
||||||
this.hasPrevPage = false
|
return state
|
||||||
this.hasNextPage = false
|
})
|
||||||
this.pageNumber = 1
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
set(defaultValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
prevPage,
|
||||||
|
nextPage,
|
||||||
|
fetched,
|
||||||
|
reset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()}`,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
// 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,
|
||||||
})
|
})
|
||||||
)
|
|
||||||
return dbUtils.pagination(response, PAGE_LIMIT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue