commit
d92f56c7d4
|
@ -20,6 +20,7 @@ export enum ViewName {
|
|||
AUTOMATION_LOGS = "automation_logs",
|
||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||
USER_BY_GROUP = "by_group_user",
|
||||
}
|
||||
|
||||
export const DeprecatedViews = {
|
||||
|
|
|
@ -36,154 +36,91 @@ async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) {
|
|||
}
|
||||
}
|
||||
|
||||
export const createNewUserEmailView = async () => {
|
||||
const db = getGlobalDB()
|
||||
export async function createView(db: any, viewJs: string, viewName: string) {
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get(DESIGN_DB)
|
||||
designDoc = (await db.get(DESIGN_DB)) as DesignDocument
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) {
|
||||
emit(doc.email.toLowerCase(), doc._id)
|
||||
}
|
||||
}`,
|
||||
map: viewJs,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewName.USER_BY_EMAIL]: view,
|
||||
[viewName]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
||||
export const createNewUserEmailView = async () => {
|
||||
const db = getGlobalDB()
|
||||
const viewJs = `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) {
|
||||
emit(doc.email.toLowerCase(), doc._id)
|
||||
}
|
||||
}`
|
||||
await createView(db, viewJs, ViewName.USER_BY_EMAIL)
|
||||
}
|
||||
|
||||
export const createAccountEmailView = async () => {
|
||||
const viewJs = `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
|
||||
emit(doc.email.toLowerCase(), doc._id)
|
||||
}
|
||||
}`
|
||||
await doWithDB(
|
||||
StaticDatabases.PLATFORM_INFO.name,
|
||||
async (db: PouchDB.Database) => {
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get<DesignDocument>(DESIGN_DB)
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
|
||||
emit(doc.email.toLowerCase(), doc._id)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewName.ACCOUNT_BY_EMAIL]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const createUserAppView = async () => {
|
||||
const db = getGlobalDB() as PouchDB.Database
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get<DesignDocument>("_design/database")
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
|
||||
for (let prodAppId of Object.keys(doc.roles)) {
|
||||
let emitted = prodAppId + "${SEPARATOR}" + doc._id
|
||||
emit(emitted, null)
|
||||
}
|
||||
const viewJs = `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
|
||||
for (let prodAppId of Object.keys(doc.roles)) {
|
||||
let emitted = prodAppId + "${SEPARATOR}" + doc._id
|
||||
emit(emitted, null)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewName.USER_BY_APP]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
}`
|
||||
await createView(db, viewJs, ViewName.USER_BY_APP)
|
||||
}
|
||||
|
||||
export const createApiKeyView = async () => {
|
||||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
} catch (err) {
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) {
|
||||
emit(doc.apiKey, doc.userId)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewName.BY_API_KEY]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
const viewJs = `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) {
|
||||
emit(doc.apiKey, doc.userId)
|
||||
}
|
||||
}`
|
||||
await createView(db, viewJs, ViewName.BY_API_KEY)
|
||||
}
|
||||
|
||||
export const createUserBuildersView = async () => {
|
||||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
map: `function(doc) {
|
||||
if (doc.builder && doc.builder.global === true) {
|
||||
emit(doc._id, doc._id)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewName.USER_BY_BUILDERS]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
const viewJs = `function(doc) {
|
||||
if (doc.builder && doc.builder.global === true) {
|
||||
emit(doc._id, doc._id)
|
||||
}
|
||||
}`
|
||||
await createView(db, viewJs, ViewName.USER_BY_BUILDERS)
|
||||
}
|
||||
|
||||
export const createPlatformUserView = async () => {
|
||||
const viewJs = `function(doc) {
|
||||
if (doc.tenantId) {
|
||||
emit(doc._id.toLowerCase(), doc._id)
|
||||
}
|
||||
}`
|
||||
await doWithDB(
|
||||
StaticDatabases.PLATFORM_INFO.name,
|
||||
async (db: PouchDB.Database) => {
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get<DesignDocument>(DESIGN_DB)
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc.tenantId) {
|
||||
emit(doc._id.toLowerCase(), doc._id)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewName.PLATFORM_USERS_LOWERCASE]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -196,7 +133,7 @@ export const queryView = async <T>(
|
|||
viewName: ViewName,
|
||||
params: PouchDB.Query.Options<T, T>,
|
||||
db: PouchDB.Database,
|
||||
CreateFuncByName: any,
|
||||
createFunc: any,
|
||||
opts?: QueryViewOptions
|
||||
): Promise<T[] | T | undefined> => {
|
||||
try {
|
||||
|
@ -213,10 +150,9 @@ export const queryView = async <T>(
|
|||
}
|
||||
} catch (err: any) {
|
||||
if (err != null && err.name === "not_found") {
|
||||
const createFunc = CreateFuncByName[viewName]
|
||||
await removeDeprecated(db, viewName)
|
||||
await createFunc()
|
||||
return queryView(viewName, params, db, CreateFuncByName, opts)
|
||||
return queryView(viewName, params, db, createFunc, opts)
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
|
@ -228,7 +164,7 @@ export const queryPlatformView = async <T>(
|
|||
params: PouchDB.Query.Options<T, T>,
|
||||
opts?: QueryViewOptions
|
||||
): Promise<T[] | T | undefined> => {
|
||||
const CreateFuncByName = {
|
||||
const CreateFuncByName: any = {
|
||||
[ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView,
|
||||
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
|
||||
}
|
||||
|
@ -236,7 +172,8 @@ export const queryPlatformView = async <T>(
|
|||
return doWithDB(
|
||||
StaticDatabases.PLATFORM_INFO.name,
|
||||
async (db: PouchDB.Database) => {
|
||||
return queryView(viewName, params, db, CreateFuncByName, opts)
|
||||
const createFn = CreateFuncByName[viewName]
|
||||
return queryView(viewName, params, db, createFn, opts)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -247,7 +184,7 @@ export const queryGlobalView = async <T>(
|
|||
db?: PouchDB.Database,
|
||||
opts?: QueryViewOptions
|
||||
): Promise<T[] | T | undefined> => {
|
||||
const CreateFuncByName = {
|
||||
const CreateFuncByName: any = {
|
||||
[ViewName.USER_BY_EMAIL]: createNewUserEmailView,
|
||||
[ViewName.BY_API_KEY]: createApiKeyView,
|
||||
[ViewName.USER_BY_BUILDERS]: createUserBuildersView,
|
||||
|
@ -257,5 +194,6 @@ export const queryGlobalView = async <T>(
|
|||
if (!db) {
|
||||
db = getGlobalDB() as PouchDB.Database
|
||||
}
|
||||
return queryView(viewName, params, db, CreateFuncByName, opts)
|
||||
const createFn = CreateFuncByName[viewName]
|
||||
return queryView(viewName, params, db, createFn, opts)
|
||||
}
|
||||
|
|
|
@ -40,9 +40,9 @@ export async function usersAdded(count: number, group: UserGroup) {
|
|||
await publishEvent(Event.USER_GROUP_USERS_ADDED, properties)
|
||||
}
|
||||
|
||||
export async function usersDeleted(emails: string[], group: UserGroup) {
|
||||
export async function usersDeleted(count: number, group: UserGroup) {
|
||||
const properties: GroupUsersDeletedEvent = {
|
||||
count: emails.length,
|
||||
count,
|
||||
groupId: group._id as string,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties)
|
||||
|
|
|
@ -53,7 +53,7 @@ exports.getTenantFeatureFlags = tenantId => {
|
|||
return flags
|
||||
}
|
||||
|
||||
exports.FeatureFlag = {
|
||||
exports.TenantFeatureFlag = {
|
||||
LICENSING: "LICENSING",
|
||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||
USER_GROUPS: "USER_GROUPS",
|
||||
|
|
|
@ -18,6 +18,7 @@ import * as logging from "./logging"
|
|||
import pino from "./pino"
|
||||
import * as middleware from "./middleware"
|
||||
import plugins from "./plugin"
|
||||
import encryption from "./security/encryption"
|
||||
|
||||
// mimic the outer package exports
|
||||
import * as db from "./pkg/db"
|
||||
|
@ -60,6 +61,7 @@ const core = {
|
|||
...pino,
|
||||
...errorClasses,
|
||||
middleware,
|
||||
encryption,
|
||||
}
|
||||
|
||||
export = core
|
||||
|
|
|
@ -78,7 +78,7 @@ function isBuiltin(role) {
|
|||
*/
|
||||
exports.builtinRoleToNumber = id => {
|
||||
const builtins = exports.getBuiltinRoles()
|
||||
const MAX = Object.values(BUILTIN_IDS).length + 1
|
||||
const MAX = Object.values(builtins).length + 1
|
||||
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
|
||||
return MAX
|
||||
}
|
||||
|
@ -94,6 +94,22 @@ exports.builtinRoleToNumber = id => {
|
|||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts any role to a number, but has to be async to get the roles from db.
|
||||
*/
|
||||
exports.roleToNumber = async id => {
|
||||
if (exports.isBuiltin(id)) {
|
||||
return exports.builtinRoleToNumber(id)
|
||||
}
|
||||
const hierarchy = await exports.getUserRoleHierarchy(id)
|
||||
for (let role of hierarchy) {
|
||||
if (isBuiltin(role.inherits)) {
|
||||
return exports.builtinRoleToNumber(role.inherits) + 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whichever builtin roleID is lower.
|
||||
*/
|
||||
|
@ -172,7 +188,7 @@ async function getAllUserRoles(userRoleId) {
|
|||
* to determine if a user can access something that requires a specific role.
|
||||
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
||||
* @param {object} opts Various options, such as whether to only retrieve the IDs (default true).
|
||||
* @returns {Promise<string[]>} returns an ordered array of the roles, with the first being their
|
||||
* @returns {Promise<string[]|object[]>} returns an ordered array of the roles, with the first being their
|
||||
* highest level of access and the last being the lowest level.
|
||||
*/
|
||||
exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => {
|
||||
|
|
|
@ -121,7 +121,7 @@ export const getTenantUser = async (
|
|||
return response
|
||||
}
|
||||
|
||||
export const isUserInAppTenant = (appId: string, user: any) => {
|
||||
export const isUserInAppTenant = (appId: string, user?: any) => {
|
||||
let userTenantId
|
||||
if (user) {
|
||||
userTenantId = user.tenantId || DEFAULT_TENANT_ID
|
||||
|
|
|
@ -6,7 +6,24 @@ import {
|
|||
} from "./db/utils"
|
||||
import { queryGlobalView } from "./db/views"
|
||||
import { UNICODE_MAX } from "./db/constants"
|
||||
import { User } from "@budibase/types"
|
||||
import { BulkDocsResponse, User } from "@budibase/types"
|
||||
import { getGlobalDB } from "./context"
|
||||
import PouchDB from "pouchdb"
|
||||
|
||||
export const bulkGetGlobalUsersById = async (userIds: string[]) => {
|
||||
const db = getGlobalDB() as PouchDB.Database
|
||||
return (
|
||||
await db.allDocs({
|
||||
keys: userIds,
|
||||
include_docs: true,
|
||||
})
|
||||
).rows.map(row => row.doc) as User[]
|
||||
}
|
||||
|
||||
export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
||||
const db = getGlobalDB() as PouchDB.Database
|
||||
return (await db.bulkDocs(users)) as BulkDocsResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an email address this will use a view to search through
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||
import Detail from "../../Typography/Detail.svelte"
|
||||
import Search from "./Search.svelte"
|
||||
import IconAvatar from "../../Icon/IconAvatar.svelte"
|
||||
|
||||
export let primaryLabel = ""
|
||||
export let primaryValue = null
|
||||
export let id = null
|
||||
export let placeholder = "Choose an option or type"
|
||||
export let disabled = false
|
||||
export let updateOnChange = true
|
||||
export let error = null
|
||||
export let secondaryOptions = []
|
||||
export let primaryOptions = []
|
||||
|
@ -204,19 +204,11 @@
|
|||
})}
|
||||
>
|
||||
{#if primaryOptions[title].getIcon(option)}
|
||||
<div
|
||||
style="background: {primaryOptions[title].getColour(
|
||||
option
|
||||
)};"
|
||||
class="circle"
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
size="S"
|
||||
name={primaryOptions[title].getIcon(option)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<IconAvatar
|
||||
size="S"
|
||||
icon={primaryOptions[title].getIcon(option)}
|
||||
background={primaryOptions[title].getColour(option)}
|
||||
/>
|
||||
{:else if getPrimaryOptionColour(option, idx)}
|
||||
<span class="option-left">
|
||||
<StatusLight
|
||||
|
@ -226,12 +218,13 @@
|
|||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
<span
|
||||
<div
|
||||
class="primary-text"
|
||||
class:spacing-group={primaryOptions[title].getIcon(option)}
|
||||
>
|
||||
{primaryOptions[title].getLabel(option)}
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
|
@ -335,6 +328,11 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.primary-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.spacing-group {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
@ -367,25 +365,6 @@
|
|||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.circle {
|
||||
border-radius: 50%;
|
||||
height: 28px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
line-height: 48px;
|
||||
font-size: 1.2em;
|
||||
width: 28px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.circle > div {
|
||||
position: absolute;
|
||||
text-decoration: none;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
.iconPadding {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let error = null
|
||||
export let updateOnChange = true
|
||||
export let getSecondaryOptionLabel = option =>
|
||||
extractProperty(option, "label")
|
||||
export let getSecondaryOptionValue = option =>
|
||||
|
@ -100,7 +99,6 @@
|
|||
{searchTerm}
|
||||
{autocomplete}
|
||||
{dataCy}
|
||||
{updateOnChange}
|
||||
{error}
|
||||
{disabled}
|
||||
{readonly}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
import Icon from "./Icon.svelte"
|
||||
|
||||
export let icon
|
||||
export let background
|
||||
export let color
|
||||
export let size = "M"
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="icon size--{size}"
|
||||
style="background: {background || `transparent`};"
|
||||
class:filled={!!background}
|
||||
>
|
||||
<Icon name={icon} color={background ? "white" : color} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.icon :global(.spectrum-Icon) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.icon.filled :global(.spectrum-Icon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.icon.size--S {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.icon.size--S :global(.spectrum-Icon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.icon.size--S.filled :global(.spectrum-Icon) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.icon.size--L {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.icon.size--L :global(.spectrum-Icon) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.icon.size--L.filled :global(.spectrum-Icon) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
</style>
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import Body from "../Typography/Body.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import IconAvatar from "../Icon/IconAvatar.svelte"
|
||||
import Label from "../Label/Label.svelte"
|
||||
import Avatar from "../Avatar/Avatar.svelte"
|
||||
|
||||
export let icon = null
|
||||
export let iconBackground = null
|
||||
export let iconColor = null
|
||||
export let avatar = false
|
||||
export let title = null
|
||||
export let subtitle = null
|
||||
|
@ -17,9 +18,7 @@
|
|||
<div class="list-item" class:hoverable on:click>
|
||||
<div class="left">
|
||||
{#if icon}
|
||||
<div class="icon" style="background: {iconBackground || `transparent`};">
|
||||
<Icon name={icon} size="S" color={iconBackground ? "white" : null} />
|
||||
</div>
|
||||
<IconAvatar {icon} color={iconColor} background={iconBackground} />
|
||||
{/if}
|
||||
{#if avatar}
|
||||
<Avatar {initials} />
|
||||
|
@ -88,11 +87,4 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.icon {
|
||||
width: var(--spectrum-alias-avatar-size-400);
|
||||
height: var(--spectrum-alias-avatar-size-400);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
{/if}
|
||||
</h1>
|
||||
{#if showDivider}
|
||||
<Divider size="M" />
|
||||
<Divider />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<style>
|
||||
.spectrum-Popover {
|
||||
min-width: var(--spectrum-global-dimension-size-2000);
|
||||
border-color: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
||||
margin-top: var(--spacing-xs);
|
||||
|
|
|
@ -20,6 +20,7 @@ export { default as Button } from "./Button/Button.svelte"
|
|||
export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.svelte"
|
||||
export { default as ClearButton } from "./ClearButton/ClearButton.svelte"
|
||||
export { default as Icon, directions } from "./Icon/Icon.svelte"
|
||||
export { default as IconAvatar } from "./Icon/IconAvatar.svelte"
|
||||
export { default as Toggle } from "./Form/Toggle.svelte"
|
||||
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
||||
|
|
|
@ -402,8 +402,8 @@ Cypress.Commands.add("searchForApplication", appName => {
|
|||
// Searches for the app
|
||||
cy.get(".filter").then(() => {
|
||||
cy.get(".spectrum-Textfield").within(() => {
|
||||
cy.get("input").eq(0).clear()
|
||||
cy.get("input").eq(0).type(appName)
|
||||
cy.get("input").eq(0).clear({ force: true })
|
||||
cy.get("input").eq(0).type(appName, { force: true })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
</Body>
|
||||
</ConfirmDialog>
|
||||
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Tables</Heading>
|
||||
<div class="table-buttons">
|
||||
|
@ -209,7 +209,7 @@
|
|||
{:else}
|
||||
<Body size="S"><i>No tables found.</i></Body>
|
||||
{/if}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Relationships</Heading>
|
||||
<Button primary on:click={() => openRelationshipModal()}>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Headers</Heading>
|
||||
|
@ -61,7 +61,7 @@
|
|||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Authentication</Heading>
|
||||
|
@ -73,7 +73,7 @@
|
|||
</Body>
|
||||
<RestAuthenticationBuilder bind:configs={datasource.config.authConfigs} />
|
||||
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Variables</Heading>
|
||||
|
|
|
@ -30,13 +30,14 @@
|
|||
background: var(--spectrum-alias-background-color-primary);
|
||||
border-radius: var(--border-radius-s);
|
||||
overflow: hidden;
|
||||
min-height: 150px;
|
||||
min-height: 170px;
|
||||
}
|
||||
.dash-card-header {
|
||||
padding: var(--spacing-xl) var(--spectrum-global-dimension-static-size-400);
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: background-color 130ms ease-out;
|
||||
}
|
||||
.dash-card-body {
|
||||
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2);
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { roles } from "stores/backend"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { Constants, RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
export let value
|
||||
export let error
|
||||
export let placeholder = null
|
||||
export let autoWidth = false
|
||||
export let quiet = false
|
||||
export let allowPublic = true
|
||||
|
||||
$: options = getOptions($roles, allowPublic)
|
||||
|
||||
const getOptions = (roles, allowPublic) => {
|
||||
if (allowPublic) {
|
||||
return roles
|
||||
}
|
||||
return roles.filter(role => role._id !== Constants.Roles.PUBLIC)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select
|
||||
|
@ -15,7 +25,7 @@
|
|||
{quiet}
|
||||
bind:value
|
||||
on:change
|
||||
options={$roles}
|
||||
{options}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
|
||||
|
|
|
@ -106,12 +106,3 @@
|
|||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon-wrapper {
|
||||
display: contents;
|
||||
}
|
||||
.icon-wrapper.highlight :global(svg) {
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -200,7 +200,7 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{#if views?.length}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Views</Heading>
|
||||
</div>
|
||||
|
@ -211,7 +211,7 @@
|
|||
</ul>
|
||||
{/if}
|
||||
{#if queries?.length}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Queries</Heading>
|
||||
</div>
|
||||
|
@ -227,7 +227,7 @@
|
|||
</ul>
|
||||
{/if}
|
||||
{#if links?.length}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Relationships</Heading>
|
||||
</div>
|
||||
|
@ -238,7 +238,7 @@
|
|||
</ul>
|
||||
{/if}
|
||||
{#if fields?.length}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Fields</Heading>
|
||||
</div>
|
||||
|
@ -249,7 +249,7 @@
|
|||
</ul>
|
||||
{/if}
|
||||
{#if jsonArrays?.length}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">JSON Arrays</Heading>
|
||||
</div>
|
||||
|
@ -260,7 +260,7 @@
|
|||
</ul>
|
||||
{/if}
|
||||
{#if showDataProviders && dataProviders?.length}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Data Providers</Heading>
|
||||
</div>
|
||||
|
@ -276,7 +276,7 @@
|
|||
</ul>
|
||||
{/if}
|
||||
{#if otherSources?.length}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Other</Heading>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { ExpiringKeys } from "./constants"
|
||||
import { getBanners } from "./licensingBanners"
|
||||
import { banner } from "@budibase/bbui"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
|
@ -89,7 +90,8 @@
|
|||
userLoaded &&
|
||||
$licensing.usageMetrics &&
|
||||
domLoaded &&
|
||||
!licensingLoaded
|
||||
!licensingLoaded &&
|
||||
isEnabled(TENANT_FEATURE_FLAGS.LICENSING)
|
||||
) {
|
||||
licensingLoaded = true
|
||||
queuedModals = processModals()
|
||||
|
|
|
@ -1,75 +1,116 @@
|
|||
<script>
|
||||
import { ActionButton, Icon, Search, Divider, Detail } from "@budibase/bbui"
|
||||
import { Icon, Search, Layout } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let searchTerm = ""
|
||||
export let selected
|
||||
export let filtered = []
|
||||
export let addAll
|
||||
export let select
|
||||
export let title
|
||||
export let key
|
||||
export let list = []
|
||||
export let labelKey
|
||||
export let iconComponent = null
|
||||
export let extractIconProps = x => x
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: enrichedList = enrich(list, selected)
|
||||
$: sortedList = sort(enrichedList)
|
||||
|
||||
const enrich = (list, selected) => {
|
||||
return list.map(item => {
|
||||
return {
|
||||
...item,
|
||||
selected: selected.find(x => x === item._id) != null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sort = list => {
|
||||
let sortedList = list.slice()
|
||||
sortedList.sort((a, b) => {
|
||||
if (a.selected === b.selected) {
|
||||
return a[labelKey] < b[labelKey] ? -1 : 1
|
||||
} else if (a.selected) {
|
||||
return -1
|
||||
} else if (b.selected) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return sortedList
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style="padding: var(--spacing-m)">
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
<div class="header sub-header">
|
||||
<div>
|
||||
<Detail
|
||||
>{filtered.length} {title}{filtered.length === 1 ? "" : "s"}</Detail
|
||||
>
|
||||
<div class="container">
|
||||
<Layout gap="S">
|
||||
<div class="header">
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton on:click={addAll} emphasized size="S">Add all</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
<div>
|
||||
{#each filtered as item}
|
||||
<div
|
||||
on:click={() => {
|
||||
select(item._id)
|
||||
}}
|
||||
style="padding-bottom: var(--spacing-m)"
|
||||
class="selection"
|
||||
>
|
||||
<div>
|
||||
{item[key]}
|
||||
</div>
|
||||
|
||||
{#if selected.includes(item._id)}
|
||||
<div>
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-blue-600);"
|
||||
name="Checkmark"
|
||||
<div class="items">
|
||||
{#each sortedList as item}
|
||||
<div
|
||||
on:click={() => {
|
||||
dispatch(item.selected ? "deselect" : "select", item._id)
|
||||
}}
|
||||
class="item"
|
||||
>
|
||||
{#if iconComponent}
|
||||
<svelte:component
|
||||
this={iconComponent}
|
||||
{...extractIconProps(item)}
|
||||
/>
|
||||
{/if}
|
||||
<div class="text">
|
||||
{item[labelKey]}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if item.selected}
|
||||
<div>
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-blue-600);"
|
||||
name="Checkmark"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 280px;
|
||||
}
|
||||
.header {
|
||||
align-items: center;
|
||||
padding: var(--spacing-m) 0 var(--spacing-m) 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
gap: var(--spacing-m);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.selection {
|
||||
align-items: end;
|
||||
.items {
|
||||
max-height: 242px;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0 calc(-1 * var(--spacing-m));
|
||||
margin-top: -8px;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-s) var(--spacing-l);
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
transition: background 130ms ease-out;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selection > :first-child {
|
||||
padding-top: var(--spacing-m);
|
||||
.item:hover {
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sub-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import { auth } from "../stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const FEATURE_FLAGS = {
|
||||
export const TENANT_FEATURE_FLAGS = {
|
||||
LICENSING: "LICENSING",
|
||||
USER_GROUPS: "USER_GROUPS",
|
||||
}
|
||||
|
||||
export const isEnabled = featureFlag => {
|
||||
const user = get(auth).user
|
||||
if (user?.featureFlags?.includes(featureFlag)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return !!user?.featureFlags?.includes(featureFlag)
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
</header>
|
||||
<Body size="M">{integration.description}</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="config-header">
|
||||
<Heading size="S">Configuration</Heading>
|
||||
<Button disabled={!changed} cta on:click={saveDatasource}>Save</Button>
|
||||
|
@ -111,7 +111,7 @@
|
|||
{#if datasource.plus}
|
||||
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||
{/if}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Queries</Heading>
|
||||
<div class="query-buttons">
|
||||
|
|
|
@ -592,7 +592,7 @@
|
|||
</div>
|
||||
<div class="bottom">
|
||||
<Layout paddingY="S" gap="S">
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
{#if !response && Object.keys(schema).length === 0}
|
||||
<Heading size="M">Response</Heading>
|
||||
<div class="placeholder">
|
||||
|
|
|
@ -59,7 +59,6 @@
|
|||
|
||||
// Use the currently selected role
|
||||
if (!screenAccessRole) {
|
||||
console.log("NO ROLE")
|
||||
return
|
||||
}
|
||||
screen.routing.roleId = screenAccessRole
|
||||
|
|
|
@ -52,7 +52,8 @@
|
|||
? publishedApps
|
||||
: publishedApps.filter(app => {
|
||||
return userGroups.find(group => {
|
||||
return Object.keys(group.roles)
|
||||
return groups.actions
|
||||
.getGroupAppIds(group)
|
||||
.map(role => apps.extractAppId(role))
|
||||
.includes(app.appId)
|
||||
})
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
import UpdateAPIKeyModal from "components/settings/UpdateAPIKeyModal.svelte"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { isEnabled, FEATURE_FLAGS } from "../../../helpers/featureFlags"
|
||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
|
||||
let loaded = false
|
||||
let userInfoModal
|
||||
|
@ -44,7 +44,7 @@
|
|||
href: "/builder/portal/manage/users",
|
||||
heading: "Manage",
|
||||
},
|
||||
isEnabled(FEATURE_FLAGS.USER_GROUPS)
|
||||
isEnabled(TENANT_FEATURE_FLAGS.USER_GROUPS)
|
||||
? {
|
||||
title: "User Groups",
|
||||
href: "/builder/portal/manage/groups",
|
||||
|
@ -103,7 +103,7 @@
|
|||
])
|
||||
}
|
||||
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||
// always show usage in self-host or cloud if licensing enabled
|
||||
menu = menu.concat([
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { PickerDropdown, notifications } from "@budibase/bbui"
|
||||
import { PickerDropdown } from "@budibase/bbui"
|
||||
import { groups } from "stores/portal"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -25,14 +25,6 @@
|
|||
const appIds = groupSelected?.apps || null
|
||||
dispatch("change", appIds)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await groups.actions.init()
|
||||
} catch (error) {
|
||||
notifications.error("Error")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<PickerDropdown
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
|
||||
{#if loaded && $templates?.length}
|
||||
<TemplateDisplay templates={$templates} />
|
||||
|
|
|
@ -10,9 +10,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: wide =
|
||||
$page.path.includes("email/:template") ||
|
||||
($page.path.includes("groups") && !$page.path.includes(":groupId"))
|
||||
$: wide = $page.path.includes("email/:template")
|
||||
</script>
|
||||
|
||||
{#if $auth.isAdmin}
|
||||
|
|
|
@ -311,7 +311,7 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
{#if providers.google}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">
|
||||
<div class="provider-title">
|
||||
|
@ -350,7 +350,7 @@
|
|||
</Layout>
|
||||
{/if}
|
||||
{#if providers.oidc}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">
|
||||
<div class="provider-title">
|
||||
|
|
|
@ -132,7 +132,7 @@
|
|||
values below and click activate.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
{#if smtpConfig}
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">SMTP</Heading>
|
||||
|
@ -186,7 +186,7 @@
|
|||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Templates</Heading>
|
||||
<Body size="S">
|
||||
|
|
|
@ -5,13 +5,16 @@
|
|||
Button,
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Icon,
|
||||
Popover,
|
||||
notifications,
|
||||
List,
|
||||
ListItem,
|
||||
StatusLight,
|
||||
Divider,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
|
@ -19,91 +22,32 @@
|
|||
import { onMount } from "svelte"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { roles } from "stores/backend"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
||||
import GroupIcon from "./_components/GroupIcon.svelte"
|
||||
|
||||
export let groupId
|
||||
|
||||
let popoverAnchor
|
||||
let popover
|
||||
let searchTerm = ""
|
||||
let selectedUsers = []
|
||||
let prevSearch = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let loaded = false
|
||||
let editModal
|
||||
let deleteModal
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: fetchUsers(page, searchTerm)
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
|
||||
async function addAll() {
|
||||
selectedUsers = [...selectedUsers, ...filtered.map(u => u._id)]
|
||||
|
||||
let reducedUserObjects = filtered.map(u => {
|
||||
return {
|
||||
_id: u._id,
|
||||
email: u.email,
|
||||
}
|
||||
})
|
||||
group.users = [...reducedUserObjects, ...group.users]
|
||||
|
||||
await groups.actions.save(group)
|
||||
|
||||
$users.data.forEach(async user => {
|
||||
let userToEdit = await users.get(user._id)
|
||||
let userGroups = userToEdit.userGroups || []
|
||||
userGroups.push(groupId)
|
||||
await users.save({
|
||||
...userToEdit,
|
||||
userGroups,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function selectUser(id) {
|
||||
let selectedUser = selectedUsers.includes(id)
|
||||
if (selectedUser) {
|
||||
selectedUsers = selectedUsers.filter(id => id !== selectedUser)
|
||||
let newUsers = group.users.filter(user => user._id !== id)
|
||||
group.users = newUsers
|
||||
} else {
|
||||
let enrichedUser = $users.data
|
||||
.filter(user => user._id === id)
|
||||
.map(u => {
|
||||
return {
|
||||
_id: u._id,
|
||||
email: u.email,
|
||||
}
|
||||
})[0]
|
||||
selectedUsers = [...selectedUsers, id]
|
||||
group.users.push(enrichedUser)
|
||||
$: filtered = $users.data
|
||||
$: groupApps = $apps.filter(app =>
|
||||
groups.actions.getGroupAppIds(group).includes(`app_${app.appId}`)
|
||||
)
|
||||
$: {
|
||||
if (loaded && !group?._id) {
|
||||
$goto("./")
|
||||
}
|
||||
|
||||
await groups.actions.save(group)
|
||||
|
||||
let user = await users.get(id)
|
||||
|
||||
let userGroups = user.userGroups || []
|
||||
userGroups.push(groupId)
|
||||
await users.save({
|
||||
...user,
|
||||
userGroups,
|
||||
})
|
||||
}
|
||||
$: filtered =
|
||||
$users.data?.filter(x => !group?.users.map(y => y._id).includes(x._id)) ||
|
||||
[]
|
||||
|
||||
$: groupApps = $apps.filter(x => group.apps.includes(x.appId))
|
||||
async function removeUser(id) {
|
||||
let newUsers = group.users.filter(user => user._id !== id)
|
||||
group.users = newUsers
|
||||
let user = await users.get(id)
|
||||
|
||||
await users.save({
|
||||
...user,
|
||||
userGroups: [],
|
||||
})
|
||||
|
||||
await groups.actions.save(group)
|
||||
}
|
||||
|
||||
async function fetchUsers(page, search) {
|
||||
|
@ -131,6 +75,24 @@
|
|||
return role?.name || "Custom role"
|
||||
}
|
||||
|
||||
async function deleteGroup() {
|
||||
try {
|
||||
await groups.actions.delete(group)
|
||||
notifications.success("User group deleted successfully")
|
||||
$goto("./")
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to delete user group`)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGroup(group) {
|
||||
try {
|
||||
await groups.actions.save(group)
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to save user group`)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await Promise.all([groups.actions.init(), apps.load(), roles.fetch()])
|
||||
|
@ -142,119 +104,137 @@
|
|||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<Layout noPadding>
|
||||
<Layout noPadding gap="XL">
|
||||
<div>
|
||||
<ActionButton
|
||||
on:click={() => $goto("../groups")}
|
||||
size="S"
|
||||
icon="ArrowLeft"
|
||||
>
|
||||
<ActionButton on:click={() => $goto("../groups")} icon="ArrowLeft">
|
||||
Back
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div style="background: {group?.color};" class="circle">
|
||||
<div>
|
||||
<Icon size="M" name={group?.icon} />
|
||||
|
||||
<Layout noPadding gap="M">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<GroupIcon {group} size="L" />
|
||||
<div class="text-padding">
|
||||
<Heading>{group?.name}</Heading>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-padding">
|
||||
<Heading>{group?.name}</Heading>
|
||||
<div>
|
||||
<ActionMenu align="right">
|
||||
<span slot="control">
|
||||
<Icon hoverable name="More" />
|
||||
</span>
|
||||
<MenuItem icon="Refresh" on:click={() => editModal.show()}>
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={() => deleteModal.show()}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div bind:this={popoverAnchor}>
|
||||
<Button on:click={popover.show()} icon="UserAdd" cta>Add user</Button>
|
||||
</div>
|
||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||
<UserGroupPicker
|
||||
key={"email"}
|
||||
title={"User"}
|
||||
bind:searchTerm
|
||||
bind:selected={selectedUsers}
|
||||
bind:filtered
|
||||
{addAll}
|
||||
select={selectUser}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<List>
|
||||
{#if group?.users.length}
|
||||
{#each group.users as user}
|
||||
<ListItem title={user?.email} avatar
|
||||
><Icon
|
||||
on:click={() => removeUser(user?._id)}
|
||||
hoverable
|
||||
size="L"
|
||||
name="Close"
|
||||
/></ListItem
|
||||
>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="UserGroup" title="You have no users in this team" />
|
||||
{/if}
|
||||
</List>
|
||||
<div
|
||||
style="flex-direction: column; margin-top: var(--spacing-m)"
|
||||
class="title"
|
||||
>
|
||||
<Heading weight="light" size="XS">Apps</Heading>
|
||||
<div style="margin-top: var(--spacing-xs)">
|
||||
<Body size="S"
|
||||
>Manage apps that this User group has been assigned to</Body
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
<List>
|
||||
{#if groupApps.length}
|
||||
{#each groupApps as app}
|
||||
<ListItem
|
||||
title={app.name}
|
||||
icon={app?.icon?.name || "Apps"}
|
||||
iconBackground={app?.icon?.color || ""}
|
||||
>
|
||||
<div class="title ">
|
||||
<StatusLight
|
||||
square
|
||||
color={RoleUtils.getRoleColour(group.roles[`app_${app.appId}`])}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="header">
|
||||
<Heading size="S">Users</Heading>
|
||||
<div bind:this={popoverAnchor}>
|
||||
<Button on:click={popover.show()} icon="UserAdd" cta>
|
||||
Add user
|
||||
</Button>
|
||||
</div>
|
||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||
<UserGroupPicker
|
||||
bind:searchTerm
|
||||
labelKey="email"
|
||||
selected={group.users?.map(user => user._id)}
|
||||
list={$users.data}
|
||||
on:select={e => groups.actions.addUser(groupId, e.detail)}
|
||||
on:deselect={e => groups.actions.removeUser(groupId, e.detail)}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
<List>
|
||||
{#if group?.users.length}
|
||||
{#each group.users as user}
|
||||
<ListItem
|
||||
title={user.email}
|
||||
on:click={() => $goto(`../users/${user._id}`)}
|
||||
hoverable
|
||||
>
|
||||
{getRoleLabel(app.appId)}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="UserGroup" title="No apps" />
|
||||
{/if}
|
||||
</List>
|
||||
<Icon
|
||||
on:click={e => {
|
||||
groups.actions.removeUser(groupId, user._id)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
hoverable
|
||||
size="S"
|
||||
name="Close"
|
||||
/>
|
||||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="UserGroup" title="This user group has no users" />
|
||||
{/if}
|
||||
</List>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="S">Apps</Heading>
|
||||
<List>
|
||||
{#if groupApps.length}
|
||||
{#each groupApps as app}
|
||||
<ListItem
|
||||
title={app.name}
|
||||
icon={app?.icon?.name || "Apps"}
|
||||
iconColor={app?.icon?.color || ""}
|
||||
on:click={() => $goto(`../../overview/${app.devId}`)}
|
||||
hoverable
|
||||
>
|
||||
<div class="title ">
|
||||
<StatusLight
|
||||
square
|
||||
color={RoleUtils.getRoleColour(
|
||||
group.roles[`app_${app.appId}`]
|
||||
)}
|
||||
>
|
||||
{getRoleLabel(app.appId)}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="Apps" title="This user group has access to no apps" />
|
||||
{/if}
|
||||
</List>
|
||||
</Layout>
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.text-padding {
|
||||
margin-left: var(--spacing-l);
|
||||
}
|
||||
<Modal bind:this={editModal}>
|
||||
<CreateEditGroupModal {group} {saveGroup} />
|
||||
</Modal>
|
||||
<ConfirmDialog
|
||||
bind:this={deleteModal}
|
||||
title="Delete user group"
|
||||
okText="Delete user group"
|
||||
onOk={deleteGroup}
|
||||
>
|
||||
Are you sure you wish to delete <b>{group?.name}?</b>
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
}
|
||||
.circle {
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
font-size: 1.2em;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.circle > div {
|
||||
padding: calc(1.5 * var(--spacing-xs)) var(--spacing-xs);
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
|
||||
$: count = Object.keys(value || {}).length
|
||||
</script>
|
||||
|
||||
<div class="align">
|
||||
<div class="spacing">
|
||||
<Icon name="WebPage" />
|
||||
</div>
|
||||
{count}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.align {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import { IconAvatar } from "@budibase/bbui"
|
||||
|
||||
export let group
|
||||
export let size = "M"
|
||||
</script>
|
||||
|
||||
<IconAvatar icon={group?.icon} background={group?.color} {size} />
|
|
@ -1,20 +1,13 @@
|
|||
<script>
|
||||
import { Avatar } from "@budibase/bbui"
|
||||
import GroupIcon from "./GroupIcon.svelte"
|
||||
|
||||
export let value
|
||||
export let row
|
||||
</script>
|
||||
|
||||
<div class="align">
|
||||
{#if value}
|
||||
<div class="spacing">
|
||||
<Avatar
|
||||
size="L"
|
||||
initials={value
|
||||
.split(" ")
|
||||
.map(x => x[0])
|
||||
.join("")}
|
||||
/>
|
||||
</div>
|
||||
<GroupIcon group={row} />
|
||||
{value}
|
||||
{:else}
|
||||
<div class="text">-</div>
|
||||
|
@ -26,12 +19,8 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.text {
|
||||
opacity: 0.8;
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Body,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import CreateEditGroupModal from "./CreateEditGroupModal.svelte"
|
||||
|
||||
export let group
|
||||
export let deleteGroup
|
||||
export let saveGroup
|
||||
let modal
|
||||
function editGroup() {
|
||||
modal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="title">
|
||||
<div class="name" style="display: flex; margin-left: var(--spacing-xl)">
|
||||
<div style="background: {group.color};" class="circle">
|
||||
<div>
|
||||
<Icon size="M" name={group.icon} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="name" data-cy="app-name-link">
|
||||
<Body size="S">{group.name}</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desktop tableElement">
|
||||
<Icon name="User" />
|
||||
<div style="margin-left: var(--spacing-l">
|
||||
{parseInt(group?.users?.length) || 0} user{parseInt(
|
||||
group?.users?.length
|
||||
) === 1
|
||||
? ""
|
||||
: "s"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="desktop tableElement">
|
||||
<Icon name="WebPage" />
|
||||
|
||||
<div style="margin-left: var(--spacing-l)">
|
||||
{parseInt(group?.apps?.length) || 0} app{parseInt(group?.apps?.length) === 1
|
||||
? ""
|
||||
: "s"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="group-row-actions">
|
||||
<div>
|
||||
<Button on:click={() => $goto(`./${group._id}`)} size="S" cta
|
||||
>Manage</Button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<ActionMenu align="right">
|
||||
<span slot="control">
|
||||
<Icon hoverable name="More" />
|
||||
</span>
|
||||
<MenuItem on:click={() => deleteGroup(group)} icon="Delete"
|
||||
>Delete</MenuItem
|
||||
>
|
||||
<MenuItem on:click={() => editGroup(group)} icon="Edit">Edit</MenuItem>
|
||||
</ActionMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditGroupModal {group} {saveGroup} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.group-row-actions {
|
||||
display: flex;
|
||||
float: right;
|
||||
margin-right: var(--spacing-xl);
|
||||
grid-template-columns: 75px 75px;
|
||||
grid-gap: var(--spacing-xl);
|
||||
}
|
||||
.name {
|
||||
grid-gap: var(--spacing-xl);
|
||||
grid-template-columns: 75px 75px;
|
||||
align-items: center;
|
||||
}
|
||||
.circle {
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
font-size: 1.2em;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.tableElement {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.circle > div {
|
||||
padding: calc(1.5 * var(--spacing-xs)) var(--spacing-xs);
|
||||
}
|
||||
.name {
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.name :global(.spectrum-Heading) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: calc(1.5 * var(--spacing-xl));
|
||||
}
|
||||
.title :global(h1:hover) {
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
cursor: pointer;
|
||||
transition: color 130ms ease;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.desktop {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div class="align">
|
||||
<div class="spacing">
|
||||
<Icon name="User" />
|
||||
</div>
|
||||
{parseInt(value?.length) || 0}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.align {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -4,16 +4,23 @@
|
|||
Heading,
|
||||
Body,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Modal,
|
||||
Tag,
|
||||
Tags,
|
||||
Table,
|
||||
Divider,
|
||||
Search,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { groups, auth, licensing, admin } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
||||
import UserGroupsRow from "./_components/UserGroupsRow.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import GroupAppsTableRenderer from "./_components/GroupAppsTableRenderer.svelte"
|
||||
import UsersTableRenderer from "./_components/UsersTableRenderer.svelte"
|
||||
import GroupNameTableRenderer from "./_components/GroupNameTableRenderer.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
const DefaultGroup = {
|
||||
name: "",
|
||||
|
@ -23,20 +30,38 @@
|
|||
apps: [],
|
||||
roles: {},
|
||||
}
|
||||
let modal
|
||||
let group = cloneDeep(DefaultGroup)
|
||||
|
||||
async function deleteGroup(group) {
|
||||
try {
|
||||
groups.actions.delete(group)
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to delete group`)
|
||||
let modal
|
||||
let searchString
|
||||
let group = cloneDeep(DefaultGroup)
|
||||
let customRenderers = [
|
||||
{ column: "name", component: GroupNameTableRenderer },
|
||||
{ column: "users", component: UsersTableRenderer },
|
||||
{ column: "roles", component: GroupAppsTableRenderer },
|
||||
]
|
||||
|
||||
$: schema = {
|
||||
name: {},
|
||||
users: { sortable: false },
|
||||
roles: { sortable: false, displayName: "Apps" },
|
||||
}
|
||||
$: filteredGroups = filterGroups($groups, searchString)
|
||||
|
||||
const filterGroups = (groups, searchString) => {
|
||||
if (!searchString) {
|
||||
return groups
|
||||
}
|
||||
searchString = searchString.toLocaleLowerCase()
|
||||
return groups?.filter(group => {
|
||||
return group.name?.toLowerCase().includes(searchString)
|
||||
})
|
||||
}
|
||||
|
||||
async function saveGroup(group) {
|
||||
try {
|
||||
await groups.actions.save(group)
|
||||
group = await groups.actions.save(group)
|
||||
$goto(`./${group._id}`)
|
||||
notifications.success(`User group created successfully`)
|
||||
} catch (error) {
|
||||
if (error.status === 400) {
|
||||
notifications.error(error.message)
|
||||
|
@ -59,64 +84,75 @@
|
|||
await groups.actions.init()
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error getting User groups")
|
||||
notifications.error("Error getting user groups")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
<Layout noPadding gap="M">
|
||||
<Layout gap="XS" noPadding>
|
||||
<div style="display: flex;">
|
||||
<Heading size="M">User groups</Heading>
|
||||
{#if !$licensing.groupsEnabled}
|
||||
<Tags>
|
||||
<div class="tags">
|
||||
<div class="tag">
|
||||
<Tag icon="LockClosed">Pro plan</Tag>
|
||||
</div>
|
||||
<Heading size="M">User groups</Heading>
|
||||
{#if !$licensing.groupsEnabled}
|
||||
<Tags>
|
||||
<div class="tags">
|
||||
<div class="tag">
|
||||
<Tag icon="LockClosed">Pro plan</Tag>
|
||||
</div>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
<Body>Easily assign and manage your users access with User Groups</Body>
|
||||
{#if !$auth.accountPortalAccess && $admin.cloud}
|
||||
<Body>Contact your account holder to upgrade</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
<div class="align-buttons">
|
||||
{#if $licensing.groupsEnabled}
|
||||
<!--Show the group create button-->
|
||||
<Button newStyles icon={"UserGroup"} cta on:click={showCreateGroupModal}>
|
||||
Create user group
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
newStyles
|
||||
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||
on:click={$licensing.goToUpgradePage()}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
<!--Show the view plans button-->
|
||||
<Button
|
||||
newStyles
|
||||
secondary
|
||||
on:click={() => {
|
||||
window.open("https://budibase.com/pricing/", "_blank")
|
||||
}}>View Plans</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $licensing.groupsEnabled && $groups.length}
|
||||
<div class="groupTable">
|
||||
{#each $groups as group}
|
||||
<div>
|
||||
<UserGroupsRow {saveGroup} {deleteGroup} {group} />
|
||||
</div>
|
||||
{/each}
|
||||
</Tags>
|
||||
{/if}
|
||||
<Body>
|
||||
Easily assign and manage your users' access with user groups.
|
||||
{#if !$auth.accountPortalAccess && !$licensing.groupsEnabled && $admin.cloud}
|
||||
Contact your account holder to upgrade your plan.
|
||||
{/if}
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<div class="controls">
|
||||
<ButtonGroup>
|
||||
{#if $licensing.groupsEnabled}
|
||||
<!--Show the group create button-->
|
||||
<Button
|
||||
newStyles
|
||||
icon={"UserGroup"}
|
||||
cta
|
||||
on:click={showCreateGroupModal}
|
||||
>
|
||||
Create user group
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
newStyles
|
||||
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||
on:click={$licensing.goToUpgradePage()}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
<!--Show the view plans button-->
|
||||
<Button
|
||||
newStyles
|
||||
secondary
|
||||
on:click={() => {
|
||||
window.open("https://budibase.com/pricing/", "_blank")
|
||||
}}
|
||||
>
|
||||
View Plans
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
<div class="controls-right">
|
||||
<Search bind:value={searchString} placeholder="Search" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Table
|
||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||
{schema}
|
||||
data={filteredGroups}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
{customRenderers}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
@ -124,37 +160,24 @@
|
|||
</Modal>
|
||||
|
||||
<style>
|
||||
.align-buttons {
|
||||
.controls {
|
||||
display: flex;
|
||||
column-gap: var(--spacing-xl);
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.controls-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.controls-right :global(.spectrum-Search) {
|
||||
width: 200px;
|
||||
}
|
||||
.tag {
|
||||
margin-top: var(--spacing-xs);
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
|
||||
.groupTable {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
|
||||
border-left: 1px solid var(--spectrum-alias-border-color-mid);
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
}
|
||||
|
||||
.groupTable :global(> div) {
|
||||
background: var(--bg-color);
|
||||
|
||||
height: 55px;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-gap: var(--spacing-xl);
|
||||
grid-template-columns: 2fr 2fr 2fr auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 var(--spacing-s);
|
||||
border-top: 1px solid var(--spectrum-alias-border-color-mid);
|
||||
border-right: 1px solid var(--spectrum-alias-border-color-mid);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="M">Plugins</Heading>
|
||||
<Body>Add your own custom datasources and components</Body>
|
||||
<Body>Add your own custom datasources and components.</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Layout noPadding>
|
||||
|
|
|
@ -19,17 +19,17 @@
|
|||
Modal,
|
||||
notifications,
|
||||
Divider,
|
||||
Banner,
|
||||
StatusLight,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { fetchData } from "helpers"
|
||||
import { users, auth, groups, apps, licensing } from "stores/portal"
|
||||
import { roles } from "stores/backend"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||
import DeleteUserModal from "./_components/DeleteUserModal.svelte"
|
||||
import GroupIcon from "../groups/_components/GroupIcon.svelte"
|
||||
import { Constants, RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
export let userId
|
||||
|
||||
|
@ -38,59 +38,57 @@
|
|||
let popoverAnchor
|
||||
let searchTerm = ""
|
||||
let popover
|
||||
let selectedGroups = []
|
||||
let allAppList = []
|
||||
let user
|
||||
let loaded = false
|
||||
|
||||
$: fetchUser(userId)
|
||||
$: fullName = $userFetch?.data?.firstName
|
||||
? $userFetch?.data?.firstName + " " + $userFetch?.data?.lastName
|
||||
: ""
|
||||
$: nameLabel = getNameLabel($userFetch)
|
||||
$: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : ""
|
||||
$: privileged = user?.admin?.global || user?.builder?.global
|
||||
$: nameLabel = getNameLabel(user)
|
||||
$: initials = getInitials(nameLabel)
|
||||
$: allAppList = $apps
|
||||
.filter(x => {
|
||||
if ($userFetch.data?.roles) {
|
||||
return Object.keys($userFetch.data.roles).find(y => {
|
||||
return x.appId === apps.extractAppId(y)
|
||||
})
|
||||
}
|
||||
})
|
||||
.map(app => {
|
||||
let roles = Object.fromEntries(
|
||||
Object.entries($userFetch.data.roles).filter(([key]) => {
|
||||
return apps.extractAppId(key) === app.appId
|
||||
})
|
||||
)
|
||||
return {
|
||||
name: app.name,
|
||||
devId: app.devId,
|
||||
icon: app.icon,
|
||||
roles,
|
||||
}
|
||||
})
|
||||
// Used for searching through groups in the add group popover
|
||||
$: filteredGroups = $groups.filter(
|
||||
group =>
|
||||
selectedGroups &&
|
||||
group?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||
$: userGroups = $groups.filter(x => {
|
||||
return x.users?.find(y => {
|
||||
return y._id === userId
|
||||
})
|
||||
})
|
||||
$: globalRole = $userFetch?.data?.admin?.global
|
||||
$: globalRole = user?.admin?.global
|
||||
? "admin"
|
||||
: $userFetch?.data?.builder?.global
|
||||
: user?.builder?.global
|
||||
? "developer"
|
||||
: "appUser"
|
||||
|
||||
const userFetch = fetchData(`/api/global/users/${userId}`)
|
||||
const getAvailableApps = (appList, privileged, roles) => {
|
||||
let availableApps = appList.slice()
|
||||
if (!privileged) {
|
||||
availableApps = availableApps.filter(x => {
|
||||
return Object.keys(roles || {}).find(y => {
|
||||
return x.appId === apps.extractAppId(y)
|
||||
})
|
||||
})
|
||||
}
|
||||
return availableApps.map(app => {
|
||||
const prodAppId = apps.getProdAppID(app.appId)
|
||||
console.log(prodAppId)
|
||||
return {
|
||||
name: app.name,
|
||||
devId: app.devId,
|
||||
icon: app.icon,
|
||||
role: privileged ? Constants.Roles.ADMIN : roles[prodAppId],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getNameLabel = userFetch => {
|
||||
const { firstName, lastName, email } = userFetch?.data || {}
|
||||
const getFilteredGroups = (groups, search) => {
|
||||
if (!search) {
|
||||
return groups
|
||||
}
|
||||
search = search.toLowerCase()
|
||||
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
||||
}
|
||||
|
||||
const getNameLabel = user => {
|
||||
const { firstName, lastName, email } = user || {}
|
||||
if (!firstName && !lastName) {
|
||||
return email || ""
|
||||
}
|
||||
|
@ -122,38 +120,19 @@
|
|||
return role?.name || "Custom role"
|
||||
}
|
||||
|
||||
function getHighestRole(roles) {
|
||||
let highestRole
|
||||
let highestRoleNumber = 0
|
||||
Object.keys(roles).forEach(role => {
|
||||
let roleNumber = RoleUtils.getRolePriority(roles[role])
|
||||
if (roleNumber > highestRoleNumber) {
|
||||
highestRoleNumber = roleNumber
|
||||
highestRole = roles[role]
|
||||
}
|
||||
})
|
||||
return highestRole
|
||||
}
|
||||
async function updateUserFirstName(evt) {
|
||||
try {
|
||||
await users.save({ ...$userFetch?.data, firstName: evt.target.value })
|
||||
await userFetch.refresh()
|
||||
await users.save({ ...user, firstName: evt.target.value })
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
}
|
||||
|
||||
async function removeGroup(id) {
|
||||
let updatedGroup = $groups.find(x => x._id === id)
|
||||
let newUsers = updatedGroup.users.filter(user => user._id !== userId)
|
||||
updatedGroup.users = newUsers
|
||||
groups.actions.save(updatedGroup)
|
||||
}
|
||||
|
||||
async function updateUserLastName(evt) {
|
||||
try {
|
||||
await users.save({ ...$userFetch?.data, lastName: evt.target.value })
|
||||
await userFetch.refresh()
|
||||
await users.save({ ...user, lastName: evt.target.value })
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
|
@ -169,40 +148,40 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function addGroup(groupId) {
|
||||
let selectedGroup = selectedGroups.includes(groupId)
|
||||
let group = $groups.find(group => group._id === groupId)
|
||||
|
||||
if (selectedGroup) {
|
||||
selectedGroups = selectedGroups.filter(id => id === selectedGroup)
|
||||
let newUsers = group.users.filter(groupUser => user._id !== groupUser._id)
|
||||
group.users = newUsers
|
||||
} else {
|
||||
selectedGroups = [...selectedGroups, groupId]
|
||||
group.users.push(user)
|
||||
async function fetchUser() {
|
||||
user = await users.get(userId)
|
||||
if (!user?._id) {
|
||||
$goto("./")
|
||||
}
|
||||
|
||||
await groups.actions.save(group)
|
||||
}
|
||||
|
||||
async function fetchUser(userId) {
|
||||
let userPromise = users.get(userId)
|
||||
user = await userPromise
|
||||
}
|
||||
|
||||
async function toggleFlags(detail) {
|
||||
try {
|
||||
await users.save({ ...$userFetch?.data, ...detail })
|
||||
await userFetch.refresh()
|
||||
await users.save({ ...user, ...detail })
|
||||
await fetchUser()
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
}
|
||||
|
||||
function addAll() {}
|
||||
const addGroup = async groupId => {
|
||||
await groups.actions.addUser(groupId, userId)
|
||||
await fetchUser()
|
||||
}
|
||||
|
||||
const removeGroup = async groupId => {
|
||||
await groups.actions.removeUser(groupId, userId)
|
||||
await fetchUser()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await Promise.all([groups.actions.init(), apps.load(), roles.fetch()])
|
||||
await Promise.all([
|
||||
fetchUser(),
|
||||
groups.actions.init(),
|
||||
apps.load(),
|
||||
roles.fetch(),
|
||||
])
|
||||
loaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error getting user groups")
|
||||
|
@ -225,13 +204,13 @@
|
|||
<Avatar size="XXL" {initials} />
|
||||
<div class="subtitle">
|
||||
<Heading size="S">{nameLabel}</Heading>
|
||||
{#if nameLabel !== $userFetch?.data?.email}
|
||||
<Body size="S">{$userFetch?.data?.email}</Body>
|
||||
{#if nameLabel !== user?.email}
|
||||
<Body size="S">{user?.email}</Body>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if userId !== $auth.user._id}
|
||||
{#if userId !== $auth.user?._id}
|
||||
<div>
|
||||
<ActionMenu align="right">
|
||||
<span slot="control">
|
||||
|
@ -247,27 +226,21 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="S">Details</Heading>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Email</Label>
|
||||
<Input disabled value={$userFetch?.data?.email} />
|
||||
<Input disabled value={user?.email} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<Label size="L">First name</Label>
|
||||
<Input
|
||||
value={$userFetch?.data?.firstName}
|
||||
on:blur={updateUserFirstName}
|
||||
/>
|
||||
<Input value={user?.firstName} on:blur={updateUserFirstName} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<Label size="L">Last name</Label>
|
||||
<Input
|
||||
value={$userFetch?.data?.lastName}
|
||||
on:blur={updateUserLastName}
|
||||
/>
|
||||
<Input value={user?.lastName} on:blur={updateUserLastName} />
|
||||
</div>
|
||||
<!-- don't let a user remove the privileges that let them be here -->
|
||||
{#if userId !== $auth.user._id}
|
||||
|
@ -301,13 +274,14 @@
|
|||
</div>
|
||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||
<UserGroupPicker
|
||||
key={"name"}
|
||||
title={"User group"}
|
||||
labelKey="name"
|
||||
bind:searchTerm
|
||||
bind:selected={selectedGroups}
|
||||
bind:filtered={filteredGroups}
|
||||
{addAll}
|
||||
select={addGroup}
|
||||
list={filteredGroups}
|
||||
selected={user.userGroups}
|
||||
on:select={e => addGroup(e.detail)}
|
||||
on:deselect={e => removeGroup(e.detail)}
|
||||
iconComponent={GroupIcon}
|
||||
extractIconProps={item => ({ group: item, size: "S" })}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
@ -322,7 +296,10 @@
|
|||
on:click={() => $goto(`../groups/${group._id}`)}
|
||||
>
|
||||
<Icon
|
||||
on:click={removeGroup(group._id)}
|
||||
on:click={e => {
|
||||
removeGroup(group._id)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
hoverable
|
||||
size="S"
|
||||
name="Close"
|
||||
|
@ -330,7 +307,7 @@
|
|||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="UserGroup" title="No groups" />
|
||||
<ListItem icon="UserGroup" title="This user is in no user groups" />
|
||||
{/if}
|
||||
</List>
|
||||
</Layout>
|
||||
|
@ -339,27 +316,28 @@
|
|||
<Layout gap="S" noPadding>
|
||||
<Heading size="S">Apps</Heading>
|
||||
<List>
|
||||
{#if allAppList.length}
|
||||
{#each allAppList as app}
|
||||
{#if privileged}
|
||||
<Banner showCloseButton={false}>
|
||||
This user's role grants admin access to all apps
|
||||
</Banner>
|
||||
{:else if availableApps.length}
|
||||
{#each availableApps as app}
|
||||
<ListItem
|
||||
title={app.name}
|
||||
iconBackground={app?.icon?.color || ""}
|
||||
iconColor={app?.icon?.color}
|
||||
icon={app?.icon?.name || "Apps"}
|
||||
hoverable
|
||||
on:click={() => $goto(`../../overview/${app.devId}`)}
|
||||
>
|
||||
<div class="title ">
|
||||
<StatusLight
|
||||
square
|
||||
color={RoleUtils.getRoleColour(getHighestRole(app.roles))}
|
||||
>
|
||||
{getRoleLabel(getHighestRole(app.roles))}
|
||||
<StatusLight square color={RoleUtils.getRoleColour(app.role)}>
|
||||
{getRoleLabel(app.role)}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="Apps" title="No apps" />
|
||||
<ListItem icon="Apps" title="This user has access to no apps" />
|
||||
{/if}
|
||||
</List>
|
||||
</Layout>
|
||||
|
@ -367,13 +345,10 @@
|
|||
{/if}
|
||||
|
||||
<Modal bind:this={deleteModal}>
|
||||
<DeleteUserModal user={$userFetch.data} />
|
||||
<DeleteUserModal {user} />
|
||||
</Modal>
|
||||
<Modal bind:this={resetPasswordModal}>
|
||||
<ForceResetPasswordModal
|
||||
user={$userFetch.data}
|
||||
on:update={userFetch.refresh}
|
||||
/>
|
||||
<ForceResetPasswordModal {user} on:update={fetchUser} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { apps } from "stores/portal"
|
||||
|
||||
export let value
|
||||
export let row
|
||||
|
||||
$: priviliged = row?.admin?.global || row?.builder?.global
|
||||
$: count = priviliged ? $apps.length : value?.length || 0
|
||||
</script>
|
||||
|
||||
<div class="align">
|
||||
<div class="spacing">
|
||||
<Icon name="WebPage" />
|
||||
</div>
|
||||
{parseInt(value?.length) || 0}
|
||||
{count}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -15,7 +21,6 @@
|
|||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
|
|
@ -18,11 +18,9 @@
|
|||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.opacity {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@
|
|||
size="M"
|
||||
title="Import users"
|
||||
confirmText="Done"
|
||||
showCancelButton={false}
|
||||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
Table,
|
||||
Layout,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Search,
|
||||
notifications,
|
||||
Pagination,
|
||||
|
@ -22,48 +23,52 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import OnboardingTypeModal from "./_components/OnboardingTypeModal.svelte"
|
||||
import PasswordModal from "./_components/PasswordModal.svelte"
|
||||
import InvitedModal from "./_components/InvitedModal.svelte"
|
||||
import DeletionFailureModal from "./_components/DeletionFailureModal.svelte"
|
||||
import ImportUsersModal from "./_components/ImportUsersModal.svelte"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
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 createUserModal,
|
||||
inviteConfirmationModal,
|
||||
onboardingTypeModal,
|
||||
passwordModal,
|
||||
importUsersModal,
|
||||
deletionFailureModal
|
||||
let pageInfo = createPaginationStore()
|
||||
let prevEmail = undefined,
|
||||
searchEmail = undefined
|
||||
importUsersModal
|
||||
let searchEmail = undefined
|
||||
let selectedRows = []
|
||||
let bulkSaveResponse
|
||||
let customRenderers = [
|
||||
{ column: "userGroups", component: GroupsTableRenderer },
|
||||
{ column: "apps", component: AppsTableRenderer },
|
||||
{ column: "role", component: RoleTableRenderer },
|
||||
]
|
||||
let userData = []
|
||||
|
||||
$: debouncedUpdateFetch(searchEmail)
|
||||
$: schema = {
|
||||
email: {},
|
||||
email: {
|
||||
sortable: false,
|
||||
},
|
||||
role: {
|
||||
sortable: false,
|
||||
},
|
||||
...($licensing.groupsEnabled && {
|
||||
userGroups: { sortable: false, displayName: "Groups" },
|
||||
}),
|
||||
apps: {},
|
||||
apps: {
|
||||
sortable: false,
|
||||
},
|
||||
}
|
||||
$: userData = []
|
||||
$: createUsersResponse = { successful: [], unsuccessful: [] }
|
||||
$: deleteUsersResponse = { successful: [], unsuccessful: [] }
|
||||
$: inviteUsersResponse = { successful: [], unsuccessful: [] }
|
||||
$: page = $pageInfo.page
|
||||
$: fetchUsers(page, searchEmail)
|
||||
$: {
|
||||
enrichedUsers = $users.data?.map(user => {
|
||||
enrichedUsers = $fetch.rows?.map(user => {
|
||||
let userGroups = []
|
||||
$groups.forEach(group => {
|
||||
if (group.users) {
|
||||
|
@ -83,6 +88,15 @@
|
|||
})
|
||||
}
|
||||
|
||||
const updateFetch = email => {
|
||||
fetch.update({
|
||||
query: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
}
|
||||
const debouncedUpdateFetch = Utils.debounce(updateFetch, 250)
|
||||
|
||||
const showOnboardingTypeModal = async addUsersData => {
|
||||
userData = await removingDuplicities(addUsersData)
|
||||
if (!userData?.users?.length) return
|
||||
|
@ -95,9 +109,11 @@
|
|||
email: user.email,
|
||||
builder: user.role === Constants.BudibaseRoles.Developer,
|
||||
admin: user.role === Constants.BudibaseRoles.Admin,
|
||||
groups: userData.groups,
|
||||
}))
|
||||
try {
|
||||
inviteUsersResponse = await users.invite(payload)
|
||||
const res = await users.invite(payload)
|
||||
notifications.success(res.message)
|
||||
inviteConfirmationModal.show()
|
||||
} catch (error) {
|
||||
notifications.error("Error inviting user")
|
||||
|
@ -120,9 +136,8 @@
|
|||
newUsers.push(user)
|
||||
}
|
||||
|
||||
if (!newUsers.length) {
|
||||
if (!newUsers.length)
|
||||
notifications.info("Duplicated! There is no new users to add.")
|
||||
}
|
||||
return { ...userData, users: newUsers }
|
||||
}
|
||||
|
||||
|
@ -149,12 +164,11 @@
|
|||
|
||||
async function createUsers() {
|
||||
try {
|
||||
createUsersResponse = await users.create(
|
||||
await removingDuplicities(userData)
|
||||
)
|
||||
bulkSaveResponse = await users.create(await removingDuplicities(userData))
|
||||
notifications.success("Successfully created user")
|
||||
await groups.actions.init()
|
||||
passwordModal.show()
|
||||
await fetch.refresh()
|
||||
} catch (error) {
|
||||
notifications.error("Error creating user")
|
||||
}
|
||||
|
@ -162,20 +176,12 @@
|
|||
|
||||
async function chooseCreationType(onboardingType) {
|
||||
if (onboardingType === "emailOnboarding") {
|
||||
createUserFlow()
|
||||
await createUserFlow()
|
||||
} else {
|
||||
await createUsers()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await groups.actions.init()
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching User Group data")
|
||||
}
|
||||
})
|
||||
|
||||
const deleteRows = async () => {
|
||||
try {
|
||||
let ids = selectedRows.map(user => user._id)
|
||||
|
@ -183,105 +189,100 @@
|
|||
notifications.error("You cannot delete yourself")
|
||||
return
|
||||
}
|
||||
deleteUsersResponse = await users.bulkDelete(ids)
|
||||
if (deleteUsersResponse.unsuccessful?.length) {
|
||||
deletionFailureModal.show()
|
||||
} else {
|
||||
notifications.success(
|
||||
`Successfully deleted ${selectedRows.length} users`
|
||||
)
|
||||
}
|
||||
|
||||
await users.bulkDelete(ids)
|
||||
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
|
||||
selectedRows = []
|
||||
await fetchUsers(page, searchEmail)
|
||||
await fetch.refresh()
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting rows")
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUsers(page, email) {
|
||||
if ($pageInfo.loading) {
|
||||
return
|
||||
}
|
||||
// need to remove the page if they've started searching
|
||||
if (email && !prevEmail) {
|
||||
pageInfo.reset()
|
||||
page = undefined
|
||||
}
|
||||
prevEmail = email
|
||||
onMount(async () => {
|
||||
try {
|
||||
pageInfo.loading()
|
||||
await users.search({ page, email })
|
||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||
loaded = false
|
||||
await groups.actions.init()
|
||||
loaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error getting user list")
|
||||
notifications.error("Error fetching User Group data")
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Layout noPadding gap="M">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading>Users</Heading>
|
||||
<Body>Add users and control who gets access to your published apps</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<div class="controls">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
dataCy="add-user"
|
||||
on:click={createUserModal.show}
|
||||
icon="UserAdd"
|
||||
cta>Add users</Button
|
||||
>
|
||||
<Button
|
||||
on:click={importUsersModal.show}
|
||||
icon="Import"
|
||||
secondary
|
||||
newStyles
|
||||
>
|
||||
Import users
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<div class="controls-right">
|
||||
<Search bind:value={searchEmail} placeholder="Search email" />
|
||||
{#if selectedRows.length > 0}
|
||||
<DeleteRowsButton
|
||||
item="user"
|
||||
on:updaterows
|
||||
{selectedRows}
|
||||
{deleteRows}
|
||||
/>
|
||||
{/if}
|
||||
{#if loaded && $fetch.loaded}
|
||||
<Layout noPadding gap="M">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading>Users</Heading>
|
||||
<Body>Add users and control who gets access to your published apps.</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<div class="controls">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
dataCy="add-user"
|
||||
on:click={createUserModal.show}
|
||||
icon="UserAdd"
|
||||
cta
|
||||
>Add users
|
||||
</Button>
|
||||
<Button
|
||||
on:click={importUsersModal.show}
|
||||
icon="Import"
|
||||
secondary
|
||||
newStyles
|
||||
>
|
||||
Import users
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<div class="controls-right">
|
||||
<Search bind:value={searchEmail} placeholder="Search" />
|
||||
{#if selectedRows.length > 0}
|
||||
<DeleteRowsButton
|
||||
item="user"
|
||||
on:updaterows
|
||||
{selectedRows}
|
||||
{deleteRows}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||
{schema}
|
||||
bind:selectedRows
|
||||
data={enrichedUsers}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={true}
|
||||
showHeaderBorder={false}
|
||||
{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}
|
||||
<Table
|
||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||
{schema}
|
||||
bind:selectedRows
|
||||
data={enrichedUsers}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={true}
|
||||
{customRenderers}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
<div class="pagination">
|
||||
<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}>
|
||||
<AddUserModal {showOnboardingTypeModal} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={inviteConfirmationModal}>
|
||||
<InvitedModal {inviteUsersResponse} />
|
||||
<ModalContent
|
||||
showCancelButton={false}
|
||||
title="Invites sent!"
|
||||
confirmText="Done"
|
||||
>
|
||||
<Body size="S">
|
||||
Your users should now recieve an email invite to get access to their
|
||||
Budibase account
|
||||
</Body>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={onboardingTypeModal}>
|
||||
|
@ -289,11 +290,10 @@
|
|||
</Modal>
|
||||
|
||||
<Modal bind:this={passwordModal}>
|
||||
<PasswordModal {createUsersResponse} userData={userData.users} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={deletionFailureModal}>
|
||||
<DeletionFailureModal {deleteUsersResponse} />
|
||||
<PasswordModal
|
||||
createUsersResponse={bulkSaveResponse}
|
||||
userData={userData.users}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importUsersModal}>
|
||||
|
@ -313,6 +313,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -320,6 +321,7 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.controls-right :global(.spectrum-Search) {
|
||||
width: 200px;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
import AccessTab from "../_components/AccessTab.svelte"
|
||||
import { API } from "api"
|
||||
import { store } from "builderStore"
|
||||
import { apps, auth } from "stores/portal"
|
||||
import { apps, auth, groups } from "stores/portal"
|
||||
import analytics, { Events, EventSource } from "analytics"
|
||||
import { AppStatus } from "constants"
|
||||
import AppLockModal from "components/common/AppLockModal.svelte"
|
||||
|
@ -36,17 +36,21 @@
|
|||
|
||||
export let application
|
||||
|
||||
let promise = getPackage()
|
||||
let loaded = false
|
||||
let deletionModal
|
||||
let unpublishModal
|
||||
let exportModal
|
||||
let appName = ""
|
||||
let deployments = []
|
||||
let published
|
||||
|
||||
// App
|
||||
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||
$: loaded && !selectedApp && backToAppList()
|
||||
$: isPublished =
|
||||
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
|
||||
$: appUrl = `${window.origin}/app${selectedApp?.url}`
|
||||
|
||||
// Locking
|
||||
$: lockedBy = selectedApp?.lockedBy
|
||||
|
@ -58,18 +62,11 @@
|
|||
}`
|
||||
|
||||
// App deployments
|
||||
$: deployments = []
|
||||
$: latestDeployments = deployments
|
||||
.filter(
|
||||
deployment =>
|
||||
deployment.status === "SUCCESS" && application === deployment.appId
|
||||
)
|
||||
.filter(x => x.status === "SUCCESS" && application === x.appId)
|
||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||
|
||||
$: isPublished =
|
||||
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
|
||||
|
||||
$: appUrl = `${window.origin}/app${selectedApp?.url}`
|
||||
// Tabs
|
||||
$: tabs = ["Overview", "Automation History", "Backups", "Settings", "Access"]
|
||||
$: selectedTab = "Overview"
|
||||
|
||||
|
@ -87,17 +84,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function getPackage() {
|
||||
try {
|
||||
const pkg = await API.fetchAppPackage(application)
|
||||
await store.actions.initialise(pkg)
|
||||
loaded = true
|
||||
return pkg
|
||||
} catch (error) {
|
||||
notifications.error(`Error initialising app: ${error?.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const reviewPendingDeployments = (deployments, newDeployments) => {
|
||||
if (deployments.length > 0) {
|
||||
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
|
||||
|
@ -187,24 +173,37 @@
|
|||
appName = null
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
store.actions.reset()
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get("tab")) {
|
||||
selectedTab = params.get("tab")
|
||||
}
|
||||
|
||||
// Check app exists
|
||||
try {
|
||||
const pkg = await API.fetchAppPackage(application)
|
||||
await store.actions.initialise(pkg)
|
||||
} catch (error) {
|
||||
// Swallow
|
||||
backToAppList()
|
||||
}
|
||||
|
||||
// Initialise application
|
||||
try {
|
||||
await API.syncApp(application)
|
||||
deployments = await fetchDeployments()
|
||||
await groups.actions.init()
|
||||
if (!apps.length) {
|
||||
await apps.load()
|
||||
}
|
||||
await API.syncApp(application)
|
||||
deployments = await fetchDeployments()
|
||||
} catch (error) {
|
||||
notifications.error("Error initialising app overview")
|
||||
}
|
||||
loaded = true
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
store.actions.reset()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -214,11 +213,11 @@
|
|||
|
||||
<span class="overview-wrap">
|
||||
<Page wide noPadding>
|
||||
{#await promise}
|
||||
{#if !loaded || !selectedApp}
|
||||
<div class="loading">
|
||||
<ProgressCircle size="XL" />
|
||||
</div>
|
||||
{:then _}
|
||||
{:else}
|
||||
<Layout paddingX="XXL" paddingY="XL" gap="L">
|
||||
<span class="page-header" class:loaded>
|
||||
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
||||
|
@ -360,9 +359,7 @@
|
|||
>
|
||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
{:catch error}
|
||||
<p>Something went wrong: {error.message}</p>
|
||||
{/await}
|
||||
{/if}
|
||||
</Page>
|
||||
</span>
|
||||
|
||||
|
|
|
@ -16,53 +16,36 @@
|
|||
import RoleSelect from "components/common/RoleSelect.svelte"
|
||||
import { users, groups, apps, licensing } from "stores/portal"
|
||||
import AssignmentModal from "./AssignmentModal.svelte"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import { roles } from "stores/backend"
|
||||
import { API } from "api"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
|
||||
export let app
|
||||
|
||||
let assignmentModal
|
||||
let appGroups = []
|
||||
let appUsers = []
|
||||
let prevSearch = undefined,
|
||||
search = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let fixedAppId
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||
$: appGroups = $groups.filter(x => {
|
||||
return x.apps.includes(app.appId)
|
||||
const usersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
appId: apps.getProdAppID(app.devId),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
async function addData(appData) {
|
||||
let gr_prefix = "gr"
|
||||
let us_prefix = "us"
|
||||
appData.forEach(async data => {
|
||||
if (data.id.startsWith(gr_prefix)) {
|
||||
let matchedGroup = $groups.find(group => {
|
||||
return group._id === data.id
|
||||
})
|
||||
matchedGroup.apps.push(app.appId)
|
||||
matchedGroup.roles[fixedAppId] = data.role
|
||||
let assignmentModal
|
||||
let appGroups
|
||||
let appUsers
|
||||
|
||||
groups.actions.save(matchedGroup)
|
||||
} else if (data.id.startsWith(us_prefix)) {
|
||||
let matchedUser = $users.data.find(user => {
|
||||
return user._id === data.id
|
||||
})
|
||||
|
||||
let newUser = {
|
||||
...matchedUser,
|
||||
roles: { [fixedAppId]: data.role, ...matchedUser.roles },
|
||||
}
|
||||
|
||||
await users.save(newUser, { opts: { appId: fixedAppId } })
|
||||
await fetchUsers(page, search)
|
||||
}
|
||||
})
|
||||
await groups.actions.init()
|
||||
}
|
||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||
$: appUsers = $usersFetch.rows
|
||||
$: appGroups = $groups.filter(group => {
|
||||
if (!group.roles) {
|
||||
return false
|
||||
}
|
||||
return groups.actions.getGroupAppIds(group).includes(fixedAppId)
|
||||
})
|
||||
|
||||
async function removeUser(user) {
|
||||
// Remove the user role
|
||||
|
@ -74,67 +57,27 @@
|
|||
...filteredRoles,
|
||||
},
|
||||
})
|
||||
await fetchUsers(page, search)
|
||||
await usersFetch.refresh()
|
||||
}
|
||||
|
||||
async function removeGroup(group) {
|
||||
// Remove the user role
|
||||
let filteredApps = group.apps.filter(
|
||||
x => apps.extractAppId(x) !== app.appId
|
||||
)
|
||||
const filteredRoles = { ...group.roles }
|
||||
delete filteredRoles[fixedAppId]
|
||||
|
||||
await groups.actions.save({
|
||||
...group,
|
||||
apps: filteredApps,
|
||||
roles: { ...filteredRoles },
|
||||
})
|
||||
|
||||
await fetchUsers(page, search)
|
||||
await groups.actions.removeApp(group._id, fixedAppId)
|
||||
await groups.actions.init()
|
||||
await usersFetch.refresh()
|
||||
}
|
||||
|
||||
async function updateUserRole(role, user) {
|
||||
user.roles[fixedAppId] = role
|
||||
users.save(user)
|
||||
await users.save(user)
|
||||
}
|
||||
|
||||
async function updateGroupRole(role, group) {
|
||||
group.roles[fixedAppId] = role
|
||||
groups.actions.save(group)
|
||||
}
|
||||
|
||||
async function fetchUsers(page, search) {
|
||||
if ($pageInfo.loading) {
|
||||
return
|
||||
}
|
||||
// need to remove the page if they've started searching
|
||||
if (search && !prevSearch) {
|
||||
pageInfo.reset()
|
||||
page = undefined
|
||||
}
|
||||
prevSearch = search
|
||||
try {
|
||||
pageInfo.loading()
|
||||
await users.search({ page, appId: fixedAppId })
|
||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||
appUsers =
|
||||
$users.data?.filter(x => {
|
||||
return Object.keys(x.roles).find(y => {
|
||||
return y === fixedAppId
|
||||
})
|
||||
}) || []
|
||||
} catch (error) {
|
||||
notifications.error("Error getting user list")
|
||||
}
|
||||
await groups.actions.addApp(group._id, fixedAppId, role)
|
||||
await usersFetch.refresh()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await fetchUsers(page, search)
|
||||
|
||||
await groups.actions.init()
|
||||
await apps.load()
|
||||
await roles.fetch()
|
||||
} catch (error) {
|
||||
notifications.error(error)
|
||||
|
@ -149,11 +92,11 @@
|
|||
<Heading>Access</Heading>
|
||||
<div class="subtitle">
|
||||
<Body size="S">
|
||||
Assign users to your app and define their access here</Body
|
||||
>
|
||||
<Button on:click={assignmentModal.show} icon="User" cta
|
||||
>Assign users</Button
|
||||
>
|
||||
Assign users and groups to your app and define their access here
|
||||
</Body>
|
||||
<Button on:click={assignmentModal.show} icon="User" cta>
|
||||
Assign access
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if $licensing.groupsEnabled && appGroups.length}
|
||||
|
@ -169,8 +112,11 @@
|
|||
autoWidth
|
||||
quiet
|
||||
value={group.roles[
|
||||
Object.keys(group.roles).find(x => x === fixedAppId)
|
||||
groups.actions
|
||||
.getGroupAppIds(group)
|
||||
.find(x => x === fixedAppId)
|
||||
]}
|
||||
allowPublic={false}
|
||||
/>
|
||||
<Icon
|
||||
on:click={() => removeGroup(group)}
|
||||
|
@ -183,40 +129,37 @@
|
|||
</List>
|
||||
{/if}
|
||||
{#if appUsers.length}
|
||||
<List title="Users">
|
||||
{#each appUsers as user}
|
||||
<ListItem title={user.email} avatar>
|
||||
<RoleSelect
|
||||
on:change={e => updateUserRole(e.detail, user)}
|
||||
autoWidth
|
||||
quiet
|
||||
value={user.roles[
|
||||
Object.keys(user.roles).find(x => x === fixedAppId)
|
||||
]}
|
||||
/>
|
||||
<Icon
|
||||
on:click={() => removeUser(user)}
|
||||
hoverable
|
||||
size="S"
|
||||
name="Close"
|
||||
/>
|
||||
</ListItem>
|
||||
{/each}
|
||||
</List>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$pageInfo.pageNumber}
|
||||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
||||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
||||
goToPrevPage={async () => {
|
||||
await pageInfo.prevPage()
|
||||
fetchUsers(page, search)
|
||||
}}
|
||||
goToNextPage={async () => {
|
||||
await pageInfo.nextPage()
|
||||
fetchUsers(page, search)
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<List title="Users">
|
||||
{#each appUsers as user}
|
||||
<ListItem title={user.email} avatar>
|
||||
<RoleSelect
|
||||
on:change={e => updateUserRole(e.detail, user)}
|
||||
autoWidth
|
||||
quiet
|
||||
value={user.roles[
|
||||
Object.keys(user.roles).find(x => x === fixedAppId)
|
||||
]}
|
||||
allowPublic={false}
|
||||
/>
|
||||
<Icon
|
||||
on:click={() => removeUser(user)}
|
||||
hoverable
|
||||
size="S"
|
||||
name="Close"
|
||||
/>
|
||||
</ListItem>
|
||||
{/each}
|
||||
</List>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$usersFetch.pageNumber + 1}
|
||||
hasPrevPage={$usersFetch.hasPrevPage}
|
||||
hasNextPage={$usersFetch.hasNextPage}
|
||||
goToPrevPage={$usersFetch.loading ? null : fetch.prevPage}
|
||||
goToNextPage={$usersFetch.loading ? null : fetch.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
|
@ -224,14 +167,18 @@
|
|||
<Layout gap="S">
|
||||
<Heading>No users assigned</Heading>
|
||||
<div class="opacity">
|
||||
<Body size="S"
|
||||
>Assign users to your app and set their access here</Body
|
||||
>
|
||||
<Body size="S">
|
||||
Assign users/groups to your app and set their access here
|
||||
</Body>
|
||||
</div>
|
||||
<div class="padding">
|
||||
<Button on:click={() => assignmentModal.show()} cta icon="UserArrow"
|
||||
>Assign Users</Button
|
||||
<Button
|
||||
on:click={() => assignmentModal.show()}
|
||||
cta
|
||||
icon="UserArrow"
|
||||
>
|
||||
Assign access
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
@ -240,7 +187,7 @@
|
|||
</div>
|
||||
|
||||
<Modal bind:this={assignmentModal}>
|
||||
<AssignmentModal {app} {appUsers} {addData} />
|
||||
<AssignmentModal {app} {appUsers} on:update={usersFetch.refresh} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -5,37 +5,47 @@
|
|||
ActionButton,
|
||||
Layout,
|
||||
Icon,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { roles } from "stores/backend"
|
||||
import { groups, users, licensing } from "stores/portal"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import { groups, users, licensing, apps } from "stores/portal"
|
||||
import { Constants, RoleUtils, fetchData } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let app
|
||||
export let addData
|
||||
export let appUsers = []
|
||||
|
||||
let prevSearch = undefined,
|
||||
search = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let appData = [{ id: "", role: "" }]
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: fetchUsers(page, search)
|
||||
$: availableUsers = getAvailableUsers($users, appUsers, appData)
|
||||
$: filteredGroups = $groups.filter(group => {
|
||||
return !group.apps.find(appId => {
|
||||
return appId === app.appId
|
||||
})
|
||||
const dispatch = createEventDispatcher()
|
||||
const usersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
email: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
$: valid =
|
||||
appData?.length && !appData?.some(x => !x.id?.length || !x.role?.length)
|
||||
|
||||
let search = ""
|
||||
let data = [{ id: "", role: "" }]
|
||||
|
||||
$: usersFetch.update({
|
||||
query: {
|
||||
email: search,
|
||||
},
|
||||
})
|
||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||
$: availableUsers = getAvailableUsers($usersFetch.rows, appUsers, data)
|
||||
$: availableGroups = getAvailableGroups($groups, app.appId, search, data)
|
||||
$: console.log(availableGroups)
|
||||
$: valid = data?.length && !data?.some(x => !x.id?.length || !x.role?.length)
|
||||
$: optionSections = {
|
||||
...($licensing.groupsEnabled &&
|
||||
filteredGroups.length && {
|
||||
availableGroups.length && {
|
||||
["User groups"]: {
|
||||
data: filteredGroups,
|
||||
data: availableGroups,
|
||||
getLabel: group => group.name,
|
||||
getValue: group => group._id,
|
||||
getIcon: group => group.icon,
|
||||
|
@ -51,8 +61,45 @@
|
|||
},
|
||||
}
|
||||
|
||||
const addData = async appData => {
|
||||
const gr_prefix = "gr"
|
||||
const us_prefix = "us"
|
||||
for (let data of appData) {
|
||||
// Assign group
|
||||
if (data.id.startsWith(gr_prefix)) {
|
||||
const group = $groups.find(group => {
|
||||
return group._id === data.id
|
||||
})
|
||||
if (!group) {
|
||||
continue
|
||||
}
|
||||
await groups.actions.addApp(group._id, fixedAppId, data.role)
|
||||
}
|
||||
// Assign user
|
||||
else if (data.id.startsWith(us_prefix)) {
|
||||
const user = await users.get(data.id)
|
||||
await users.save({
|
||||
...user,
|
||||
roles: {
|
||||
...user.roles,
|
||||
[fixedAppId]: data.role,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh data when completed
|
||||
await usersFetch.refresh()
|
||||
dispatch("update")
|
||||
}
|
||||
|
||||
const getAvailableUsers = (allUsers, appUsers, newUsers) => {
|
||||
return (allUsers.data || []).filter(user => {
|
||||
return (allUsers || []).filter(user => {
|
||||
// Filter out admin users
|
||||
if (user?.admin?.global || user?.builder?.global) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out assigned users
|
||||
if (appUsers.find(x => x._id === user._id)) {
|
||||
return false
|
||||
|
@ -63,31 +110,31 @@
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchUsers(page, search) {
|
||||
if ($pageInfo.loading) {
|
||||
return
|
||||
}
|
||||
// need to remove the page if they've started searching
|
||||
if (search && !prevSearch) {
|
||||
pageInfo.reset()
|
||||
page = undefined
|
||||
}
|
||||
prevSearch = search
|
||||
try {
|
||||
pageInfo.loading()
|
||||
await users.search({ page, email: search })
|
||||
pageInfo.fetched($users.hasNextPage, $users.nextPage)
|
||||
} catch (error) {
|
||||
notifications.error("Error getting user list")
|
||||
}
|
||||
const getAvailableGroups = (allGroups, appId, search, newGroups) => {
|
||||
search = search?.toLowerCase()
|
||||
return (allGroups || []).filter(group => {
|
||||
// Filter out assigned groups
|
||||
const appIds = groups.actions.getGroupAppIds(group)
|
||||
if (appIds.includes(`app_${appId}`)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out new groups which are going to be assigned
|
||||
if (newGroups.find(x => x.id === group._id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Match search string
|
||||
return !search || group.name.toLowerCase().includes(search)
|
||||
})
|
||||
}
|
||||
|
||||
function addNewInput() {
|
||||
appData = [...appData, { id: "", role: "" }]
|
||||
data = [...data, { id: "", role: "" }]
|
||||
}
|
||||
|
||||
const removeItem = index => {
|
||||
appData = appData.filter((x, idx) => idx !== index)
|
||||
data = data.filter((x, idx) => idx !== index)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -96,20 +143,22 @@
|
|||
title="Assign users to your app"
|
||||
confirmText="Done"
|
||||
cancelText="Cancel"
|
||||
onConfirm={() => addData(appData)}
|
||||
onConfirm={() => addData(data)}
|
||||
showCloseIcon={false}
|
||||
disabled={!valid}
|
||||
>
|
||||
{#if appData?.length}
|
||||
{#if data.length}
|
||||
<Layout noPadding gap="XS">
|
||||
{#each appData as input, index}
|
||||
{#each data as input, index}
|
||||
<div class="item">
|
||||
<div class="picker">
|
||||
<PickerDropdown
|
||||
autocomplete
|
||||
showClearIcon={false}
|
||||
primaryOptions={optionSections}
|
||||
secondaryOptions={$roles}
|
||||
secondaryOptions={$roles.filter(
|
||||
x => x._id !== Constants.Roles.PUBLIC
|
||||
)}
|
||||
secondaryPlaceholder="Access"
|
||||
bind:primaryValue={input.id}
|
||||
bind:secondaryValue={input.role}
|
||||
|
|
|
@ -5,30 +5,56 @@
|
|||
import { store } from "builderStore"
|
||||
import clientPackage from "@budibase/client/package.json"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { users, auth, apps } from "stores/portal"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { users, auth, apps, groups } from "stores/portal"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GroupIcon from "../../manage/groups/_components/GroupIcon.svelte"
|
||||
|
||||
export let app
|
||||
export let deployments
|
||||
export let navigateTab
|
||||
let userCount
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const appUsersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
appId: apps.getProdAppID(app.devId),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let appEditor
|
||||
|
||||
$: updateAvailable = clientPackage.version !== $store.version
|
||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||
$: fetchAppEditor(appEditorId)
|
||||
$: appUsers = $appUsersFetch.rows || []
|
||||
$: appUsersFetch.update({
|
||||
query: {
|
||||
appId: apps.getProdAppID(app.devId),
|
||||
},
|
||||
})
|
||||
$: prodAppId = apps.getProdAppID(app.devId)
|
||||
$: appGroups = $groups.filter(group => {
|
||||
if (!group.roles) {
|
||||
return false
|
||||
}
|
||||
return groups.actions.getGroupAppIds(group).includes(prodAppId)
|
||||
})
|
||||
|
||||
const unpublishApp = () => {
|
||||
dispatch("unpublish", app)
|
||||
}
|
||||
|
||||
let appEditor, appEditorPromise
|
||||
|
||||
$: updateAvailable = clientPackage.version !== $store.version
|
||||
$: isPublished = app && app?.status === AppStatus.DEPLOYED
|
||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||
$: fetchAppEditor(appEditorId)
|
||||
|
||||
async function fetchAppEditor(editorId) {
|
||||
appEditorPromise = users.get(editorId)
|
||||
appEditor = await appEditorPromise
|
||||
appEditor = await users.get(editorId)
|
||||
}
|
||||
|
||||
const getInitials = user => {
|
||||
|
@ -36,16 +62,8 @@
|
|||
initials += user.firstName ? user.firstName[0] : ""
|
||||
initials += user.lastName ? user.lastName[0] : ""
|
||||
|
||||
return initials == "" ? user.email[0] : initials
|
||||
return initials === "" ? user.email[0] : initials
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
let resp = await users.getUserCountByApp({
|
||||
appId: apps.getProdAppID(app.devId),
|
||||
})
|
||||
userCount = resp.userCount
|
||||
await users.search({ appId: apps.getProdAppID(app.devId), limit: 4 })
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="overview-tab">
|
||||
|
@ -83,11 +101,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</DashCard>
|
||||
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
|
||||
<div class="last-edited-content">
|
||||
{#await appEditorPromise}
|
||||
<Avatar size="M" initials={"-"} />
|
||||
{:then _}
|
||||
{#if appEditor}
|
||||
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
|
||||
<div class="last-edited-content">
|
||||
<div class="updated-by">
|
||||
{#if appEditor}
|
||||
<Avatar size="M" initials={getInitials(appEditor)} />
|
||||
|
@ -96,22 +112,20 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:catch error}
|
||||
<p>Could not fetch user: {error.message}</p>
|
||||
{/await}
|
||||
<div class="last-edit-text">
|
||||
{#if app}
|
||||
{processStringSync(
|
||||
"Last edited {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() - new Date(app?.updatedAt).getTime(),
|
||||
}
|
||||
)}
|
||||
{/if}
|
||||
<div class="last-edit-text">
|
||||
{#if app}
|
||||
{processStringSync(
|
||||
"Last edited {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() - new Date(app?.updatedAt).getTime(),
|
||||
}
|
||||
)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashCard>
|
||||
</DashCard>
|
||||
{/if}
|
||||
<DashCard
|
||||
title={"App Version"}
|
||||
showIcon={true}
|
||||
|
@ -141,26 +155,44 @@
|
|||
{/if}
|
||||
</div>
|
||||
</DashCard>
|
||||
<DashCard
|
||||
title={"Access"}
|
||||
showIcon={true}
|
||||
action={() => {
|
||||
navigateTab("Access")
|
||||
}}
|
||||
dataCy={"access"}
|
||||
>
|
||||
<div class="last-edited-content">
|
||||
{#if $users?.data?.length}
|
||||
{#if $appUsersFetch.loaded}
|
||||
<DashCard
|
||||
title={"Access"}
|
||||
showIcon={true}
|
||||
action={() => {
|
||||
navigateTab("Access")
|
||||
}}
|
||||
dataCy={"access"}
|
||||
>
|
||||
{#if appUsers.length || appGroups.length}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="users-tab">
|
||||
{#each $users?.data as user}
|
||||
<Avatar size="M" initials={getInitials(user)} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="users-text">
|
||||
{userCount}
|
||||
{userCount > 1 ? `users have` : `user has`} access to this app
|
||||
<div class="access-tab-content">
|
||||
{#if appUsers.length}
|
||||
<div class="users">
|
||||
<div class="list">
|
||||
{#each appUsers.slice(0, 4) as user}
|
||||
<Avatar size="M" initials={getInitials(user)} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="text">
|
||||
{appUsers.length}
|
||||
{appUsers.length > 1 ? "users" : "user"} assigned
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if appGroups.length}
|
||||
<div class="groups">
|
||||
<div class="list">
|
||||
{#each appGroups.slice(0, 4) as group}
|
||||
<GroupIcon {group} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="text">
|
||||
{appGroups.length} user
|
||||
{appGroups.length > 1 ? "groups" : "group"} assigned
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
{:else}
|
||||
|
@ -171,8 +203,8 @@
|
|||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
</DashCard>
|
||||
</DashCard>
|
||||
{/if}
|
||||
</div>
|
||||
{#if false}
|
||||
<div class="bottom">
|
||||
|
@ -224,17 +256,29 @@
|
|||
.overview-tab .top {
|
||||
display: grid;
|
||||
grid-gap: var(--spectrum-alias-grid-gutter-medium);
|
||||
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
}
|
||||
|
||||
.users-tab {
|
||||
.access-tab-content {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.access-tab-content > * {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
.access-tab-content .list {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.access-tab-content .text {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.users-text {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.overview-tab .bottom,
|
||||
.automation-metrics {
|
||||
display: grid;
|
||||
|
@ -242,23 +286,6 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.overview-tab .top {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.overview-tab .bottom {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.overview-tab .top,
|
||||
.overview-tab .bottom {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.status-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
$: updateAvailable = clientPackage.version !== $store.version
|
||||
$: appUrl = `${window.origin}/app${app?.url}`
|
||||
$: appDeployed = app.status === AppStatus.DEPLOYED
|
||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||
</script>
|
||||
|
||||
<div class="settings-tab">
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
analytics.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Information</Heading>
|
||||
<Body size="S">Here you can update your logo and organization name.</Body>
|
||||
|
@ -110,7 +110,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{#if !$admin.cloud}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Platform</Heading>
|
||||
<Body size="S">Here you can set up general platform settings.</Body>
|
||||
|
@ -128,7 +128,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if !$admin.cloud}
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Analytics</Heading>
|
||||
<Body size="S">Choose whether to opt-in or opt-out of analytics.</Body>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<Heading size="M">Theming</Heading>
|
||||
<Body>Customize how Budibase looks and feels.</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">Builder theme</Label>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
latest features, security updates and much more.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
{#if version}
|
||||
<div>
|
||||
<Label size="L">Current version</Label>
|
||||
|
|
|
@ -101,18 +101,16 @@
|
|||
<Heading size="M">Upgrade</Heading>
|
||||
<Body size="M">
|
||||
{#if license.plan.type === "free"}
|
||||
Upgrade your budibase installation to unlock additional features. To
|
||||
subscribe to a plan visit your <Link size="L" href={upgradeUrl}
|
||||
>Account</Link
|
||||
>.
|
||||
Upgrade your Budibase installation to unlock additional features. To
|
||||
subscribe to a plan visit your
|
||||
<Link size="L" href={upgradeUrl}>Account</Link>.
|
||||
{:else}
|
||||
To manage your plan visit your <Link size="L" href={upgradeUrl}
|
||||
>Account</Link
|
||||
>.
|
||||
To manage your plan visit your
|
||||
<Link size="L" href={upgradeUrl}>Account</Link>.
|
||||
{/if}
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Activate</Heading>
|
||||
<Body size="S">Enter your license key below to activate your plan</Body>
|
||||
|
@ -120,7 +118,7 @@
|
|||
<Layout noPadding>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<Label size="L">License Key</Label>
|
||||
<Label size="L">License key</Label>
|
||||
<Input
|
||||
thin
|
||||
bind:value={licenseKey}
|
||||
|
@ -144,7 +142,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="L" noPadding>
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="S">Plan</Heading>
|
||||
|
|
|
@ -176,18 +176,18 @@
|
|||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<Layout>
|
||||
<Layout noPadding gap="S">
|
||||
<Layout noPadding>
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading>Usage</Heading>
|
||||
<Body
|
||||
>Get information about your current usage within Budibase.
|
||||
<Body>
|
||||
Get information about your current usage within Budibase.
|
||||
{#if accountPortalAccess}
|
||||
To upgrade your plan and usage limits visit your <Link
|
||||
on:click={goToAccountPortal}
|
||||
size="L">Account</Link
|
||||
>
|
||||
{:else}
|
||||
To upgrade your plan and usage limits contact your account holder
|
||||
To upgrade your plan and usage limits contact your account holder.
|
||||
{/if}
|
||||
</Body>
|
||||
</Layout>
|
||||
|
|
|
@ -8,14 +8,21 @@ const extractAppId = id => {
|
|||
}
|
||||
|
||||
const getProdAppID = appId => {
|
||||
if (!appId || !appId.startsWith("app_dev")) {
|
||||
if (!appId) {
|
||||
return appId
|
||||
}
|
||||
// split to take off the app_dev element, then join it together incase any other app_ exist
|
||||
const split = appId.split("app_dev")
|
||||
split.shift()
|
||||
const rest = split.join("app_dev")
|
||||
return `${"app"}${rest}`
|
||||
let rest,
|
||||
separator = ""
|
||||
if (appId.startsWith("app_dev")) {
|
||||
// split to take off the app_dev element, then join it together incase any other app_ exist
|
||||
const split = appId.split("app_dev")
|
||||
split.shift()
|
||||
rest = split.join("app_dev")
|
||||
} else if (!appId.startsWith("app")) {
|
||||
rest = appId
|
||||
separator = "_"
|
||||
}
|
||||
return `app${separator}${rest}`
|
||||
}
|
||||
|
||||
export function createAppStore() {
|
||||
|
|
|
@ -5,29 +5,41 @@ import { licensing } from "stores/portal"
|
|||
export function createGroupsStore() {
|
||||
const store = writable([])
|
||||
|
||||
const updateStore = group => {
|
||||
store.update(state => {
|
||||
const currentIdx = state.findIndex(gr => gr._id === group._id)
|
||||
if (currentIdx >= 0) {
|
||||
state.splice(currentIdx, 1, group)
|
||||
} else {
|
||||
state.push(group)
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const getGroup = async groupId => {
|
||||
const group = await API.getGroup(groupId)
|
||||
updateStore(group)
|
||||
}
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
// only init if these is a groups license, just to be sure but the feature will be blocked
|
||||
// only init if there is a groups license, just to be sure but the feature will be blocked
|
||||
// on the backend anyway
|
||||
if (get(licensing).groupsEnabled) {
|
||||
const users = await API.getGroups()
|
||||
store.set(users)
|
||||
const groups = await API.getGroups()
|
||||
store.set(groups)
|
||||
}
|
||||
},
|
||||
|
||||
get: getGroup,
|
||||
|
||||
save: async group => {
|
||||
const response = await API.saveGroup(group)
|
||||
group._id = response._id
|
||||
group._rev = response._rev
|
||||
store.update(state => {
|
||||
const currentIdx = state.findIndex(gr => gr._id === response._id)
|
||||
if (currentIdx >= 0) {
|
||||
state.splice(currentIdx, 1, group)
|
||||
} else {
|
||||
state.push(group)
|
||||
}
|
||||
return state
|
||||
})
|
||||
updateStore(group)
|
||||
return group
|
||||
},
|
||||
|
||||
delete: async group => {
|
||||
|
@ -40,6 +52,34 @@ export function createGroupsStore() {
|
|||
return state
|
||||
})
|
||||
},
|
||||
|
||||
addUser: async (groupId, userId) => {
|
||||
await API.addUsersToGroup(groupId, userId)
|
||||
// refresh the group enrichment
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
removeUser: async (groupId, userId) => {
|
||||
await API.removeUsersFromGroup(groupId, userId)
|
||||
// refresh the group enrichment
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
addApp: async (groupId, appId, roleId) => {
|
||||
await API.addAppsToGroup(groupId, [{ appId, roleId }])
|
||||
// refresh the group roles
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
removeApp: async (groupId, appId) => {
|
||||
await API.removeAppsFromGroup(groupId, [{ appId }])
|
||||
// refresh the group roles
|
||||
await getGroup(groupId)
|
||||
},
|
||||
|
||||
getGroupAppIds: group => {
|
||||
return Object.keys(group?.roles || {})
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { API } from "api"
|
|||
import { auth, admin } from "stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { StripeStatus } from "components/portal/licensing/constants"
|
||||
import { FEATURE_FLAGS, isEnabled } from "../../helpers/featureFlags"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
|
||||
export const createLicensingStore = () => {
|
||||
const DEFAULT = {
|
||||
|
@ -75,7 +75,7 @@ export const createLicensingStore = () => {
|
|||
})
|
||||
},
|
||||
setUsageMetrics: () => {
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||
const quota = get(store).quotaUsage
|
||||
const license = get(auth).user.license
|
||||
const now = new Date()
|
||||
|
|
|
@ -1,40 +1,91 @@
|
|||
export const buildGroupsEndpoints = API => ({
|
||||
/**
|
||||
* Creates a user group.
|
||||
* @param user the new group to create
|
||||
*/
|
||||
saveGroup: async group => {
|
||||
export const buildGroupsEndpoints = API => {
|
||||
// underlying functionality of adding/removing users/apps to groups
|
||||
async function updateGroupResource(groupId, resource, operation, ids) {
|
||||
if (!Array.isArray(ids)) {
|
||||
ids = [ids]
|
||||
}
|
||||
return await API.post({
|
||||
url: "/api/global/groups",
|
||||
body: group,
|
||||
url: `/api/global/groups/${groupId}/${resource}`,
|
||||
body: {
|
||||
[operation]: ids,
|
||||
},
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Gets all of the user groups
|
||||
*/
|
||||
getGroups: async () => {
|
||||
return await API.get({
|
||||
url: "/api/global/groups",
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a group by ID
|
||||
*/
|
||||
getGroup: async id => {
|
||||
return await API.get({
|
||||
url: `/api/global/groups/${id}`,
|
||||
})
|
||||
},
|
||||
return {
|
||||
/**
|
||||
* Creates a user group.
|
||||
* @param group the new group to create
|
||||
*/
|
||||
saveGroup: async group => {
|
||||
return await API.post({
|
||||
url: "/api/global/groups",
|
||||
body: group,
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Gets all the user groups
|
||||
*/
|
||||
getGroups: async () => {
|
||||
return await API.get({
|
||||
url: "/api/global/groups",
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a user group
|
||||
* @param id the id of the config to delete
|
||||
* @param rev the revision of the config to delete
|
||||
*/
|
||||
deleteGroup: async ({ id, rev }) => {
|
||||
return await API.delete({
|
||||
url: `/api/global/groups/${id}/${rev}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
/**
|
||||
* Gets a group by ID
|
||||
*/
|
||||
getGroup: async id => {
|
||||
return await API.get({
|
||||
url: `/api/global/groups/${id}`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a user group
|
||||
* @param id the id of the config to delete
|
||||
* @param rev the revision of the config to delete
|
||||
*/
|
||||
deleteGroup: async ({ id, rev }) => {
|
||||
return await API.delete({
|
||||
url: `/api/global/groups/${id}/${rev}`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds users to a group
|
||||
* @param groupId The group to update
|
||||
* @param userIds The user IDs to be added
|
||||
*/
|
||||
addUsersToGroup: async (groupId, userIds) => {
|
||||
return updateGroupResource(groupId, "users", "add", userIds)
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes users from a group
|
||||
* @param groupId The group to update
|
||||
* @param userIds The user IDs to be removed
|
||||
*/
|
||||
removeUsersFromGroup: async (groupId, userIds) => {
|
||||
return updateGroupResource(groupId, "users", "remove", userIds)
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds apps to a group
|
||||
* @param groupId The group to update
|
||||
* @param appArray Array of objects, containing the appId and roleId to be added
|
||||
*/
|
||||
addAppsToGroup: async (groupId, appArray) => {
|
||||
return updateGroupResource(groupId, "apps", "add", appArray)
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes apps from a group
|
||||
* @param groupId The group to update
|
||||
* @param appArray Array of objects, containing the appId to be removed
|
||||
*/
|
||||
removeAppsFromGroup: async (groupId, appArray) => {
|
||||
return updateGroupResource(groupId, "apps", "remove", appArray)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,15 +86,19 @@ export const buildUserEndpoints = API => ({
|
|||
/**
|
||||
* Creates multiple users.
|
||||
* @param users the array of user objects to create
|
||||
* @param groups the array of group ids to add all users to
|
||||
*/
|
||||
createUsers: async ({ users, groups }) => {
|
||||
return await API.post({
|
||||
url: "/api/global/users/bulkCreate",
|
||||
const res = await API.post({
|
||||
url: "/api/global/users/bulk",
|
||||
body: {
|
||||
users,
|
||||
groups,
|
||||
create: {
|
||||
users,
|
||||
groups,
|
||||
},
|
||||
},
|
||||
})
|
||||
return res.created
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -109,15 +113,18 @@ export const buildUserEndpoints = API => ({
|
|||
|
||||
/**
|
||||
* Deletes multiple users
|
||||
* @param userId the ID of the user to delete
|
||||
* @param userIds the ID of the user to delete
|
||||
*/
|
||||
deleteUsers: async userIds => {
|
||||
return await API.post({
|
||||
url: `/api/global/users/bulkDelete`,
|
||||
const res = await API.post({
|
||||
url: `/api/global/users/bulk`,
|
||||
body: {
|
||||
userIds,
|
||||
delete: {
|
||||
userIds,
|
||||
},
|
||||
},
|
||||
})
|
||||
return res.deleted
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -151,6 +158,7 @@ export const buildUserEndpoints = API => ({
|
|||
userInfo: {
|
||||
admin: user.admin ? { global: true } : undefined,
|
||||
builder: user.admin || user.builder ? { global: true } : undefined,
|
||||
groups: user.groups,
|
||||
},
|
||||
})),
|
||||
})
|
||||
|
|
|
@ -158,6 +158,8 @@ export default class DataFetch {
|
|||
schema,
|
||||
query,
|
||||
loading: true,
|
||||
cursors: [],
|
||||
cursor: null,
|
||||
}))
|
||||
|
||||
// Actually fetch data
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
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 getDefinition() {
|
||||
return {
|
||||
schema: {},
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,12 +5,14 @@ import RelationshipFetch from "./RelationshipFetch.js"
|
|||
import NestedProviderFetch from "./NestedProviderFetch.js"
|
||||
import FieldFetch from "./FieldFetch.js"
|
||||
import JSONArrayFetch from "./JSONArrayFetch.js"
|
||||
import UserFetch from "./UserFetch.js"
|
||||
|
||||
const DataFetchMap = {
|
||||
table: TableFetch,
|
||||
view: ViewFetch,
|
||||
query: QueryFetch,
|
||||
link: RelationshipFetch,
|
||||
user: UserFetch,
|
||||
|
||||
// Client specific datasource types
|
||||
provider: NestedProviderFetch,
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
--spectrum-global-color-static-blue-600: #5680b4;
|
||||
--spectrum-global-color-static-blue-700: #4e79af;
|
||||
--spectrum-global-color-static-blue-800: #4a73a6;
|
||||
--spectrum-global-color-static-blue: var(--spectrum-global-color-blue-600);
|
||||
|
||||
--spectrum-global-color-gray-50: #2e3440;
|
||||
--spectrum-global-color-gray-75: #353b4a;
|
||||
|
|
|
@ -19,3 +19,24 @@ 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 () => {
|
||||
resolve(await callback(...params))
|
||||
}, minDelay)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,14 +47,9 @@ import { checkAppMetadata } from "../../automations/logging"
|
|||
import { getUniqueRows } from "../../utilities/usageQuota/rows"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { errors, events, migrations } from "@budibase/backend-core"
|
||||
import {
|
||||
App,
|
||||
Layout,
|
||||
Screen,
|
||||
MigrationType,
|
||||
AppNavigation,
|
||||
} from "@budibase/types"
|
||||
import { App, Layout, Screen, MigrationType } from "@budibase/types"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||
import { groups } from "@budibase/pro"
|
||||
|
||||
const URL_REGEX_SLASH = /\/|\\/g
|
||||
|
||||
|
@ -501,6 +496,7 @@ const preDestroyApp = async (ctx: any) => {
|
|||
|
||||
const postDestroyApp = async (ctx: any) => {
|
||||
const rowCount = ctx.rowCount
|
||||
await groups.cleanupApp(ctx.params.appId)
|
||||
if (rowCount) {
|
||||
await quotas.removeRows(rowCount)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
const { outputProcessing } = require("../../utilities/rowProcessor")
|
||||
const { InternalTables } = require("../../db/utils")
|
||||
const { getFullUser } = require("../../utilities/users")
|
||||
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles")
|
||||
const { getAppDB, getAppId } = require("@budibase/backend-core/context")
|
||||
import { outputProcessing } from "../../utilities/rowProcessor"
|
||||
import { InternalTables } from "../../db/utils"
|
||||
import { getFullUser } from "../../utilities/users"
|
||||
import { roles, context } from "@budibase/backend-core"
|
||||
import { groups } from "@budibase/pro"
|
||||
|
||||
const PUBLIC_ROLE = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||
|
||||
/**
|
||||
* Add the attributes that are session based to the current user.
|
||||
*/
|
||||
const addSessionAttributesToUser = ctx => {
|
||||
const addSessionAttributesToUser = (ctx: any) => {
|
||||
if (ctx.user) {
|
||||
ctx.body.license = ctx.user.license
|
||||
}
|
||||
}
|
||||
|
||||
exports.fetchSelf = async ctx => {
|
||||
export async function fetchSelf(ctx: any) {
|
||||
let userId = ctx.user.userId || ctx.user._id
|
||||
/* istanbul ignore next */
|
||||
if (!userId || !ctx.isAuthenticated) {
|
||||
|
@ -21,30 +23,30 @@ exports.fetchSelf = async ctx => {
|
|||
return
|
||||
}
|
||||
|
||||
const appId = context.getAppId()
|
||||
const user = await getFullUser(ctx, userId)
|
||||
// this shouldn't be returned by the app self
|
||||
delete user.roles
|
||||
// forward the csrf token from the session
|
||||
user.csrfToken = ctx.user.csrfToken
|
||||
|
||||
if (getAppId()) {
|
||||
const db = getAppDB()
|
||||
if (appId) {
|
||||
const db = context.getAppDB()
|
||||
// check for group permissions
|
||||
if (!user.roleId || user.roleId === PUBLIC_ROLE) {
|
||||
const groupRoleId = await groups.getGroupRoleId(user, appId)
|
||||
user.roleId = groupRoleId || user.roleId
|
||||
}
|
||||
// remove the full roles structure
|
||||
delete user.roles
|
||||
try {
|
||||
const userTable = await db.get(InternalTables.USER_METADATA)
|
||||
const metadata = await db.get(userId)
|
||||
// make sure there is never a stale csrf token
|
||||
delete metadata.csrfToken
|
||||
// specifically needs to make sure is enriched
|
||||
ctx.body = await outputProcessing(userTable, {
|
||||
...user,
|
||||
...metadata,
|
||||
})
|
||||
} catch (err) {
|
||||
ctx.body = await outputProcessing(userTable, user)
|
||||
} catch (err: any) {
|
||||
let response
|
||||
// user didn't exist in app, don't pretend they do
|
||||
if (user.roleId === BUILTIN_ROLE_IDS.PUBLIC) {
|
||||
if (user.roleId === PUBLIC_ROLE) {
|
||||
response = {}
|
||||
}
|
||||
// user has a role of some sort, return them
|
|
@ -8,7 +8,7 @@ exports.fetch = async function (ctx) {
|
|||
const defs = await getDefinitions()
|
||||
|
||||
// for google sheets integration google verification
|
||||
if (featureFlags.isEnabled(featureFlags.FeatureFlag.GOOGLE_SHEETS)) {
|
||||
if (featureFlags.isEnabled(featureFlags.TenantFeatureFlag.GOOGLE_SHEETS)) {
|
||||
defs[SourceName.GOOGLE_SHEETS] = googlesheets.schema
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/analytics")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router.get("/api/bbtel", controller.isEnabled)
|
||||
router.post("/api/bbtel/ping", controller.ping)
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/apikeys")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/keys", authorized(BUILDER), controller.fetch)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/auth")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get("/api/self", controller.fetchSelf)
|
||||
|
||||
module.exports = router
|
|
@ -0,0 +1,8 @@
|
|||
import Router from "@koa/router"
|
||||
import * as controller from "../controllers/auth"
|
||||
|
||||
const router = new Router()
|
||||
|
||||
router.get("/api/self", controller.fetchSelf)
|
||||
|
||||
export default router
|
|
@ -13,7 +13,7 @@ const {
|
|||
} = require("../../middleware/appInfo")
|
||||
const { automationValidator } = require("./utils/validators")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get(
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/backup")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router.get("/api/backups/export", authorized(BUILDER), controller.exportAppDump)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/cloud")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/cloud/export", authorized(BUILDER), controller.exportApps)
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/component")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router.get(
|
||||
"/api/:appId/components/definitions",
|
||||
|
|
|
@ -11,7 +11,7 @@ const {
|
|||
datasourceQueryValidator,
|
||||
} = require("./utils/validators")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/datasources", authorized(BUILDER), datasourceController.fetch)
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/deploy")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/deployments", authorized(BUILDER), controller.fetchDeployments)
|
||||
|
|
|
@ -4,7 +4,7 @@ const env = require("../../environment")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
function redirectPath(path) {
|
||||
router
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/integration")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/integrations", authorized(BUILDER), controller.fetch)
|
||||
|
|
|
@ -3,7 +3,7 @@ const authorized = require("../../middleware/authorized")
|
|||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
const controller = require("../controllers/layout")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.post("/api/layouts", authorized(BUILDER), controller.save)
|
||||
|
|
|
@ -7,7 +7,7 @@ const {
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.post(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const Router = require("@koa/router")
|
||||
const migrationsController = require("../controllers/migrations")
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
const { internalApi } = require("@budibase/backend-core/auth")
|
||||
|
||||
router
|
||||
|
|
|
@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized")
|
|||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
const { permissionValidator } = require("./utils/validators")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/permission/builtin", authorized(BUILDER), controller.fetchBuiltin)
|
||||
|
|
|
@ -16,7 +16,7 @@ const {
|
|||
generateQueryValidation,
|
||||
} = require("../controllers/query/validation")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/queries", authorized(BUILDER), queryController.fetch)
|
||||
|
|
|
@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized")
|
|||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
const { roleValidator } = require("./utils/validators")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.post("/api/roles", authorized(BUILDER), roleValidator(), controller.save)
|
||||
|
|
|
@ -3,7 +3,7 @@ const authorized = require("../../middleware/authorized")
|
|||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
const controller = require("../controllers/routing")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
// gets correct structure for user role
|
||||
|
|
|
@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized")
|
|||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
const { screenValidator } = require("./utils/validators")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/screens", authorized(BUILDER), controller.fetch)
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/script")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router.post("/api/script", authorized(BUILDER), controller.save)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ const {
|
|||
} = require("@budibase/backend-core/permissions")
|
||||
const { tableValidator } = require("./utils/validators")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ const controller = require("../controllers/templates")
|
|||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/templates", authorized(BUILDER), controller.fetch)
|
||||
|
|
|
@ -6,7 +6,7 @@ const {
|
|||
PermissionTypes,
|
||||
} = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get(
|
||||
|
|
|
@ -9,7 +9,7 @@ const {
|
|||
PermissionLevels,
|
||||
} = require("@budibase/backend-core/permissions")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/views/export", authorized(BUILDER), viewController.exportView)
|
||||
|
|
|
@ -4,7 +4,7 @@ const authorized = require("../../middleware/authorized")
|
|||
const { BUILDER } = require("@budibase/backend-core/permissions")
|
||||
const { webhookValidator } = require("./utils/validators")
|
||||
|
||||
const router = Router()
|
||||
const router = new Router()
|
||||
|
||||
router
|
||||
.get("/api/webhooks", authorized(BUILDER), controller.fetch)
|
||||
|
|
|
@ -52,9 +52,9 @@ const checkAuthorizedResource = async (
|
|||
) => {
|
||||
// get the user's roles
|
||||
const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC
|
||||
const userRoles = await getUserRoleHierarchy(roleId, {
|
||||
const userRoles = (await getUserRoleHierarchy(roleId, {
|
||||
idOnly: false,
|
||||
})
|
||||
})) as { _id: string }[]
|
||||
const permError = "User does not have permission"
|
||||
// check if the user has the required role
|
||||
if (resourceRoles.length > 0) {
|
||||
|
|
|
@ -43,9 +43,10 @@ exports.updateAppRole = (user, { appId } = {}) => {
|
|||
}
|
||||
|
||||
async function checkGroupRoles(user, { appId } = {}) {
|
||||
let roleId = await groups.getGroupRoleId(user, appId)
|
||||
user.roleId = roleId
|
||||
|
||||
if (user.roleId && user.roleId !== BUILTIN_ROLE_IDS.PUBLIC) {
|
||||
return user
|
||||
}
|
||||
user.roleId = await groups.getGroupRoleId(user, appId)
|
||||
return user
|
||||
}
|
||||
|
||||
|
@ -74,8 +75,9 @@ exports.getRawGlobalUser = async userId => {
|
|||
}
|
||||
|
||||
exports.getGlobalUser = async userId => {
|
||||
const appId = getAppId()
|
||||
let user = await exports.getRawGlobalUser(userId)
|
||||
return processUser(user)
|
||||
return processUser(user, { appId })
|
||||
}
|
||||
|
||||
exports.getGlobalUsers = async (users = null) => {
|
||||
|
|
|
@ -2,10 +2,11 @@ const { InternalTables } = require("../db/utils")
|
|||
const { getGlobalUser } = require("../utilities/global")
|
||||
const { getAppDB } = require("@budibase/backend-core/context")
|
||||
const { getProdAppID } = require("@budibase/backend-core/db")
|
||||
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles")
|
||||
|
||||
exports.getFullUser = async (ctx, userId) => {
|
||||
const global = await getGlobalUser(userId)
|
||||
let metadata
|
||||
let metadata = {}
|
||||
try {
|
||||
// this will throw an error if the db doesn't exist, or there is no appId
|
||||
const db = getAppDB()
|
||||
|
@ -15,9 +16,11 @@ exports.getFullUser = async (ctx, userId) => {
|
|||
delete global._id
|
||||
delete global._rev
|
||||
}
|
||||
delete metadata.csrfToken
|
||||
return {
|
||||
...global,
|
||||
...metadata,
|
||||
...global,
|
||||
roleId: global.roleId || BUILTIN_ROLE_IDS.PUBLIC,
|
||||
tableId: InternalTables.USER_METADATA,
|
||||
// make sure the ID is always a local ID, not a global one
|
||||
_id: userId,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue