commit
4b5218e97f
217 changed files with 3457 additions and 4591 deletions
|
@ -8,3 +8,4 @@ packages/builder/.routify
|
|||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/builder/cypress/reports
|
||||
packages/sdk/sdk
|
16
.github/workflows/budibase_ci.yml
vendored
16
.github/workflows/budibase_ci.yml
vendored
|
@ -23,6 +23,15 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
couchdb:
|
||||
image: ibmcom/couchdb3
|
||||
env:
|
||||
COUCHDB_PASSWORD: budibase
|
||||
COUCHDB_USER: budibase
|
||||
ports:
|
||||
- 4567:5984
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
|
@ -53,13 +62,6 @@ jobs:
|
|||
name: codecov-umbrella
|
||||
verbose: true
|
||||
|
||||
# TODO: parallelise this
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
install: false
|
||||
command: yarn test:e2e:ci
|
||||
|
||||
- name: QA Core Integration Tests
|
||||
run: |
|
||||
cd qa-core
|
||||
|
|
1
.github/workflows/release-develop.yml
vendored
1
.github/workflows/release-develop.yml
vendored
|
@ -47,6 +47,7 @@ jobs:
|
|||
- run: yarn bootstrap
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
- run: yarn build:sdk
|
||||
- run: yarn test
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
|
|
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
|
@ -56,6 +56,7 @@ jobs:
|
|||
- run: yarn bootstrap
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
- run: yarn build:sdk
|
||||
- run: yarn test
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
|
|
|
@ -9,3 +9,4 @@ packages/server/src/definitions/openapi.ts
|
|||
packages/builder/.routify
|
||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/sdk/sdk
|
|
@ -76,6 +76,7 @@ affinity: {}
|
|||
globals:
|
||||
appVersion: "latest"
|
||||
budibaseEnv: PRODUCTION
|
||||
tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS"
|
||||
enableAnalytics: "1"
|
||||
sentryDSN: ""
|
||||
posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
|
||||
declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONMENT" "CLUSTER_PORT" "DEPLOYMENT_ENVIRONMENT" "MINIO_URL" "NODE_ENV" "POSTHOG_TOKEN" "REDIS_URL" "SELF_HOSTED" "WORKER_PORT" "WORKER_URL")
|
||||
declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONMENT" "CLUSTER_PORT" "DEPLOYMENT_ENVIRONMENT" "MINIO_URL" "NODE_ENV" "POSTHOG_TOKEN" "REDIS_URL" "SELF_HOSTED" "WORKER_PORT" "WORKER_URL" "TENANT_FEATURE_FLAGS" "ACCOUNT_PORTAL_URL")
|
||||
# Check the env vars set in Dockerfile have come through, AAS seems to drop them
|
||||
[[ -z "${APP_PORT}" ]] && export APP_PORT=4001
|
||||
[[ -z "${ARCHITECTURE}" ]] && export ARCHITECTURE=amd
|
||||
|
@ -10,6 +10,8 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
|
|||
[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000
|
||||
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
|
||||
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||
[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS"
|
||||
[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app
|
||||
[[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379
|
||||
[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
|
||||
[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.4.17",
|
||||
"version": "1.4.18-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh",
|
||||
"build": "lerna run build",
|
||||
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"build:sdk": "lerna run build:sdk",
|
||||
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.4.17",
|
||||
"version": "1.4.18-alpha.0",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "^1.4.17",
|
||||
"@budibase/types": "1.4.18-alpha.0",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
|
|
|
@ -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 createAccountEmailView = async () => {
|
||||
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()
|
||||
export const createNewUserEmailView = async () => {
|
||||
const db = getGlobalDB()
|
||||
const viewJs = `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) {
|
||||
emit(doc.email.toLowerCase(), doc._id)
|
||||
}
|
||||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
}`
|
||||
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)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewName.ACCOUNT_BY_EMAIL]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}`
|
||||
await doWithDB(
|
||||
StaticDatabases.PLATFORM_INFO.name,
|
||||
async (db: PouchDB.Database) => {
|
||||
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) {
|
||||
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) {
|
||||
const viewJs = `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)
|
||||
}`
|
||||
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) {
|
||||
const viewJs = `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)
|
||||
}`
|
||||
await createView(db, viewJs, ViewName.USER_BY_BUILDERS)
|
||||
}
|
||||
|
||||
export const createPlatformUserView = async () => {
|
||||
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) {
|
||||
const viewJs = `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 doWithDB(
|
||||
StaticDatabases.PLATFORM_INFO.name,
|
||||
async (db: PouchDB.Database) => {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -23,9 +23,11 @@ export default class LoggingProcessor implements EventProcessor {
|
|||
return
|
||||
}
|
||||
let timestampString = getTimestampString(timestamp)
|
||||
console.log(
|
||||
`[audit] [tenant=${identity.tenantId}] [identityType=${identity.type}] [identity=${identity.id}] ${timestampString} ${event} `
|
||||
)
|
||||
let message = `[audit] [tenant=${identity.tenantId}] [identityType=${identity.type}] [identity=${identity.id}] ${timestampString} ${event} `
|
||||
if (env.isDev()) {
|
||||
message = message + `[debug: [properties=${JSON.stringify(properties)}] ]`
|
||||
}
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
async identify(identity: Identity, timestamp?: string | number) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,27 +1,78 @@
|
|||
import { publishEvent } from "../events"
|
||||
import {
|
||||
Event,
|
||||
License,
|
||||
LicenseActivatedEvent,
|
||||
LicenseDowngradedEvent,
|
||||
LicenseUpdatedEvent,
|
||||
LicenseUpgradedEvent,
|
||||
LicensePlanChangedEvent,
|
||||
LicenseTierChangedEvent,
|
||||
PlanType,
|
||||
Account,
|
||||
LicensePortalOpenedEvent,
|
||||
LicenseCheckoutSuccessEvent,
|
||||
LicenseCheckoutOpenedEvent,
|
||||
LicensePaymentFailedEvent,
|
||||
LicensePaymentRecoveredEvent,
|
||||
} from "@budibase/types"
|
||||
|
||||
// TODO
|
||||
export async function updgraded(license: License) {
|
||||
const properties: LicenseUpgradedEvent = {}
|
||||
await publishEvent(Event.LICENSE_UPGRADED, properties)
|
||||
export async function tierChanged(account: Account, from: number, to: number) {
|
||||
const properties: LicenseTierChangedEvent = {
|
||||
accountId: account.accountId,
|
||||
to,
|
||||
from,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_TIER_CHANGED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
export async function downgraded(license: License) {
|
||||
const properties: LicenseDowngradedEvent = {}
|
||||
await publishEvent(Event.LICENSE_DOWNGRADED, properties)
|
||||
export async function planChanged(
|
||||
account: Account,
|
||||
from: PlanType,
|
||||
to: PlanType
|
||||
) {
|
||||
const properties: LicensePlanChangedEvent = {
|
||||
accountId: account.accountId,
|
||||
to,
|
||||
from,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_PLAN_CHANGED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
export async function activated(license: License) {
|
||||
const properties: LicenseActivatedEvent = {}
|
||||
export async function activated(account: Account) {
|
||||
const properties: LicenseActivatedEvent = {
|
||||
accountId: account.accountId,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_ACTIVATED, properties)
|
||||
}
|
||||
|
||||
export async function checkoutOpened(account: Account) {
|
||||
const properties: LicenseCheckoutOpenedEvent = {
|
||||
accountId: account.accountId,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_CHECKOUT_OPENED, properties)
|
||||
}
|
||||
|
||||
export async function checkoutSuccess(account: Account) {
|
||||
const properties: LicenseCheckoutSuccessEvent = {
|
||||
accountId: account.accountId,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_CHECKOUT_SUCCESS, properties)
|
||||
}
|
||||
|
||||
export async function portalOpened(account: Account) {
|
||||
const properties: LicensePortalOpenedEvent = {
|
||||
accountId: account.accountId,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_PORTAL_OPENED, properties)
|
||||
}
|
||||
|
||||
export async function paymentFailed(account: Account) {
|
||||
const properties: LicensePaymentFailedEvent = {
|
||||
accountId: account.accountId,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_PAYMENT_FAILED, properties)
|
||||
}
|
||||
|
||||
export async function paymentRecovered(account: Account) {
|
||||
const properties: LicensePaymentRecoveredEvent = {
|
||||
accountId: account.accountId,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_PAYMENT_RECOVERED, 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
|
||||
|
|
|
@ -75,6 +75,15 @@ function validateDatasource(schema) {
|
|||
})
|
||||
.unknown(true)
|
||||
.required(),
|
||||
extra: joi.object().pattern(
|
||||
joi.string(),
|
||||
joi.object({
|
||||
type: joi.string().required(),
|
||||
displayName: joi.string().required(),
|
||||
required: joi.boolean(),
|
||||
data: joi.object(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
runJoi(validator, schema)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.4.17",
|
||||
"version": "1.4.18-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.4.17",
|
||||
"@budibase/string-templates": "1.4.18-alpha.0",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -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
|
||||
<IconAvatar
|
||||
size="S"
|
||||
name={primaryOptions[title].getIcon(option)}
|
||||
icon={primaryOptions[title].getIcon(option)}
|
||||
background={primaryOptions[title].getColour(option)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{: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}
|
||||
|
|
58
packages/bbui/src/Icon/IconAvatar.svelte
Normal file
58
packages/bbui/src/Icon/IconAvatar.svelte
Normal file
|
@ -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"
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
const cypressConfig = require("../cypress.json")
|
||||
const path = require("path")
|
||||
|
||||
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
||||
|
||||
// normal development system
|
||||
const SERVER_PORT = cypressConfig.env.PORT
|
||||
const WORKER_PORT = cypressConfig.env.WORKER_PORT
|
||||
|
||||
process.env.NODE_ENV = "cypress"
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = "cypress"
|
||||
}
|
||||
process.env.ENABLE_ANALYTICS = "0"
|
||||
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
|
||||
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
|
||||
process.env.SELF_HOSTED = 1
|
||||
process.env.WORKER_URL = `http://localhost:${WORKER_PORT}/`
|
||||
process.env.APPS_URL = `http://localhost:${SERVER_PORT}/`
|
||||
|
|
|
@ -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 })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.4.17",
|
||||
"version": "1.4.18-alpha.0",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -71,10 +71,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.4.17",
|
||||
"@budibase/client": "^1.4.17",
|
||||
"@budibase/frontend-core": "^1.4.17",
|
||||
"@budibase/string-templates": "^1.4.17",
|
||||
"@budibase/bbui": "1.4.18-alpha.0",
|
||||
"@budibase/client": "1.4.18-alpha.0",
|
||||
"@budibase/frontend-core": "1.4.18-alpha.0",
|
||||
"@budibase/string-templates": "1.4.18-alpha.0",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -9,14 +9,14 @@ import {
|
|||
import { store } from "builderStore"
|
||||
import {
|
||||
queries as queriesStores,
|
||||
roles as rolesStore,
|
||||
tables as tablesStore,
|
||||
roles as rolesStore,
|
||||
} from "stores/backend"
|
||||
import {
|
||||
makePropSafe,
|
||||
isJSBinding,
|
||||
decodeJSBinding,
|
||||
encodeJSBinding,
|
||||
isJSBinding,
|
||||
makePropSafe,
|
||||
} from "@budibase/string-templates"
|
||||
import { TableNames } from "../constants"
|
||||
import { JSONUtils } from "@budibase/frontend-core"
|
||||
|
@ -71,17 +71,19 @@ export const getAuthBindings = () => {
|
|||
runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`,
|
||||
readable: `Current User.OAuthToken`,
|
||||
key: "accessToken",
|
||||
display: { name: "OAuthToken" },
|
||||
},
|
||||
]
|
||||
|
||||
bindings = Object.keys(authBindings).map(key => {
|
||||
const fieldBinding = authBindings[key]
|
||||
bindings = authBindings.map(fieldBinding => {
|
||||
return {
|
||||
type: "context",
|
||||
runtimeBinding: fieldBinding.runtime,
|
||||
readableBinding: fieldBinding.readable,
|
||||
fieldSchema: { type: "string", name: fieldBinding.key },
|
||||
providerId: "user",
|
||||
category: "Current User",
|
||||
display: fieldBinding.display,
|
||||
}
|
||||
})
|
||||
return bindings
|
||||
|
@ -93,7 +95,7 @@ export const getAuthBindings = () => {
|
|||
* @param {string} prefix A contextual string prefix/path for a user readable binding
|
||||
* @return {object[]} An array containing readable/runtime binding objects
|
||||
*/
|
||||
export const toBindingsArray = (valueMap, prefix) => {
|
||||
export const toBindingsArray = (valueMap, prefix, category) => {
|
||||
if (!valueMap) {
|
||||
return []
|
||||
}
|
||||
|
@ -101,11 +103,20 @@ export const toBindingsArray = (valueMap, prefix) => {
|
|||
if (!binding || !valueMap[binding]) {
|
||||
return acc
|
||||
}
|
||||
acc.push({
|
||||
|
||||
let config = {
|
||||
type: "context",
|
||||
runtimeBinding: binding,
|
||||
readableBinding: `${prefix}.${binding}`,
|
||||
})
|
||||
icon: "Brackets",
|
||||
}
|
||||
|
||||
if (category) {
|
||||
config.category = category
|
||||
}
|
||||
|
||||
acc.push(config)
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
@ -382,9 +393,11 @@ export const getUserBindings = () => {
|
|||
const { schema } = getSchemaForTable(TableNames.USERS)
|
||||
const keys = Object.keys(schema).sort()
|
||||
const safeUser = makePropSafe("user")
|
||||
keys.forEach(key => {
|
||||
|
||||
bindings = keys.reduce((acc, key) => {
|
||||
const fieldSchema = schema[key]
|
||||
bindings.push({
|
||||
if (fieldSchema.type !== "link") {
|
||||
acc.push({
|
||||
type: "context",
|
||||
runtimeBinding: `${safeUser}.${makePropSafe(key)}`,
|
||||
readableBinding: `Current User.${key}`,
|
||||
|
@ -394,9 +407,11 @@ export const getUserBindings = () => {
|
|||
providerId: "user",
|
||||
category: "Current User",
|
||||
icon: "User",
|
||||
display: fieldSchema,
|
||||
})
|
||||
})
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +8,7 @@
|
|||
let accountDowngradeModal
|
||||
|
||||
$: accountUrl = $admin.accountPortalUrl
|
||||
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||
$: billingUrl = `${accountUrl}/portal/billing`
|
||||
|
||||
export function show() {
|
||||
accountDowngradeModal.show()
|
||||
|
@ -24,10 +24,10 @@
|
|||
title="Your account is now on the Free plan"
|
||||
size="M"
|
||||
showCancelButton={$auth.user.accountPortalAccess}
|
||||
confirmText={$auth.user.accountPortalAccess ? "Upgrade" : "Confirm"}
|
||||
confirmText={$auth.user.accountPortalAccess ? "Billing" : "Confirm"}
|
||||
onConfirm={$auth.user.accountPortalAccess
|
||||
? () => {
|
||||
window.location.href = upgradeUrl
|
||||
window.location.href = billingUrl
|
||||
}
|
||||
: null}
|
||||
>
|
||||
|
@ -35,7 +35,9 @@
|
|||
The payment for your subscription has failed and we have downgraded your
|
||||
account to the <span class="free-plan">Free plan</span>.
|
||||
</Body>
|
||||
<Body>Upgrade to restore full functionality.</Body>
|
||||
<Body>
|
||||
Please update your billing details to restore full functionality.
|
||||
</Body>
|
||||
{#if !$auth.user.accountPortalAccess}
|
||||
<Body>Please contact the account holder to upgrade.</Body>
|
||||
{/if}
|
||||
|
|
|
@ -12,7 +12,10 @@
|
|||
|
||||
$: daysRemaining = $licensing.quotaResetDaysRemaining
|
||||
$: quotaResetDate = $licensing.quotaResetDate
|
||||
$: dayPassesUsed = $licensing.usageMetrics?.dayPasses
|
||||
$: dayPassesUsed =
|
||||
$licensing.usageMetrics?.dayPasses > 100
|
||||
? 100
|
||||
: $licensing.usageMetrics?.dayPasses
|
||||
$: dayPassesTitle =
|
||||
dayPassesUsed >= 100
|
||||
? "You have run out of Day Passes"
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
|
||||
let modal
|
||||
|
||||
export let onConfirm
|
||||
|
||||
export function show() {
|
||||
modal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
modal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:hide={modal}>
|
||||
<ModalContent
|
||||
title="Confirm deletion"
|
||||
size="S"
|
||||
showCancelButton={true}
|
||||
confirmText={"Confirm"}
|
||||
{onConfirm}
|
||||
>
|
||||
<Body size="S">Are you sure you want to delete this license key?</Body>
|
||||
<Body size="S">This license key cannot be re-activated.</Body>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
import { ExpiringKeys } from "./constants"
|
||||
import { getBanners } from "./licensingBanners"
|
||||
import { banner } from "@budibase/bbui"
|
||||
import { FEATURE_FLAGS, isEnabled } from "../../../helpers/featureFlags"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
|
@ -18,8 +18,8 @@
|
|||
let paymentFailedModal
|
||||
let accountDowngradeModal
|
||||
let userLoaded = false
|
||||
let loaded = false
|
||||
let licensingLoaded = false
|
||||
let domLoaded = false
|
||||
let currentModalCfg = null
|
||||
|
||||
const processModals = () => {
|
||||
|
@ -43,7 +43,7 @@
|
|||
{
|
||||
key: ExpiringKeys.LICENSING_PAYMENT_FAILED,
|
||||
criteria: () => {
|
||||
return $licensing.accountPastDue && !$licensing.isFreePlan()
|
||||
return $licensing.accountPastDue && !$licensing.isFreePlan
|
||||
},
|
||||
action: () => {
|
||||
paymentFailedModal.show()
|
||||
|
@ -82,12 +82,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: if (!userLoaded && $auth.user) {
|
||||
userLoaded = true
|
||||
}
|
||||
|
||||
$: if (
|
||||
userLoaded &&
|
||||
licensingLoaded &&
|
||||
loaded &&
|
||||
isEnabled(FEATURE_FLAGS.LICENSING)
|
||||
$licensing.usageMetrics &&
|
||||
domLoaded &&
|
||||
!licensingLoaded &&
|
||||
isEnabled(TENANT_FEATURE_FLAGS.LICENSING)
|
||||
) {
|
||||
licensingLoaded = true
|
||||
queuedModals = processModals()
|
||||
queuedBanners = getBanners()
|
||||
showNextModal()
|
||||
|
@ -95,18 +101,7 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
auth.subscribe(state => {
|
||||
if (state.user && !userLoaded) {
|
||||
userLoaded = true
|
||||
}
|
||||
})
|
||||
|
||||
licensing.subscribe(state => {
|
||||
if (state.usageMetrics && !licensingLoaded) {
|
||||
licensingLoaded = true
|
||||
}
|
||||
})
|
||||
loaded = true
|
||||
domLoaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
const paymentFailedTitle = "Payment failed"
|
||||
$: accountUrl = $admin.accountPortalUrl
|
||||
$: upgradeUrl = `${accountUrl}/portal/upgrade`
|
||||
$: billingUrl = `${accountUrl}/portal/billing`
|
||||
|
||||
export function show() {
|
||||
paymentFailedModal.show()
|
||||
|
@ -33,14 +33,14 @@
|
|||
<ModalContent
|
||||
title={paymentFailedTitle}
|
||||
size="M"
|
||||
confirmText="Upgrade"
|
||||
confirmText="Billing"
|
||||
onConfirm={() => {
|
||||
window.location.href = upgradeUrl
|
||||
window.location.href = billingUrl
|
||||
}}
|
||||
>
|
||||
<Body>The payment for your subscription has failed</Body>
|
||||
<Body>
|
||||
Please upgrade your billing details before your account gets downgraded
|
||||
Please update your billing details before your account gets downgraded
|
||||
to the free plan
|
||||
</Body>
|
||||
<Body weight={800}>
|
||||
|
|
|
@ -10,15 +10,31 @@ const defaultCacheFn = key => {
|
|||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const defaultAction = key => {
|
||||
const upgradeAction = key => {
|
||||
return defaultNavigateAction(
|
||||
key,
|
||||
"Upgrade Plan",
|
||||
`${get(admin).accountPortalUrl}/portal/upgrade`
|
||||
)
|
||||
}
|
||||
|
||||
const billingAction = key => {
|
||||
return defaultNavigateAction(
|
||||
key,
|
||||
"Billing",
|
||||
`${get(admin).accountPortalUrl}/portal/billing`
|
||||
)
|
||||
}
|
||||
|
||||
const defaultNavigateAction = (key, actionText, url) => {
|
||||
if (!get(auth).user.accountPortalAccess) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
extraButtonText: "Upgrade Plan",
|
||||
extraButtonText: actionText,
|
||||
extraButtonAction: () => {
|
||||
defaultCacheFn(key)
|
||||
window.location.href = `${get(admin).accountPortalUrl}/portal/upgrade`
|
||||
window.location.href = url
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +81,7 @@ const buildUsageInfoBanner = (
|
|||
? bannerConfig
|
||||
: {
|
||||
...bannerConfig,
|
||||
...defaultAction(cacheKey),
|
||||
...upgradeAction(cacheKey),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +100,7 @@ const buildDayPassBanner = () => {
|
|||
? ""
|
||||
: "Please contact your account holder to upgrade."
|
||||
}`,
|
||||
...defaultAction(),
|
||||
...upgradeAction(),
|
||||
showCloseButton: false,
|
||||
}
|
||||
}
|
||||
|
@ -113,13 +129,13 @@ const buildPaymentFailedBanner = () => {
|
|||
key: "payment_Failed",
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
criteria: () => {
|
||||
return get(licensing)?.accountPastDue && !get(licensing).isFreePlan()
|
||||
return get(licensing)?.accountPastDue && !get(licensing).isFreePlan
|
||||
},
|
||||
message: `Payment Failed - Please update your billing details or your account will be downgrades in
|
||||
message: `Payment Failed - Please update your billing details or your account will be downgraded in
|
||||
${get(licensing)?.pastDueDaysRemaining} day${
|
||||
get(licensing)?.pastDueDaysRemaining == 1 ? "" : "s"
|
||||
}`,
|
||||
...defaultAction(),
|
||||
...billingAction(),
|
||||
showCloseButton: false,
|
||||
tooltip: get(licensing).pastDueEndDate,
|
||||
}
|
||||
|
|
|
@ -1,42 +1,67 @@
|
|||
<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)">
|
||||
<div class="container">
|
||||
<Layout gap="S">
|
||||
<div class="header">
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
<div class="header sub-header">
|
||||
<div>
|
||||
<Detail
|
||||
>{filtered.length} {title}{filtered.length === 1 ? "" : "s"}</Detail
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton on:click={addAll} emphasized size="S">Add all</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
<div>
|
||||
{#each filtered as item}
|
||||
<div class="items">
|
||||
{#each sortedList as item}
|
||||
<div
|
||||
on:click={() => {
|
||||
select(item._id)
|
||||
dispatch(item.selected ? "deselect" : "select", item._id)
|
||||
}}
|
||||
style="padding-bottom: var(--spacing-m)"
|
||||
class="selection"
|
||||
class="item"
|
||||
>
|
||||
<div>
|
||||
{item[key]}
|
||||
{#if iconComponent}
|
||||
<svelte:component
|
||||
this={iconComponent}
|
||||
{...extractIconProps(item)}
|
||||
/>
|
||||
{/if}
|
||||
<div class="text">
|
||||
{item[labelKey]}
|
||||
</div>
|
||||
|
||||
{#if selected.includes(item._id)}
|
||||
{#if item.selected}
|
||||
<div>
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-blue-600);"
|
||||
|
@ -47,29 +72,45 @@
|
|||
</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">
|
||||
|
|
|
@ -60,14 +60,20 @@
|
|||
|
||||
$: staticVariables = datasource?.config?.staticVariables || {}
|
||||
|
||||
$: customRequestBindings = toBindingsArray(requestBindings, "Binding")
|
||||
$: customRequestBindings = toBindingsArray(
|
||||
requestBindings,
|
||||
"Binding",
|
||||
"Bindings"
|
||||
)
|
||||
$: globalDynamicRequestBindings = toBindingsArray(
|
||||
globalDynamicBindings,
|
||||
"Dynamic",
|
||||
"Dynamic"
|
||||
)
|
||||
$: dataSourceStaticBindings = toBindingsArray(
|
||||
staticVariables,
|
||||
"Datasource.Static"
|
||||
"Datasource.Static",
|
||||
"Datasource Static"
|
||||
)
|
||||
|
||||
$: mergedBindings = [
|
||||
|
@ -586,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">
|
||||
|
|
|
@ -56,7 +56,11 @@
|
|||
]
|
||||
|
||||
let dragDisabled = true
|
||||
$: settings = getComponentSettings($selectedComponent?._component)
|
||||
$: settings = getComponentSettings($selectedComponent?._component)?.concat({
|
||||
label: "Custom CSS",
|
||||
key: "_css",
|
||||
type: "text",
|
||||
})
|
||||
$: settingOptions = settings.map(setting => ({
|
||||
label: setting.label,
|
||||
value: setting.key,
|
||||
|
|
|
@ -1,30 +1,41 @@
|
|||
<script>
|
||||
import {
|
||||
TextArea,
|
||||
DetailSummary,
|
||||
ActionButton,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
Layout,
|
||||
Body,
|
||||
Button,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { selectedScreen, store } from "builderStore"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import {
|
||||
getBindableProperties,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
export let componentInstance
|
||||
|
||||
let tempValue
|
||||
let drawer
|
||||
|
||||
$: bindings = getBindableProperties(
|
||||
$selectedScreen,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
|
||||
const openDrawer = () => {
|
||||
tempValue = componentInstance?._styles?.custom
|
||||
tempValue = runtimeToReadableBinding(
|
||||
bindings,
|
||||
componentInstance?._styles?.custom
|
||||
)
|
||||
drawer.show()
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await store.actions.components.updateCustomStyle(tempValue)
|
||||
const value = readableToRuntimeBinding(bindings, tempValue)
|
||||
await store.actions.components.updateCustomStyle(value)
|
||||
} catch (error) {
|
||||
notifications.error("Error updating custom style")
|
||||
}
|
||||
|
@ -42,26 +53,17 @@
|
|||
</DetailSummary>
|
||||
{#key componentInstance?._id}
|
||||
<Drawer bind:this={drawer} title="Custom CSS">
|
||||
<svelte:fragment slot="description">
|
||||
Custom CSS overrides all other component styles.
|
||||
</svelte:fragment>
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<DrawerContent slot="body">
|
||||
<div class="content">
|
||||
<Layout gap="S" noPadding>
|
||||
<Body size="S">Custom CSS overrides all other component styles.</Body>
|
||||
<TextArea bind:value={tempValue} placeholder="Enter some CSS..." />
|
||||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
<svelte:component
|
||||
this={ClientBindingPanel}
|
||||
slot="body"
|
||||
value={tempValue}
|
||||
on:change={event => (tempValue = event.detail)}
|
||||
allowJS
|
||||
{bindings}
|
||||
/>
|
||||
</Drawer>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.content :global(textarea) {
|
||||
font-family: monospace;
|
||||
min-height: 240px !important;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
onMount(async () => {
|
||||
try {
|
||||
await templates.load()
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
// always load latest
|
||||
await licensing.init()
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
"There was a problem loading quick start templates."
|
||||
|
@ -45,7 +45,7 @@
|
|||
})
|
||||
|
||||
const initiateAppCreation = () => {
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
if ($licensing?.usageMetrics?.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = null
|
||||
|
@ -60,7 +60,7 @@
|
|||
}
|
||||
|
||||
const initiateAppImport = () => {
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
if ($licensing?.usageMetrics?.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else {
|
||||
template = { fromFile: true }
|
||||
|
@ -117,7 +117,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
|
||||
{#if loaded && $templates?.length}
|
||||
<TemplateDisplay templates={$templates} />
|
||||
|
|
|
@ -127,7 +127,7 @@
|
|||
}
|
||||
|
||||
const initiateAppCreation = async () => {
|
||||
if ($licensing.usageMetrics.apps >= 100) {
|
||||
if ($licensing?.usageMetrics?.apps >= 100) {
|
||||
appLimitModal.show()
|
||||
} else if ($apps?.length) {
|
||||
$goto("/builder/portal/apps/create")
|
||||
|
@ -229,9 +229,8 @@
|
|||
try {
|
||||
await apps.load()
|
||||
await templates.load()
|
||||
|
||||
await licensing.getQuotaUsage()
|
||||
await licensing.getUsageMetrics()
|
||||
// always load latest
|
||||
await licensing.init()
|
||||
|
||||
if ($templates?.length === 0) {
|
||||
notifications.error(
|
||||
|
@ -361,7 +360,7 @@
|
|||
</Button>
|
||||
{/if}
|
||||
<div class="filter">
|
||||
{#if $auth.groupsEnabled}
|
||||
{#if $licensing.groupsEnabled}
|
||||
<AccessFilter on:change={accessFilterAction} />
|
||||
{/if}
|
||||
<Select
|
||||
|
|
|
@ -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,
|
||||
$: filtered = $users.data
|
||||
$: groupApps = $apps.filter(app =>
|
||||
groups.actions.getGroupAppIds(group).includes(apps.getProdAppID(app.appId))
|
||||
)
|
||||
$: {
|
||||
if (loaded && !group?._id) {
|
||||
$goto("./")
|
||||
}
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -126,11 +70,29 @@
|
|||
}
|
||||
|
||||
const getRoleLabel = appId => {
|
||||
const roleId = group?.roles?.[`app_${appId}`]
|
||||
const roleId = group?.roles?.[apps.getProdAppID(appId)]
|
||||
const role = $roles.find(x => x._id === roleId)
|
||||
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,83 +104,101 @@
|
|||
</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>
|
||||
|
||||
<Layout noPadding gap="M">
|
||||
<div class="header">
|
||||
<div class="title">
|
||||
<div style="background: {group?.color};" class="circle">
|
||||
<div>
|
||||
<Icon size="M" name={group?.icon} />
|
||||
</div>
|
||||
</div>
|
||||
<GroupIcon {group} size="L" />
|
||||
<div class="text-padding">
|
||||
<Heading>{group?.name}</Heading>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<Divider />
|
||||
|
||||
<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>
|
||||
<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}
|
||||
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} avatar
|
||||
><Icon
|
||||
on:click={() => removeUser(user?._id)}
|
||||
<ListItem
|
||||
title={user.email}
|
||||
on:click={() => $goto(`../users/${user._id}`)}
|
||||
hoverable
|
||||
size="L"
|
||||
name="Close"
|
||||
/></ListItem
|
||||
>
|
||||
<Icon
|
||||
on:click={e => {
|
||||
groups.actions.removeUser(groupId, user._id)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
hoverable
|
||||
size="S"
|
||||
name="Close"
|
||||
/>
|
||||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="UserGroup" title="You have no users in this team" />
|
||||
<ListItem icon="UserGroup" title="This user group has no users" />
|
||||
{/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>
|
||||
</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"}
|
||||
iconBackground={app?.icon?.color || ""}
|
||||
iconColor={app?.icon?.color || ""}
|
||||
on:click={() => $goto(`../../overview/${app.devId}`)}
|
||||
hoverable
|
||||
>
|
||||
<div class="title ">
|
||||
<StatusLight
|
||||
square
|
||||
color={RoleUtils.getRoleColour(group.roles[`app_${app.appId}`])}
|
||||
color={RoleUtils.getRoleColour(
|
||||
group.roles[apps.getProdAppID(app.appId)]
|
||||
)}
|
||||
>
|
||||
{getRoleLabel(app.appId)}
|
||||
</StatusLight>
|
||||
|
@ -226,35 +206,35 @@
|
|||
</ListItem>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListItem icon="UserGroup" title="No apps" />
|
||||
<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 } from "stores/portal"
|
||||
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)
|
||||
|
@ -53,20 +78,21 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
if ($auth.groupsEnabled) {
|
||||
// always load latest
|
||||
await licensing.init()
|
||||
if ($licensing.groupsEnabled) {
|
||||
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 !$auth.groupsEnabled}
|
||||
{#if !$licensing.groupsEnabled}
|
||||
<Tags>
|
||||
<div class="tags">
|
||||
<div class="tag">
|
||||
|
@ -75,40 +101,58 @@
|
|||
</div>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
<Body>Easily assign and manage your users access with User Groups</Body>
|
||||
<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>
|
||||
<div class="align-buttons">
|
||||
<Divider />
|
||||
<div class="controls">
|
||||
<ButtonGroup>
|
||||
{#if $licensing.groupsEnabled}
|
||||
<!--Show the group create button-->
|
||||
<Button
|
||||
newStyles
|
||||
icon={$auth.groupsEnabled ? "UserGroup" : ""}
|
||||
cta={$auth.groupsEnabled}
|
||||
on:click={$auth.groupsEnabled
|
||||
? showCreateGroupModal
|
||||
: window.open("https://budibase.com/pricing/", "_blank")}
|
||||
icon={"UserGroup"}
|
||||
cta
|
||||
on:click={showCreateGroupModal}
|
||||
>
|
||||
{$auth.groupsEnabled ? "Create user group" : "Upgrade Account"}
|
||||
Create user group
|
||||
</Button>
|
||||
{#if !$auth.groupsEnabled}
|
||||
{: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
|
||||
}}
|
||||
>
|
||||
View Plans
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
<div class="controls-right">
|
||||
<Search bind:value={searchString} placeholder="Search" />
|
||||
</div>
|
||||
|
||||
{#if $auth.groupsEnabled && $groups.length}
|
||||
<div class="groupTable">
|
||||
{#each $groups as group}
|
||||
<div>
|
||||
<UserGroupsRow {saveGroup} {deleteGroup} {group} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<Table
|
||||
on:click={({ detail }) => $goto(`./${detail._id}`)}
|
||||
{schema}
|
||||
data={filteredGroups}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
{customRenderers}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
@ -116,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 } from "stores/portal"
|
||||
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}
|
||||
|
@ -284,7 +257,7 @@
|
|||
</Layout>
|
||||
</Layout>
|
||||
|
||||
{#if $auth.groupsEnabled}
|
||||
{#if $licensing.groupsEnabled}
|
||||
<!-- User groups -->
|
||||
<Layout gap="S" noPadding>
|
||||
<div class="tableTitle">
|
||||
|
@ -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>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
Layout,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { groups, auth } from "stores/portal"
|
||||
import { groups, licensing } from "stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { emailValidator } from "helpers/validation"
|
||||
|
||||
|
@ -117,7 +117,7 @@
|
|||
</div>
|
||||
</Layout>
|
||||
|
||||
{#if $auth.groupsEnabled}
|
||||
{#if $licensing.groupsEnabled}
|
||||
<Multiselect
|
||||
bind:value={userGroups}
|
||||
placeholder="No groups"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { Body, ModalContent, Table } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let userData
|
||||
export let deleteUsersResponse
|
||||
|
||||
let successCount
|
||||
|
|
|
@ -18,11 +18,9 @@
|
|||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.opacity {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
Multiselect,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { groups, auth, admin } from "stores/portal"
|
||||
import { groups, licensing, admin } from "stores/portal"
|
||||
import { emailValidator } from "helpers/validation"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
|
@ -72,7 +72,6 @@
|
|||
size="M"
|
||||
title="Import users"
|
||||
confirmText="Done"
|
||||
showCancelButton={false}
|
||||
cancelText="Cancel"
|
||||
showCloseIcon={false}
|
||||
onConfirm={() => createUsersFromCsv({ userEmails, usersRole, userGroups })}
|
||||
|
@ -92,7 +91,7 @@
|
|||
options={Constants.BuilderRoleDescriptions}
|
||||
/>
|
||||
|
||||
{#if $auth.groupsEnabled}
|
||||
{#if $licensing.groupsEnabled}
|
||||
<Multiselect
|
||||
bind:value={userGroups}
|
||||
placeholder="No groups"
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
Table,
|
||||
Layout,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Search,
|
||||
notifications,
|
||||
Pagination,
|
||||
Divider,
|
||||
} from "@budibase/bbui"
|
||||
import AddUserModal from "./_components/AddUserModal.svelte"
|
||||
import { users, groups, auth } from "stores/portal"
|
||||
import { users, groups, auth, licensing } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import DeleteRowsButton from "components/backend/DataTable/buttons/DeleteRowsButton.svelte"
|
||||
import GroupsTableRenderer from "./_components/GroupsTableRenderer.svelte"
|
||||
|
@ -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,
|
||||
},
|
||||
...($auth.groupsEnabled && {
|
||||
...($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,56 +189,42 @@
|
|||
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">
|
||||
{#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>
|
||||
<Body>Add users and control who gets access to your published apps.</Body>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<div class="controls">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
dataCy="add-user"
|
||||
on:click={createUserModal.show}
|
||||
icon="UserAdd"
|
||||
cta>Add users</Button
|
||||
>
|
||||
cta
|
||||
>Add users
|
||||
</Button>
|
||||
<Button
|
||||
on:click={importUsersModal.show}
|
||||
icon="Import"
|
||||
|
@ -243,7 +235,7 @@
|
|||
</Button>
|
||||
</ButtonGroup>
|
||||
<div class="controls-right">
|
||||
<Search bind:value={searchEmail} placeholder="Search email" />
|
||||
<Search bind:value={searchEmail} placeholder="Search" />
|
||||
{#if selectedRows.length > 0}
|
||||
<DeleteRowsButton
|
||||
item="user"
|
||||
|
@ -262,26 +254,35 @@
|
|||
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}
|
||||
page={$fetch.pageNumber + 1}
|
||||
hasPrevPage={$fetch.loading ? false : $fetch.hasPrevPage}
|
||||
hasNextPage={$fetch.loading ? false : $fetch.hasNextPage}
|
||||
goToPrevPage={fetch.prevPage}
|
||||
goToNextPage={fetch.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
</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>
|
||||
|
||||
|
|
|
@ -14,55 +14,38 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
import RoleSelect from "components/common/RoleSelect.svelte"
|
||||
import { users, groups, apps, auth } from "stores/portal"
|
||||
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
|
||||
|
||||
const usersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
appId: apps.getProdAppID(app.devId),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let assignmentModal
|
||||
let appGroups = []
|
||||
let appUsers = []
|
||||
let prevSearch = undefined,
|
||||
search = undefined
|
||||
let pageInfo = createPaginationStore()
|
||||
let fixedAppId
|
||||
let appGroups
|
||||
let appUsers
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||
$: appGroups = $groups.filter(x => {
|
||||
return x.apps.includes(app.appId)
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
$: appUsers = $usersFetch.rows
|
||||
$: appGroups = $groups.filter(group => {
|
||||
if (!group.roles) {
|
||||
return false
|
||||
}
|
||||
return groups.actions.getGroupAppIds(group).includes(fixedAppId)
|
||||
})
|
||||
await groups.actions.init()
|
||||
}
|
||||
|
||||
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,14 +92,14 @@
|
|||
<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 $auth.groupsEnabled && appGroups.length}
|
||||
{#if $licensing.groupsEnabled && appGroups.length}
|
||||
<List title="User Groups">
|
||||
{#each appGroups as group}
|
||||
<ListItem
|
||||
|
@ -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,6 +129,7 @@
|
|||
</List>
|
||||
{/if}
|
||||
{#if appUsers.length}
|
||||
<div>
|
||||
<List title="Users">
|
||||
{#each appUsers as user}
|
||||
<ListItem title={user.email} avatar>
|
||||
|
@ -193,6 +140,7 @@
|
|||
value={user.roles[
|
||||
Object.keys(user.roles).find(x => x === fixedAppId)
|
||||
]}
|
||||
allowPublic={false}
|
||||
/>
|
||||
<Icon
|
||||
on:click={() => removeUser(user)}
|
||||
|
@ -205,33 +153,32 @@
|
|||
</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)
|
||||
}}
|
||||
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}
|
||||
<div class="align">
|
||||
<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,46 @@
|
|||
ActionButton,
|
||||
Layout,
|
||||
Icon,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { roles } from "stores/backend"
|
||||
import { groups, users, auth } 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: "" }]
|
||||
const dispatch = createEventDispatcher()
|
||||
const usersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
email: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
$: page = $pageInfo.page
|
||||
$: fetchUsers(page, search)
|
||||
$: availableUsers = getAvailableUsers($users, appUsers, appData)
|
||||
$: filteredGroups = $groups.filter(group => {
|
||||
return !group.apps.find(appId => {
|
||||
return appId === app.appId
|
||||
let search = ""
|
||||
let data = [{ id: "", role: "" }]
|
||||
|
||||
$: usersFetch.update({
|
||||
query: {
|
||||
email: search,
|
||||
},
|
||||
})
|
||||
})
|
||||
$: valid =
|
||||
appData?.length && !appData?.some(x => !x.id?.length || !x.role?.length)
|
||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||
$: availableUsers = getAvailableUsers($usersFetch.rows, appUsers, data)
|
||||
$: availableGroups = getAvailableGroups($groups, app.appId, search, data)
|
||||
$: valid = data?.length && !data?.some(x => !x.id?.length || !x.role?.length)
|
||||
$: optionSections = {
|
||||
...($auth.groupsEnabled &&
|
||||
filteredGroups.length && {
|
||||
...($licensing.groupsEnabled &&
|
||||
availableGroups.length && {
|
||||
["User groups"]: {
|
||||
data: filteredGroups,
|
||||
data: availableGroups,
|
||||
getLabel: group => group.name,
|
||||
getValue: group => group._id,
|
||||
getIcon: group => group.icon,
|
||||
|
@ -51,8 +60,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 +109,31 @@
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchUsers(page, search) {
|
||||
if ($pageInfo.loading) {
|
||||
return
|
||||
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(apps.getProdAppID(appId))) {
|
||||
return false
|
||||
}
|
||||
// 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")
|
||||
|
||||
// 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 +142,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>
|
||||
{#if appEditor}
|
||||
<DashCard title={"Last Edited"} dataCy={"edited-by"}>
|
||||
<div class="last-edited-content">
|
||||
{#await appEditorPromise}
|
||||
<Avatar size="M" initials={"-"} />
|
||||
{:then _}
|
||||
<div class="updated-by">
|
||||
{#if appEditor}
|
||||
<Avatar size="M" initials={getInitials(appEditor)} />
|
||||
|
@ -96,9 +112,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:catch error}
|
||||
<p>Could not fetch user: {error.message}</p>
|
||||
{/await}
|
||||
<div class="last-edit-text">
|
||||
{#if app}
|
||||
{processStringSync(
|
||||
|
@ -112,6 +125,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</DashCard>
|
||||
{/if}
|
||||
<DashCard
|
||||
title={"App Version"}
|
||||
showIcon={true}
|
||||
|
@ -141,6 +155,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
</DashCard>
|
||||
{#if $appUsersFetch.loaded}
|
||||
<DashCard
|
||||
title={"Access"}
|
||||
showIcon={true}
|
||||
|
@ -149,18 +164,35 @@
|
|||
}}
|
||||
dataCy={"access"}
|
||||
>
|
||||
<div class="last-edited-content">
|
||||
{#if $users?.data?.length}
|
||||
{#if appUsers.length || appGroups.length}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="users-tab">
|
||||
{#each $users?.data as user}
|
||||
<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="users-text">
|
||||
{userCount}
|
||||
{userCount > 1 ? `users have` : `user has`} access to this app
|
||||
<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>
|
||||
{/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>
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import { auth, admin } from "stores/portal"
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import DeleteLicenseKeyModal from "../../../../components/portal/licensing/DeleteLicenseKeyModal.svelte"
|
||||
import { API } from "api"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
|
@ -26,6 +27,7 @@
|
|||
let licenseKeyDisabled = false
|
||||
let licenseKeyType = "text"
|
||||
let licenseKey = ""
|
||||
let deleteLicenseKeyModal
|
||||
|
||||
// Make sure page can't be visited directly in cloud
|
||||
$: {
|
||||
|
@ -45,6 +47,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
const destroy = async () => {
|
||||
try {
|
||||
await API.deleteLicenseKey({ licenseKey })
|
||||
await auth.getSelf()
|
||||
await setLicenseInfo()
|
||||
// reset the form
|
||||
licenseKey = ""
|
||||
licenseKeyDisabled = false
|
||||
notifications.success("Successfully deleted")
|
||||
} catch (e) {
|
||||
notifications.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
try {
|
||||
await API.refreshLicense()
|
||||
|
@ -76,23 +92,25 @@
|
|||
</script>
|
||||
|
||||
{#if $auth.isAdmin}
|
||||
<DeleteLicenseKeyModal
|
||||
bind:this={deleteLicenseKeyModal}
|
||||
onConfirm={destroy}
|
||||
/>
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<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>
|
||||
|
@ -100,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}
|
||||
|
@ -109,13 +127,22 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="button-container">
|
||||
<div class="action-button">
|
||||
<Button cta on:click={activate} disabled={activateDisabled}
|
||||
>Activate</Button
|
||||
>
|
||||
</div>
|
||||
<div class="action-button">
|
||||
{#if licenseInfo?.licenseKey}
|
||||
<Button warning on:click={() => deleteLicenseKeyModal.show()}
|
||||
>Delete</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
<Divider size="S" />
|
||||
<Divider />
|
||||
<Layout gap="L" noPadding>
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="S">Plan</Heading>
|
||||
|
@ -152,4 +179,10 @@
|
|||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
.action-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -147,7 +147,8 @@
|
|||
|
||||
const init = async () => {
|
||||
try {
|
||||
await licensing.getQuotaUsage()
|
||||
// always load latest
|
||||
await licensing.init()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
notifications.error(e)
|
||||
|
@ -175,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
|
||||
}
|
||||
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()
|
||||
const rest = split.join("app_dev")
|
||||
return `${"app"}${rest}`
|
||||
rest = split.join("app_dev")
|
||||
} else if (!appId.startsWith("app")) {
|
||||
rest = appId
|
||||
separator = "_"
|
||||
}
|
||||
return `app${separator}${rest}`
|
||||
}
|
||||
|
||||
export function createAppStore() {
|
||||
|
|
|
@ -2,23 +2,20 @@ import { derived, writable, get } from "svelte/store"
|
|||
import { API } from "api"
|
||||
import { admin } from "stores/portal"
|
||||
import analytics from "analytics"
|
||||
import { FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
export function createAuthStore() {
|
||||
const auth = writable({
|
||||
user: null,
|
||||
accountPortalAccess: false,
|
||||
tenantId: "default",
|
||||
tenantSet: false,
|
||||
loaded: false,
|
||||
postLogout: false,
|
||||
groupsEnabled: false,
|
||||
})
|
||||
const store = derived(auth, $store => {
|
||||
let initials = null
|
||||
let isAdmin = false
|
||||
let isBuilder = false
|
||||
let groupsEnabled = false
|
||||
if ($store.user) {
|
||||
const user = $store.user
|
||||
if (user.firstName) {
|
||||
|
@ -33,12 +30,10 @@ export function createAuthStore() {
|
|||
}
|
||||
isAdmin = !!user.admin?.global
|
||||
isBuilder = !!user.builder?.global
|
||||
groupsEnabled =
|
||||
user?.license.features.includes(Constants.Features.USER_GROUPS) &&
|
||||
user?.featureFlags.includes(FEATURE_FLAGS.USER_GROUPS)
|
||||
}
|
||||
return {
|
||||
user: $store.user,
|
||||
accountPortalAccess: $store.accountPortalAccess,
|
||||
tenantId: $store.tenantId,
|
||||
tenantSet: $store.tenantSet,
|
||||
loaded: $store.loaded,
|
||||
|
@ -46,7 +41,6 @@ export function createAuthStore() {
|
|||
initials,
|
||||
isAdmin,
|
||||
isBuilder,
|
||||
groupsEnabled,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -54,6 +48,7 @@ export function createAuthStore() {
|
|||
auth.update(store => {
|
||||
store.loaded = true
|
||||
store.user = user
|
||||
store.accountPortalAccess = user?.accountPortalAccess
|
||||
if (user) {
|
||||
store.tenantId = user.tenantId || "default"
|
||||
store.tenantSet = true
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { auth } from "stores/portal"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export function createGroupsStore() {
|
||||
const store = writable([])
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
// only init if these is a groups license, just to be sure but the feature will be blocked
|
||||
// on the backend anyway
|
||||
if (
|
||||
get(auth).user.license.features.includes(Constants.Features.USER_GROUPS)
|
||||
) {
|
||||
const users = await API.getGroups()
|
||||
store.set(users)
|
||||
}
|
||||
},
|
||||
|
||||
save: async group => {
|
||||
const response = await API.saveGroup(group)
|
||||
group._id = response._id
|
||||
group._rev = response._rev
|
||||
const updateStore = group => {
|
||||
store.update(state => {
|
||||
const currentIdx = state.findIndex(gr => gr._id === response._id)
|
||||
const currentIdx = state.findIndex(gr => gr._id === group._id)
|
||||
if (currentIdx >= 0) {
|
||||
state.splice(currentIdx, 1, group)
|
||||
} else {
|
||||
|
@ -31,6 +15,31 @@ export function createGroupsStore() {
|
|||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const getGroup = async groupId => {
|
||||
const group = await API.getGroup(groupId)
|
||||
updateStore(group)
|
||||
}
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
// 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 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
|
||||
updateStore(group)
|
||||
return group
|
||||
},
|
||||
|
||||
delete: async group => {
|
||||
|
@ -43,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 {
|
||||
|
|
|
@ -1,14 +1,31 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { auth } from "stores/portal"
|
||||
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 = {
|
||||
plans: {},
|
||||
usageMetrics: {},
|
||||
// navigation
|
||||
goToUpgradePage: () => {},
|
||||
// the top level license
|
||||
license: undefined,
|
||||
isFreePlan: true,
|
||||
// features
|
||||
groupsEnabled: false,
|
||||
// the currently used quotas from the db
|
||||
quotaUsage: undefined,
|
||||
// derived quota metrics for percentages used
|
||||
usageMetrics: undefined,
|
||||
// quota reset
|
||||
quotaResetDaysRemaining: undefined,
|
||||
quotaResetDate: undefined,
|
||||
// failed payments
|
||||
accountPastDue: undefined,
|
||||
pastDueEndDate: undefined,
|
||||
pastDueDaysRemaining: undefined,
|
||||
accountDowngraded: undefined,
|
||||
}
|
||||
const oneDayInMilliseconds = 86400000
|
||||
|
||||
|
@ -16,10 +33,39 @@ export const createLicensingStore = () => {
|
|||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
await actions.getQuotaUsage()
|
||||
await actions.getUsageMetrics()
|
||||
actions.setNavigation()
|
||||
actions.setLicense()
|
||||
await actions.setQuotaUsage()
|
||||
actions.setUsageMetrics()
|
||||
},
|
||||
getQuotaUsage: async () => {
|
||||
setNavigation: () => {
|
||||
const upgradeUrl = `${get(admin).accountPortalUrl}/portal/upgrade`
|
||||
const goToUpgradePage = () => {
|
||||
window.location.href = upgradeUrl
|
||||
}
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
goToUpgradePage,
|
||||
}
|
||||
})
|
||||
},
|
||||
setLicense: () => {
|
||||
const license = get(auth).user.license
|
||||
const isFreePlan = license?.plan.type === Constants.PlanType.FREE
|
||||
const groupsEnabled = license.features.includes(
|
||||
Constants.Features.USER_GROUPS
|
||||
)
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
license,
|
||||
isFreePlan,
|
||||
groupsEnabled,
|
||||
}
|
||||
})
|
||||
},
|
||||
setQuotaUsage: async () => {
|
||||
const quotaUsage = await API.getQuotaUsage()
|
||||
store.update(state => {
|
||||
return {
|
||||
|
@ -28,8 +74,8 @@ export const createLicensingStore = () => {
|
|||
}
|
||||
})
|
||||
},
|
||||
getUsageMetrics: async () => {
|
||||
if (isEnabled(FEATURE_FLAGS.LICENSING)) {
|
||||
setUsageMetrics: () => {
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||
const quota = get(store).quotaUsage
|
||||
const license = get(auth).user.license
|
||||
const now = new Date()
|
||||
|
@ -41,7 +87,7 @@ export const createLicensingStore = () => {
|
|||
return keys.reduce((acc, key) => {
|
||||
const quotaLimit = license[key].value
|
||||
const quotaUsed = (quota[key] / quotaLimit) * 100
|
||||
acc[key] = quotaLimit > -1 ? Math.round(quotaUsed) : -1
|
||||
acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
@ -97,9 +143,6 @@ export const createLicensingStore = () => {
|
|||
accountPastDue: pastDueAtMilliseconds != null,
|
||||
pastDueEndDate,
|
||||
pastDueDaysRemaining,
|
||||
isFreePlan: () => {
|
||||
return license?.plan.type === Constants.PlanType.FREE
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.4.17",
|
||||
"version": "1.4.18-alpha.0",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
@ -26,9 +26,9 @@
|
|||
"outputPath": "build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "^1.4.17",
|
||||
"@budibase/string-templates": "^1.4.17",
|
||||
"@budibase/types": "^1.4.17",
|
||||
"@budibase/backend-core": "1.4.18-alpha.0",
|
||||
"@budibase/string-templates": "1.4.18-alpha.0",
|
||||
"@budibase/types": "1.4.18-alpha.0",
|
||||
"axios": "0.21.2",
|
||||
"chalk": "4.1.0",
|
||||
"cli-progress": "3.11.2",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.4.17",
|
||||
"version": "1.4.18-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.4.17",
|
||||
"@budibase/frontend-core": "^1.4.17",
|
||||
"@budibase/string-templates": "^1.4.17",
|
||||
"@budibase/bbui": "1.4.18-alpha.0",
|
||||
"@budibase/frontend-core": "1.4.18-alpha.0",
|
||||
"@budibase/string-templates": "1.4.18-alpha.0",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
@ -35,7 +35,6 @@
|
|||
"downloadjs": "1.4.7",
|
||||
"leaflet": "^1.7.1",
|
||||
"regexparam": "^1.3.0",
|
||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||
"sanitize-html": "^2.7.0",
|
||||
"screenfull": "^6.0.1",
|
||||
"shortid": "^2.2.15",
|
||||
|
@ -52,6 +51,7 @@
|
|||
"postcss": "^8.2.10",
|
||||
"rollup": "^2.44.0",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||
"rollup-plugin-postcss": "^4.0.0",
|
||||
"rollup-plugin-svelte": "^7.1.0",
|
||||
"rollup-plugin-svg": "^2.0.0",
|
||||
|
|
|
@ -142,6 +142,10 @@
|
|||
// Determine and apply settings to the component
|
||||
$: applySettings(staticSettings, enrichedSettings, conditionalSettings)
|
||||
|
||||
// Determine custom css.
|
||||
// Broken out as a separate variable to minimize reactivity updates.
|
||||
$: customCSS = cachedSettings?._css
|
||||
|
||||
// Scroll the selected element into view
|
||||
$: selected && scrollIntoView()
|
||||
|
||||
|
@ -151,6 +155,7 @@
|
|||
children: children.length,
|
||||
styles: {
|
||||
...instance._styles,
|
||||
custom: customCSS,
|
||||
id,
|
||||
empty: emptyState,
|
||||
interactive,
|
||||
|
@ -249,14 +254,18 @@
|
|||
// Get raw settings
|
||||
let settings = {}
|
||||
Object.entries(instance)
|
||||
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
|
||||
.filter(([name]) => !name.startsWith("_"))
|
||||
.forEach(([key, value]) => {
|
||||
settings[key] = value
|
||||
})
|
||||
|
||||
// Derive static, dynamic and nested settings if the instance changed
|
||||
let newStaticSettings = { ...settings }
|
||||
let newDynamicSettings = { ...settings }
|
||||
|
||||
// Attach some internal properties
|
||||
newDynamicSettings["_conditions"] = instance._conditions
|
||||
newDynamicSettings["_css"] = instance._styles?.custom
|
||||
|
||||
// Derive static, dynamic and nested settings if the instance changed
|
||||
settingsDefinition?.forEach(setting => {
|
||||
if (setting.nested) {
|
||||
delete newDynamicSettings[setting.key]
|
||||
|
@ -370,6 +379,11 @@
|
|||
// setting it on initialSettings directly, we avoid a double render.
|
||||
cachedSettings[key] = allSettings[key]
|
||||
|
||||
// Don't update components for internal properties
|
||||
if (key.startsWith("_")) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ref?.$$set) {
|
||||
// Programmatically set the prop to avoid svelte reactive statements
|
||||
// firing inside components. This circumvents the problems caused by
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// import { isFreePlan } from "./utils.js"
|
||||
import { isFreePlan } from "./utils.js"
|
||||
|
||||
export const logoEnabled = () => {
|
||||
return false
|
||||
// return isFreePlan()
|
||||
return isFreePlan()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { getAction } from "utils/getAction"
|
|||
import Provider from "components/context/Provider.svelte"
|
||||
import { ActionTypes } from "./constants"
|
||||
import { fetchDatasourceSchema } from "./utils/schema.js"
|
||||
import { getAPIKey } from "./utils/api.js"
|
||||
|
||||
export default {
|
||||
API,
|
||||
|
@ -36,4 +37,5 @@ export default {
|
|||
fetchDatasourceSchema,
|
||||
Provider,
|
||||
ActionTypes,
|
||||
getAPIKey,
|
||||
}
|
||||
|
|
6
packages/client/src/utils/api.js
Normal file
6
packages/client/src/utils/api.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { API } from "api"
|
||||
|
||||
export const getAPIKey = async () => {
|
||||
const { apiKey } = await API.fetchDeveloperInfo()
|
||||
return apiKey
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "1.4.17",
|
||||
"version": "1.4.18-alpha.0",
|
||||
"description": "Budibase frontend core libraries used in builder and client",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.4.17",
|
||||
"@budibase/bbui": "1.4.18-alpha.0",
|
||||
"lodash": "^4.17.21",
|
||||
"svelte": "^3.46.2"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
export const buildGroupsEndpoints = API => ({
|
||||
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/${groupId}/${resource}`,
|
||||
body: {
|
||||
[operation]: ids,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Creates a user group.
|
||||
* @param user the new group to create
|
||||
* @param group the new group to create
|
||||
*/
|
||||
saveGroup: async group => {
|
||||
return await API.post({
|
||||
|
@ -10,7 +24,7 @@ export const buildGroupsEndpoints = API => ({
|
|||
})
|
||||
},
|
||||
/**
|
||||
* Gets all of the user groups
|
||||
* Gets all the user groups
|
||||
*/
|
||||
getGroups: async () => {
|
||||
return await API.get({
|
||||
|
@ -37,4 +51,41 @@ export const buildGroupsEndpoints = API => ({
|
|||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,15 @@ export const buildLicensingEndpoints = API => ({
|
|||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a self hosted license key
|
||||
*/
|
||||
deleteLicenseKey: async () => {
|
||||
return API.delete({
|
||||
url: `/api/global/license/info`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the license info - metadata about the license including the
|
||||
* obfuscated license key.
|
||||
|
|
|
@ -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: {
|
||||
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: {
|
||||
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,
|
||||
},
|
||||
})),
|
||||
})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue