Adding user pagination, removing usages of the global user list from builder and replacing with direct user lookups where possible, still need to apply filtering to username/email serverside.
This commit is contained in:
parent
587b3a25c3
commit
98c486655b
|
@ -93,13 +93,17 @@ export function generateGlobalUserID(id?: any) {
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving users.
|
* Gets parameters for retrieving users.
|
||||||
*/
|
*/
|
||||||
export function getGlobalUserParams(globalId: any, otherProps = {}) {
|
export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
|
||||||
if (!globalId) {
|
if (!globalId) {
|
||||||
globalId = ""
|
globalId = ""
|
||||||
}
|
}
|
||||||
|
const startkey = otherProps?.startkey
|
||||||
return {
|
return {
|
||||||
...otherProps,
|
...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}`,
|
endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,6 +438,25 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => {
|
||||||
return platformUrl
|
return platformUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pagination(
|
||||||
|
response: any,
|
||||||
|
pageSize: number,
|
||||||
|
paginate: boolean = true
|
||||||
|
) {
|
||||||
|
const data = response.rows.map((row: any) => {
|
||||||
|
return row.doc ? row.doc : row
|
||||||
|
})
|
||||||
|
if (!paginate) {
|
||||||
|
return { data, hasNextPage: false }
|
||||||
|
}
|
||||||
|
const hasNextPage = data.length > pageSize
|
||||||
|
return {
|
||||||
|
data: data.slice(0, pageSize),
|
||||||
|
hasNextPage,
|
||||||
|
nextPage: hasNextPage ? data[pageSize]?._id : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getScopedConfig(db: any, params: any) {
|
export async function getScopedConfig(db: any, params: any) {
|
||||||
const configDoc = await getScopedFullConfig(db, params)
|
const configDoc = await getScopedFullConfig(db, params)
|
||||||
return configDoc && configDoc.config ? configDoc.config : configDoc
|
return configDoc && configDoc.config ? configDoc.config : configDoc
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
export class PageInfo {
|
||||||
|
constructor(fetch) {
|
||||||
|
this.reset()
|
||||||
|
this.fetch = fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToNextPage() {
|
||||||
|
this.pageNumber++
|
||||||
|
this.prevPage = this.page
|
||||||
|
this.page = this.nextPage
|
||||||
|
this.hasPrevPage = this.pageNumber > 1
|
||||||
|
await this.fetch(this.page)
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToPrevPage() {
|
||||||
|
this.pageNumber--
|
||||||
|
this.nextPage = this.page
|
||||||
|
this.page = this.prevPage
|
||||||
|
this.hasPrevPage = this.pageNumber > 1
|
||||||
|
await this.fetch(this.page)
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.prevPage = null
|
||||||
|
this.nextPage = null
|
||||||
|
this.page = undefined
|
||||||
|
this.hasPrevPage = false
|
||||||
|
this.hasNextPage = false
|
||||||
|
this.pageNumber = 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,25 +12,26 @@
|
||||||
Layout,
|
Layout,
|
||||||
Modal,
|
Modal,
|
||||||
notifications,
|
notifications,
|
||||||
|
Pagination,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
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 { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
email: {},
|
email: {},
|
||||||
developmentAccess: { displayName: "Development Access", type: "boolean" },
|
developmentAccess: { displayName: "Development Access", type: "boolean" },
|
||||||
adminAccess: { displayName: "Admin Access", type: "boolean" },
|
adminAccess: { displayName: "Admin Access", type: "boolean" },
|
||||||
// role: { type: "options" },
|
|
||||||
group: {},
|
group: {},
|
||||||
// access: {},
|
|
||||||
// group: {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pageInfo = new PageInfo(fetchUsers)
|
||||||
let search
|
let search
|
||||||
$: filteredUsers = $users
|
$: checkRefreshed($users.page)
|
||||||
.filter(user => user.email.includes(search || ""))
|
$: filteredUsers = $users.data
|
||||||
|
?.filter(user => user?.email?.includes(search || ""))
|
||||||
.map(user => ({
|
.map(user => ({
|
||||||
...user,
|
...user,
|
||||||
group: ["All users"],
|
group: ["All users"],
|
||||||
|
@ -40,12 +41,28 @@
|
||||||
|
|
||||||
let createUserModal
|
let createUserModal
|
||||||
|
|
||||||
onMount(async () => {
|
async function checkRefreshed(page) {
|
||||||
|
// 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.init()
|
await users.fetch(page)
|
||||||
|
pageInfo.hasNextPage = $users.hasNextPage
|
||||||
|
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>
|
||||||
|
|
||||||
|
@ -75,12 +92,21 @@
|
||||||
<Table
|
<Table
|
||||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||||
{schema}
|
{schema}
|
||||||
data={filteredUsers || $users}
|
data={filteredUsers || $users.data}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
customRenderers={[{ column: "group", component: TagsRenderer }]}
|
customRenderers={[{ column: "group", component: TagsRenderer }]}
|
||||||
/>
|
/>
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={pageInfo.pageNumber}
|
||||||
|
hasPrevPage={pageInfo.hasPrevPage}
|
||||||
|
hasNextPage={pageInfo.hasNextPage}
|
||||||
|
goToPrevPage={() => pageInfo.goToPrevPage()}
|
||||||
|
goToNextPage={() => pageInfo.goToNextPage()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import DashCard from "components/common/DashCard.svelte"
|
import DashCard from "components/common/DashCard.svelte"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import {
|
import { Icon, Heading, Link, Avatar, Layout } from "@budibase/bbui"
|
||||||
Icon,
|
|
||||||
Heading,
|
|
||||||
Link,
|
|
||||||
Avatar,
|
|
||||||
notifications,
|
|
||||||
Layout,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import clientPackage from "@budibase/client/package.json"
|
import clientPackage from "@budibase/client/package.json"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
@ -20,29 +13,22 @@
|
||||||
export let navigateTab
|
export let navigateTab
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const userInit = async () => {
|
|
||||||
try {
|
|
||||||
await users.init()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error getting user list")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unpublishApp = () => {
|
const unpublishApp = () => {
|
||||||
dispatch("unpublish", app)
|
dispatch("unpublish", app)
|
||||||
}
|
}
|
||||||
|
|
||||||
let userPromise = userInit()
|
let appEditor, appEditorPromise
|
||||||
|
|
||||||
$: updateAvailable = clientPackage.version !== $store.version
|
$: updateAvailable = clientPackage.version !== $store.version
|
||||||
$: isPublished = app && app?.status === AppStatus.DEPLOYED
|
$: isPublished = app && app?.status === AppStatus.DEPLOYED
|
||||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||||
$: filteredUsers = !appEditorId
|
$: fetchAppEditor(appEditorId)
|
||||||
? []
|
|
||||||
: $users.filter(user => user._id === appEditorId)
|
|
||||||
|
|
||||||
$: appEditor = filteredUsers.length ? filteredUsers[0] : null
|
async function fetchAppEditor(editorId) {
|
||||||
|
appEditorPromise = users.get(editorId)
|
||||||
|
appEditor = await appEditorPromise
|
||||||
|
}
|
||||||
|
|
||||||
const getInitials = user => {
|
const getInitials = user => {
|
||||||
let initials = ""
|
let initials = ""
|
||||||
|
@ -90,7 +76,7 @@
|
||||||
</DashCard>
|
</DashCard>
|
||||||
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
|
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
|
||||||
<div class="last-edited-content">
|
<div class="last-edited-content">
|
||||||
{#await userPromise}
|
{#await appEditorPromise}
|
||||||
<Avatar size="M" initials={"-"} />
|
<Avatar size="M" initials={"-"} />
|
||||||
{:then _}
|
{:then _}
|
||||||
<div class="updated-by">
|
<div class="updated-by">
|
||||||
|
|
|
@ -3,11 +3,23 @@ import { API } from "api"
|
||||||
import { update } from "lodash"
|
import { update } from "lodash"
|
||||||
|
|
||||||
export function createUsersStore() {
|
export function createUsersStore() {
|
||||||
const { subscribe, set } = writable([])
|
const { subscribe, set } = writable({})
|
||||||
|
|
||||||
async function init() {
|
async function fetch(page) {
|
||||||
const users = await API.getUsers()
|
const paged = await API.getUsers(page)
|
||||||
set(users)
|
set({
|
||||||
|
...paged,
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
return paged
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(userId) {
|
||||||
|
try {
|
||||||
|
return await API.getUser(userId)
|
||||||
|
} catch (err) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invite({ email, builder, admin }) {
|
async function invite({ email, builder, admin }) {
|
||||||
|
@ -47,7 +59,8 @@ export function createUsersStore() {
|
||||||
body.admin = { global: true }
|
body.admin = { global: true }
|
||||||
}
|
}
|
||||||
await API.saveUser(body)
|
await API.saveUser(body)
|
||||||
await init()
|
// re-fetch from first page
|
||||||
|
await fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function del(id) {
|
async function del(id) {
|
||||||
|
@ -61,7 +74,8 @@ export function createUsersStore() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
init,
|
fetch,
|
||||||
|
get,
|
||||||
invite,
|
invite,
|
||||||
acceptInvite,
|
acceptInvite,
|
||||||
create,
|
create,
|
||||||
|
|
|
@ -2,9 +2,20 @@ export const buildUserEndpoints = API => ({
|
||||||
/**
|
/**
|
||||||
* Gets a list of users in the current tenant.
|
* Gets a list of users in the current tenant.
|
||||||
*/
|
*/
|
||||||
getUsers: async () => {
|
getUsers: async page => {
|
||||||
|
const input = page ? { page } : {}
|
||||||
|
const params = new URLSearchParams(input)
|
||||||
return await API.get({
|
return await API.get({
|
||||||
url: "/api/global/users",
|
url: `/api/global/users?${params.toString()}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single user by ID.
|
||||||
|
*/
|
||||||
|
getUser: async userId => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/global/users/${userId}`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -98,14 +98,15 @@ 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 all = await users.allUsers()
|
const { page } = 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 all) {
|
for (let user of paginated.data) {
|
||||||
if (user) {
|
if (user) {
|
||||||
delete user.password
|
delete user.password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.body = all
|
ctx.body = paginated
|
||||||
}
|
}
|
||||||
|
|
||||||
// called internally by app server user find
|
// called internally by app server user find
|
||||||
|
|
|
@ -17,6 +17,8 @@ import {
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { MigrationType } from "@budibase/types"
|
import { MigrationType } from "@budibase/types"
|
||||||
|
|
||||||
|
const PAGE_LIMIT = 10
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all users from the current tenancy.
|
* Retrieves all users from the current tenancy.
|
||||||
*/
|
*/
|
||||||
|
@ -30,6 +32,19 @@ export const allUsers = async () => {
|
||||||
return response.rows.map((row: any) => row.doc)
|
return response.rows.map((row: any) => row.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const paginatedUsers = async (page?: string) => {
|
||||||
|
const db = tenancy.getGlobalDB()
|
||||||
|
// get one extra document, to have the next page
|
||||||
|
const response = await db.allDocs(
|
||||||
|
dbUtils.getGlobalUserParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
limit: PAGE_LIMIT + 1,
|
||||||
|
startkey: page,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return dbUtils.pagination(response, PAGE_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a user by ID from the global database, based on the current tenancy.
|
* Gets a user by ID from the global database, based on the current tenancy.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue