diff --git a/lerna.json b/lerna.json
index dde9cf03a0..3afc2c60e9 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "3.2.32",
+ "version": "3.2.33",
"npmClient": "yarn",
"concurrency": 20,
"command": {
diff --git a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
index 88e034a96b..37abd7f1eb 100644
--- a/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_components/BuilderSidePanel.svelte
@@ -236,13 +236,13 @@
}
if (!role) {
- await groups.actions.removeApp(target._id, prodAppId)
+ await groups.removeApp(target._id, prodAppId)
} else {
- await groups.actions.addApp(target._id, prodAppId, role)
+ await groups.addApp(target._id, prodAppId, role)
}
await usersFetch.refresh()
- await groups.actions.init()
+ await groups.init()
}
const onUpdateGroup = async (group, role) => {
@@ -268,7 +268,7 @@
if (!group.roles) {
return false
}
- return groups.actions.getGroupAppIds(group).includes(appId)
+ return groups.getGroupAppIds(group).includes(appId)
})
}
@@ -299,7 +299,7 @@
role: group?.builder?.apps.includes(prodAppId)
? Constants.Roles.CREATOR
: group.roles?.[
- groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
+ groups.getGroupAppIds(group).find(x => x === prodAppId)
],
}
}
@@ -485,12 +485,12 @@
}
const removeGroupAppBuilder = async groupId => {
- await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
+ await groups.removeGroupAppBuilder(groupId, prodAppId)
}
const initSidePanel = async sidePaneOpen => {
if (sidePaneOpen === true) {
- await groups.actions.init()
+ await groups.init()
}
loaded = true
}
diff --git a/packages/builder/src/pages/builder/apps/index.svelte b/packages/builder/src/pages/builder/apps/index.svelte
index 8bf96d0240..e106d0dd68 100644
--- a/packages/builder/src/pages/builder/apps/index.svelte
+++ b/packages/builder/src/pages/builder/apps/index.svelte
@@ -53,7 +53,7 @@
}
if (!Object.keys(user?.roles).length && user?.userGroups) {
return userGroups.find(group => {
- return groups.actions
+ return groups
.getGroupAppIds(group)
.map(role => appsStore.extractAppId(role))
.includes(app.appId)
@@ -86,7 +86,7 @@
try {
await organisation.init()
await appsStore.load()
- await groups.actions.init()
+ await groups.init()
} catch (error) {
notifications.error("Error loading apps")
}
diff --git a/packages/builder/src/pages/builder/portal/apps/_layout.svelte b/packages/builder/src/pages/builder/portal/apps/_layout.svelte
index 560c1394fb..0a3b02f30f 100644
--- a/packages/builder/src/pages/builder/portal/apps/_layout.svelte
+++ b/packages/builder/src/pages/builder/portal/apps/_layout.svelte
@@ -24,7 +24,7 @@
promises.push(templates.load())
}
- promises.push(groups.actions.init())
+ promises.push(groups.init())
// Always load latest
await Promise.all(promises)
diff --git a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte
index 312d87f873..58fd1d93cb 100644
--- a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte
+++ b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte
@@ -53,9 +53,7 @@
$: readonly = !isAdmin || isScimGroup
$: groupApps = $appsStore.apps
.filter(app =>
- groups.actions
- .getGroupAppIds(group)
- .includes(appsStore.getProdAppID(app.devId))
+ groups.getGroupAppIds(group).includes(appsStore.getProdAppID(app.devId))
)
.map(app => ({
...app,
@@ -72,7 +70,7 @@
async function deleteGroup() {
try {
- await groups.actions.delete(group)
+ await groups.delete(group)
notifications.success("User group deleted successfully")
$goto("./")
} catch (error) {
@@ -82,7 +80,7 @@
async function saveGroup(group) {
try {
- await groups.actions.save(group)
+ await groups.save(group)
} catch (error) {
if (error.message) {
notifications.error(error.message)
@@ -93,7 +91,7 @@
}
const removeApp = async app => {
- await groups.actions.removeApp(groupId, appsStore.getProdAppID(app.devId))
+ await groups.removeApp(groupId, appsStore.getProdAppID(app.devId))
}
setContext("roles", {
updateRole: () => {},
@@ -102,7 +100,7 @@
onMount(async () => {
try {
- await Promise.all([groups.actions.init(), roles.fetch()])
+ await Promise.all([groups.init(), roles.fetch()])
loaded = true
} catch (error) {
notifications.error("Error fetching user group data")
diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/AppAddModal.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/AppAddModal.svelte
index 75600c6fc0..88b8b4657b 100644
--- a/packages/builder/src/pages/builder/portal/users/groups/_components/AppAddModal.svelte
+++ b/packages/builder/src/pages/builder/portal/users/groups/_components/AppAddModal.svelte
@@ -23,7 +23,7 @@
return keepOpen
} else {
- await groups.actions.addApp(group._id, prodAppId, selectedRoleId)
+ await groups.addApp(group._id, prodAppId, selectedRoleId)
}
}
diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte
index 1e7e15d1b4..d360de3850 100644
--- a/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte
+++ b/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte
@@ -50,11 +50,11 @@
selected={group.users?.map(user => user._id)}
list={$users.data}
on:select={async e => {
- await groups.actions.addUser(groupId, e.detail)
+ await groups.addUser(groupId, e.detail)
onUsersUpdated()
}}
on:deselect={async e => {
- await groups.actions.removeUser(groupId, e.detail)
+ await groups.removeUser(groupId, e.detail)
onUsersUpdated()
}}
/>
diff --git a/packages/builder/src/pages/builder/portal/users/groups/index.svelte b/packages/builder/src/pages/builder/portal/users/groups/index.svelte
index 77b0dc5734..9982f85352 100644
--- a/packages/builder/src/pages/builder/portal/users/groups/index.svelte
+++ b/packages/builder/src/pages/builder/portal/users/groups/index.svelte
@@ -60,7 +60,7 @@
async function saveGroup(group) {
try {
- group = await groups.actions.save(group)
+ group = await groups.save(group)
$goto(`./${group._id}`)
notifications.success(`User group created successfully`)
} catch (error) {
@@ -83,7 +83,7 @@
try {
// always load latest
await licensing.init()
- await groups.actions.init()
+ await groups.init()
} catch (error) {
notifications.error("Error getting user groups")
}
diff --git a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte
index 94fe3081c3..6c480d9ef8 100644
--- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte
+++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte
@@ -87,6 +87,7 @@
let popover
let user, tenantOwner
let loaded = false
+ let userFieldsToUpdate = {}
$: internalGroups = $groups?.filter(g => !g?.scimInfo?.isSync)
@@ -164,40 +165,45 @@
return label
}
- async function updateUserFirstName(evt) {
+ async function saveUser() {
try {
- await users.save({ ...user, firstName: evt.target.value })
+ await users.save({ ...user, ...userFieldsToUpdate })
+ userFieldsToUpdate = {}
await fetchUser()
} catch (error) {
notifications.error("Error updating user")
}
}
+ async function updateUserFirstName(evt) {
+ userFieldsToUpdate.firstName = evt.target.value
+ }
+
async function updateUserLastName(evt) {
- try {
- await users.save({ ...user, lastName: evt.target.value })
- await fetchUser()
- } catch (error) {
- notifications.error("Error updating user")
- }
+ userFieldsToUpdate.lastName = evt.target.value
}
async function updateUserRole({ detail }) {
+ let flags = {}
if (detail === Constants.BudibaseRoles.Developer) {
- toggleFlags({ admin: { global: false }, builder: { global: true } })
+ flags = { admin: { global: false }, builder: { global: true } }
} else if (detail === Constants.BudibaseRoles.Admin) {
- toggleFlags({ admin: { global: true }, builder: { global: true } })
+ flags = { admin: { global: true }, builder: { global: true } }
} else if (detail === Constants.BudibaseRoles.AppUser) {
- toggleFlags({ admin: { global: false }, builder: { global: false } })
+ flags = { admin: { global: false }, builder: { global: false } }
} else if (detail === Constants.BudibaseRoles.Creator) {
- toggleFlags({
+ flags = {
admin: { global: false },
builder: {
global: false,
creator: true,
apps: user?.builder?.apps || [],
},
- })
+ }
+ }
+ userFieldsToUpdate = {
+ ...userFieldsToUpdate,
+ ...flags,
}
}
@@ -209,22 +215,13 @@
tenantOwner = await users.getAccountHolder()
}
- async function toggleFlags(detail) {
- try {
- await users.save({ ...user, ...detail })
- await fetchUser()
- } catch (error) {
- notifications.error("Error updating user")
- }
- }
-
const addGroup = async groupId => {
- await groups.actions.addUser(groupId, userId)
+ await groups.addUser(groupId, userId)
await fetchUser()
}
const removeGroup = async groupId => {
- await groups.actions.removeUser(groupId, userId)
+ await groups.removeUser(groupId, userId)
await fetchUser()
}
@@ -234,7 +231,7 @@
onMount(async () => {
try {
- await Promise.all([fetchUser(), groups.actions.init(), roles.fetch()])
+ await Promise.all([fetchUser(), groups.init(), roles.fetch()])
loaded = true
} catch (error) {
notifications.error("Error getting user groups")
@@ -296,7 +293,7 @@
@@ -304,7 +301,7 @@
@@ -325,6 +322,13 @@
{/if}
+
+
+
{#if $licensing.groupsEnabled}
diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte
index 80772ccbee..97120c55d4 100644
--- a/packages/builder/src/pages/builder/portal/users/users/index.svelte
+++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte
@@ -247,7 +247,7 @@
try {
bulkSaveResponse = await users.create(await removingDuplicities(userData))
notifications.success("Successfully created user")
- await groups.actions.init()
+ await groups.init()
passwordModal.show()
await fetch.refresh()
} catch (error) {
@@ -317,7 +317,7 @@
onMount(async () => {
try {
- await groups.actions.init()
+ await groups.init()
groupsLoaded = true
} catch (error) {
notifications.error("Error fetching user group data")
diff --git a/packages/builder/src/stores/builder/published.js b/packages/builder/src/stores/builder/published.ts
similarity index 53%
rename from packages/builder/src/stores/builder/published.js
rename to packages/builder/src/stores/builder/published.ts
index a59352fb22..c38f3bb718 100644
--- a/packages/builder/src/stores/builder/published.js
+++ b/packages/builder/src/stores/builder/published.ts
@@ -1,13 +1,16 @@
import { appStore } from "./app"
import { appsStore } from "@/stores/portal/apps"
import { deploymentStore } from "./deployments"
-import { derived } from "svelte/store"
+import { derived, type Readable } from "svelte/store"
+import { DeploymentProgressResponse, DeploymentStatus } from "@budibase/types"
-export const appPublished = derived(
+export const appPublished: Readable = derived(
[appStore, appsStore, deploymentStore],
([$appStore, $appsStore, $deploymentStore]) => {
const app = $appsStore.apps.find(app => app.devId === $appStore.appId)
- const deployments = $deploymentStore.filter(x => x.status === "SUCCESS")
+ const deployments = $deploymentStore.filter(
+ (x: DeploymentProgressResponse) => x.status === DeploymentStatus.SUCCESS
+ )
return app?.status === "published" && deployments.length > 0
}
)
diff --git a/packages/builder/src/stores/builder/roles.js b/packages/builder/src/stores/builder/roles.js
deleted file mode 100644
index e718545f14..0000000000
--- a/packages/builder/src/stores/builder/roles.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import { derived, writable, get } from "svelte/store"
-import { API } from "@/api"
-import { RoleUtils } from "@budibase/frontend-core"
-
-export function createRolesStore() {
- const store = writable([])
- const enriched = derived(store, $store => {
- return $store.map(role => ({
- ...role,
-
- // Ensure we have new metadata for all roles
- uiMetadata: {
- displayName: role.uiMetadata?.displayName || role.name,
- color:
- role.uiMetadata?.color || "var(--spectrum-global-color-magenta-400)",
- description: role.uiMetadata?.description || "Custom role",
- },
- }))
- })
-
- function setRoles(roles) {
- store.set(
- roles.sort((a, b) => {
- const priorityA = RoleUtils.getRolePriority(a._id)
- const priorityB = RoleUtils.getRolePriority(b._id)
- if (priorityA !== priorityB) {
- return priorityA > priorityB ? -1 : 1
- }
- const nameA = a.uiMetadata?.displayName || a.name
- const nameB = b.uiMetadata?.displayName || b.name
- return nameA < nameB ? -1 : 1
- })
- )
- }
-
- const actions = {
- fetch: async () => {
- const roles = await API.getRoles()
- setRoles(roles)
- },
- fetchByAppId: async appId => {
- const { roles } = await API.getRolesForApp(appId)
- setRoles(roles)
- },
- delete: async role => {
- await API.deleteRole(role._id, role._rev)
- await actions.fetch()
- },
- save: async role => {
- const savedRole = await API.saveRole(role)
- await actions.fetch()
- return savedRole
- },
- replace: (roleId, role) => {
- // Handles external updates of roles
- if (!roleId) {
- return
- }
-
- // Handle deletion
- if (!role) {
- store.update(state => state.filter(x => x._id !== roleId))
- return
- }
-
- // Add new role
- const index = get(store).findIndex(x => x._id === role._id)
- if (index === -1) {
- store.update(state => [...state, role])
- }
-
- // Update existing role
- else if (role) {
- store.update(state => {
- state[index] = role
- return [...state]
- })
- }
- },
- }
-
- return {
- subscribe: enriched.subscribe,
- ...actions,
- }
-}
-
-export const roles = createRolesStore()
diff --git a/packages/builder/src/stores/builder/roles.ts b/packages/builder/src/stores/builder/roles.ts
new file mode 100644
index 0000000000..732f50d6be
--- /dev/null
+++ b/packages/builder/src/stores/builder/roles.ts
@@ -0,0 +1,94 @@
+import { derived, get, type Writable } from "svelte/store"
+import { API } from "@/api"
+import { RoleUtils } from "@budibase/frontend-core"
+import { DerivedBudiStore } from "../BudiStore"
+import { Role } from "@budibase/types"
+
+export class RoleStore extends DerivedBudiStore {
+ constructor() {
+ const makeDerivedStore = (store: Writable) =>
+ derived(store, $store => {
+ return $store.map((role: Role) => ({
+ ...role,
+ // Ensure we have new metadata for all roles
+ uiMetadata: {
+ displayName: role.uiMetadata?.displayName || role.name,
+ color:
+ role.uiMetadata?.color ||
+ "var(--spectrum-global-color-magenta-400)",
+ description: role.uiMetadata?.description || "Custom role",
+ },
+ }))
+ })
+
+ super([], makeDerivedStore)
+ }
+
+ setRoles = (roles: Role[]) => {
+ this.set(
+ roles.sort((a, b) => {
+ // Ensure we have valid IDs for priority comparison
+ const priorityA = RoleUtils.getRolePriority(a._id)
+ const priorityB = RoleUtils.getRolePriority(b._id)
+ if (priorityA !== priorityB) {
+ return priorityA > priorityB ? -1 : 1
+ }
+ const nameA = a.uiMetadata?.displayName || a.name
+ const nameB = b.uiMetadata?.displayName || b.name
+ return nameA < nameB ? -1 : 1
+ })
+ )
+ }
+
+ fetch = async () => {
+ const roles = await API.getRoles()
+ this.setRoles(roles)
+ }
+
+ fetchByAppId = async (appId: string) => {
+ const { roles } = await API.getRolesForApp(appId)
+ this.setRoles(roles)
+ }
+
+ delete = async (role: Role) => {
+ if (!role._id || !role._rev) {
+ return
+ }
+ await API.deleteRole(role._id, role._rev)
+ await this.fetch()
+ }
+
+ save = async (role: Role) => {
+ const savedRole = await API.saveRole(role)
+ await this.fetch()
+ return savedRole
+ }
+
+ replace = (roleId: string, role?: Role) => {
+ // Handles external updates of roles
+ if (!roleId) {
+ return
+ }
+
+ // Handle deletion
+ if (!role) {
+ this.update(state => state.filter(x => x._id !== roleId))
+ return
+ }
+
+ // Add new role
+ const index = get(this).findIndex(x => x._id === role._id)
+ if (index === -1) {
+ this.update(state => [...state, role])
+ }
+ // Update existing role
+ else if (role) {
+ this.update(state => {
+ state[index] = role
+ return [...state]
+ })
+ }
+ }
+}
+
+export const roles = new RoleStore()
diff --git a/packages/builder/src/stores/portal/groups.js b/packages/builder/src/stores/portal/groups.js
deleted file mode 100644
index 408fb4189a..0000000000
--- a/packages/builder/src/stores/portal/groups.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { writable, get } from "svelte/store"
-import { API } from "@/api"
-import { licensing } from "@/stores/portal"
-
-export function createGroupsStore() {
- const store = writable([])
-
- const updateStore = group => {
- store.update(state => {
- const currentIdx = state.findIndex(gr => gr._id === group._id)
- if (currentIdx >= 0) {
- state.splice(currentIdx, 1, group)
- } else {
- state.push(group)
- }
- return state
- })
- }
-
- const getGroup = async groupId => {
- const group = await API.getGroup(groupId)
- updateStore(group)
- }
-
- const actions = {
- init: async () => {
- // only init if 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.data)
- }
- },
-
- get: getGroup,
-
- save: async group => {
- const { ...dataToSave } = group
- delete dataToSave.scimInfo
- delete dataToSave.userGroups
- const response = await API.saveGroup(dataToSave)
- group._id = response._id
- group._rev = response._rev
- updateStore(group)
- return group
- },
-
- delete: async group => {
- await API.deleteGroup(group._id, group._rev)
- store.update(state => {
- state = state.filter(state => state._id !== group._id)
- 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 => {
- let groupAppIds = Object.keys(group?.roles || {})
- if (group?.builder?.apps) {
- groupAppIds = groupAppIds.concat(group.builder.apps)
- }
- return groupAppIds
- },
-
- addGroupAppBuilder: async (groupId, appId) => {
- return await API.addGroupAppBuilder(groupId, appId)
- },
-
- removeGroupAppBuilder: async (groupId, appId) => {
- return await API.removeGroupAppBuilder(groupId, appId)
- },
- }
-
- return {
- subscribe: store.subscribe,
- actions,
- }
-}
-
-export const groups = createGroupsStore()
diff --git a/packages/builder/src/stores/portal/groups.ts b/packages/builder/src/stores/portal/groups.ts
new file mode 100644
index 0000000000..028f300d2c
--- /dev/null
+++ b/packages/builder/src/stores/portal/groups.ts
@@ -0,0 +1,96 @@
+import { get } from "svelte/store"
+import { API } from "@/api"
+import { licensing } from "@/stores/portal"
+import { UserGroup } from "@budibase/types"
+import { BudiStore } from "../BudiStore"
+
+class GroupStore extends BudiStore {
+ constructor() {
+ super([])
+ }
+
+ updateStore = (group: UserGroup) => {
+ this.update(state => {
+ const currentIdx = state.findIndex(gr => gr._id === group._id)
+ if (currentIdx >= 0) {
+ state.splice(currentIdx, 1, group)
+ } else {
+ state.push(group)
+ }
+ return state
+ })
+ }
+
+ async init() {
+ // 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()
+ this.set(groups)
+ }
+ }
+
+ private async refreshGroup(groupId: string) {
+ const group = await API.getGroup(groupId)
+ this.updateStore(group)
+ }
+
+ async save(group: UserGroup) {
+ const { ...dataToSave } = group
+ delete dataToSave.scimInfo
+ const response = await API.saveGroup(dataToSave)
+ group._id = response._id
+ group._rev = response._rev
+ this.updateStore(group)
+ return group
+ }
+
+ async delete(group: UserGroup) {
+ await API.deleteGroup(group._id!, group._rev!)
+ this.update(groups => {
+ const index = groups.findIndex(g => g._id === group._id)
+ if (index !== -1) {
+ groups.splice(index, 1)
+ }
+ return groups
+ })
+ }
+
+ async addUser(groupId: string, userId: string) {
+ await API.addUsersToGroup(groupId, [userId])
+ await this.refreshGroup(groupId)
+ }
+
+ async removeUser(groupId: string, userId: string) {
+ await API.removeUsersFromGroup(groupId, [userId])
+ await this.refreshGroup(groupId)
+ }
+
+ async addApp(groupId: string, appId: string, roleId: string) {
+ await API.addAppsToGroup(groupId, [{ appId, roleId }])
+ await this.refreshGroup(groupId)
+ }
+
+ async removeApp(groupId: string, appId: string) {
+ await API.removeAppsFromGroup(groupId, [{ appId }])
+ await this.refreshGroup(groupId)
+ }
+
+ getGroupAppIds(group: UserGroup) {
+ let groupAppIds = Object.keys(group?.roles || {})
+ if (group?.builder?.apps) {
+ groupAppIds = groupAppIds.concat(group.builder.apps)
+ }
+ return groupAppIds
+ }
+
+ async addGroupAppBuilder(groupId: string, appId: string) {
+ return await API.addGroupAppBuilder(groupId, appId)
+ }
+
+ async removeGroupAppBuilder(groupId: string, appId: string) {
+ return await API.removeGroupAppBuilder(groupId, appId)
+ }
+}
+
+export const groups = new GroupStore()
diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js
deleted file mode 100644
index afc3ea1628..0000000000
--- a/packages/builder/src/stores/portal/licensing.js
+++ /dev/null
@@ -1,279 +0,0 @@
-import { writable, get } from "svelte/store"
-import { API } from "@/api"
-import { auth, admin } from "@/stores/portal"
-import { Constants } from "@budibase/frontend-core"
-import { StripeStatus } from "@/components/portal/licensing/constants"
-import { PlanModel } from "@budibase/types"
-
-const UNLIMITED = -1
-
-export const createLicensingStore = () => {
- const DEFAULT = {
- // navigation
- goToUpgradePage: () => {},
- goToPricingPage: () => {},
- // the top level license
- license: undefined,
- isFreePlan: true,
- isEnterprisePlan: true,
- isBusinessPlan: true,
- // features
- groupsEnabled: false,
- backupsEnabled: false,
- brandingEnabled: false,
- scimEnabled: false,
- environmentVariablesEnabled: false,
- budibaseAIEnabled: false,
- customAIConfigsEnabled: false,
- auditLogsEnabled: 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,
- // user limits
- userCount: undefined,
- userLimit: undefined,
- userLimitReached: false,
- errUserLimit: false,
- }
-
- const oneDayInMilliseconds = 86400000
-
- const store = writable(DEFAULT)
-
- function usersLimitReached(userCount, userLimit) {
- if (userLimit === UNLIMITED) {
- return false
- }
- return userCount >= userLimit
- }
-
- function usersLimitExceeded(userCount, userLimit) {
- if (userLimit === UNLIMITED) {
- return false
- }
- return userCount > userLimit
- }
-
- async function isCloud() {
- let adminStore = get(admin)
- if (!adminStore.loaded) {
- await admin.init()
- adminStore = get(admin)
- }
- return adminStore.cloud
- }
-
- const actions = {
- init: async () => {
- actions.setNavigation()
- actions.setLicense()
- await actions.setQuotaUsage()
- },
- setNavigation: () => {
- const adminStore = get(admin)
- const authStore = get(auth)
-
- const upgradeUrl = authStore?.user?.accountPortalAccess
- ? `${adminStore.accountPortalUrl}/portal/upgrade`
- : "/builder/portal/account/upgrade"
-
- const goToUpgradePage = () => {
- window.location.href = upgradeUrl
- }
- const goToPricingPage = () => {
- window.open("https://budibase.com/pricing/", "_blank")
- }
- store.update(state => {
- return {
- ...state,
- goToUpgradePage,
- goToPricingPage,
- }
- })
- },
- setLicense: () => {
- const license = get(auth).user.license
- const planType = license?.plan.type
- const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
- const isFreePlan = planType === Constants.PlanType.FREE
- const isBusinessPlan = planType === Constants.PlanType.BUSINESS
- const isEnterpriseTrial =
- planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
- const groupsEnabled = license.features.includes(
- Constants.Features.USER_GROUPS
- )
- const backupsEnabled = license.features.includes(
- Constants.Features.APP_BACKUPS
- )
- const scimEnabled = license.features.includes(Constants.Features.SCIM)
- const environmentVariablesEnabled = license.features.includes(
- Constants.Features.ENVIRONMENT_VARIABLES
- )
- const enforceableSSO = license.features.includes(
- Constants.Features.ENFORCEABLE_SSO
- )
- const brandingEnabled = license.features.includes(
- Constants.Features.BRANDING
- )
- const auditLogsEnabled = license.features.includes(
- Constants.Features.AUDIT_LOGS
- )
- const syncAutomationsEnabled = license.features.includes(
- Constants.Features.SYNC_AUTOMATIONS
- )
- const triggerAutomationRunEnabled = license.features.includes(
- Constants.Features.TRIGGER_AUTOMATION_RUN
- )
- const perAppBuildersEnabled = license.features.includes(
- Constants.Features.APP_BUILDERS
- )
- const budibaseAIEnabled = license.features.includes(
- Constants.Features.BUDIBASE_AI
- )
- const customAIConfigsEnabled = license.features.includes(
- Constants.Features.AI_CUSTOM_CONFIGS
- )
- store.update(state => {
- return {
- ...state,
- license,
- isEnterprisePlan,
- isFreePlan,
- isBusinessPlan,
- isEnterpriseTrial,
- groupsEnabled,
- backupsEnabled,
- brandingEnabled,
- budibaseAIEnabled,
- customAIConfigsEnabled,
- scimEnabled,
- environmentVariablesEnabled,
- auditLogsEnabled,
- enforceableSSO,
- syncAutomationsEnabled,
- triggerAutomationRunEnabled,
- perAppBuildersEnabled,
- }
- })
- },
- setQuotaUsage: async () => {
- const quotaUsage = await API.getQuotaUsage()
- store.update(state => {
- return {
- ...state,
- quotaUsage,
- }
- })
- await actions.setUsageMetrics()
- },
- usersLimitReached: userCount => {
- return usersLimitReached(userCount, get(store).userLimit)
- },
- usersLimitExceeded(userCount) {
- return usersLimitExceeded(userCount, get(store).userLimit)
- },
- setUsageMetrics: async () => {
- const usage = get(store).quotaUsage
- const license = get(auth).user.license
- const now = new Date()
-
- const getMetrics = (keys, license, quota) => {
- if (!license || !quota || !keys) {
- return {}
- }
- return keys.reduce((acc, key) => {
- const quotaLimit = license[key].value
- const quotaUsed = (quota[key] / quotaLimit) * 100
- acc[key] = quotaLimit > -1 ? Math.floor(quotaUsed) : -1
- return acc
- }, {})
- }
- const monthlyMetrics = getMetrics(
- ["queries", "automations"],
- license.quotas.usage.monthly,
- usage.monthly.current
- )
- const staticMetrics = getMetrics(
- ["apps", "rows"],
- license.quotas.usage.static,
- usage.usageQuota
- )
-
- const getDaysBetween = (dateStart, dateEnd) => {
- return dateEnd > dateStart
- ? Math.round(
- (dateEnd.getTime() - dateStart.getTime()) / oneDayInMilliseconds
- )
- : 0
- }
-
- const quotaResetDate = new Date(usage.quotaReset)
- const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
-
- const accountDowngraded =
- license?.billing?.subscription?.downgradeAt &&
- license?.billing?.subscription?.downgradeAt <= now.getTime() &&
- license?.billing?.subscription?.status === StripeStatus.PAST_DUE &&
- license?.plan.type === Constants.PlanType.FREE
-
- const pastDueAtMilliseconds = license?.billing?.subscription?.pastDueAt
- const downgradeAtMilliseconds =
- license?.billing?.subscription?.downgradeAt
- let pastDueDaysRemaining
- let pastDueEndDate
-
- if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
- pastDueEndDate = new Date(downgradeAtMilliseconds)
- pastDueDaysRemaining = getDaysBetween(
- new Date(pastDueAtMilliseconds),
- pastDueEndDate
- )
- }
-
- const userQuota = license.quotas.usage.static.users
- const userLimit = userQuota?.value
- const userCount = usage.usageQuota.users
- const userLimitReached = usersLimitReached(userCount, userLimit)
- const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
- const isCloudAccount = await isCloud()
- const errUserLimit =
- isCloudAccount &&
- license.plan.model === PlanModel.PER_USER &&
- userLimitExceeded
-
- store.update(state => {
- return {
- ...state,
- usageMetrics: { ...monthlyMetrics, ...staticMetrics },
- quotaResetDaysRemaining,
- quotaResetDate,
- accountDowngraded,
- accountPastDue: pastDueAtMilliseconds != null,
- pastDueEndDate,
- pastDueDaysRemaining,
- // user limits
- userCount,
- userLimit,
- userLimitReached,
- errUserLimit,
- }
- })
- },
- }
-
- return {
- subscribe: store.subscribe,
- ...actions,
- }
-}
-
-export const licensing = createLicensingStore()
diff --git a/packages/builder/src/stores/portal/licensing.ts b/packages/builder/src/stores/portal/licensing.ts
new file mode 100644
index 0000000000..0b56e14005
--- /dev/null
+++ b/packages/builder/src/stores/portal/licensing.ts
@@ -0,0 +1,305 @@
+import { get } from "svelte/store"
+import { API } from "@/api"
+import { auth, admin } from "@/stores/portal"
+import { Constants } from "@budibase/frontend-core"
+import { StripeStatus } from "@/components/portal/licensing/constants"
+import {
+ License,
+ MonthlyQuotaName,
+ PlanModel,
+ QuotaUsage,
+ StaticQuotaName,
+} from "@budibase/types"
+import { BudiStore } from "../BudiStore"
+
+const UNLIMITED = -1
+const ONE_DAY_MILLIS = 86400000
+
+type MonthlyMetrics = { [key in MonthlyQuotaName]?: number }
+type StaticMetrics = { [key in StaticQuotaName]?: number }
+type UsageMetrics = MonthlyMetrics & StaticMetrics
+
+interface LicensingState {
+ goToUpgradePage: () => void
+ goToPricingPage: () => void
+ // the top level license
+ license?: License
+ isFreePlan: boolean
+ isEnterprisePlan: boolean
+ isBusinessPlan: boolean
+ // features
+ groupsEnabled: boolean
+ backupsEnabled: boolean
+ brandingEnabled: boolean
+ scimEnabled: boolean
+ environmentVariablesEnabled: boolean
+ budibaseAIEnabled: boolean
+ customAIConfigsEnabled: boolean
+ auditLogsEnabled: boolean
+ // the currently used quotas from the db
+ quotaUsage?: QuotaUsage
+ // derived quota metrics for percentages used
+ usageMetrics?: UsageMetrics
+ // quota reset
+ quotaResetDaysRemaining?: number
+ quotaResetDate?: Date
+ // failed payments
+ accountPastDue: boolean
+ pastDueEndDate?: Date
+ pastDueDaysRemaining?: number
+ accountDowngraded: boolean
+ // user limits
+ userCount?: number
+ userLimit?: number
+ userLimitReached: boolean
+ errUserLimit: boolean
+}
+
+class LicensingStore extends BudiStore {
+ constructor() {
+ super({
+ // navigation
+ goToUpgradePage: () => {},
+ goToPricingPage: () => {},
+ // the top level license
+ license: undefined,
+ isFreePlan: true,
+ isEnterprisePlan: true,
+ isBusinessPlan: true,
+ // features
+ groupsEnabled: false,
+ backupsEnabled: false,
+ brandingEnabled: false,
+ scimEnabled: false,
+ environmentVariablesEnabled: false,
+ budibaseAIEnabled: false,
+ customAIConfigsEnabled: false,
+ auditLogsEnabled: 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: false,
+ pastDueEndDate: undefined,
+ pastDueDaysRemaining: undefined,
+ accountDowngraded: false,
+ // user limits
+ userCount: undefined,
+ userLimit: undefined,
+ userLimitReached: false,
+ errUserLimit: false,
+ })
+ }
+
+ usersLimitReached(userCount: number, userLimit = get(this.store).userLimit) {
+ if (userLimit === UNLIMITED || userLimit === undefined) {
+ return false
+ }
+ return userCount >= userLimit
+ }
+
+ usersLimitExceeded(userCount: number, userLimit = get(this.store).userLimit) {
+ if (userLimit === UNLIMITED || userLimit === undefined) {
+ return false
+ }
+ return userCount > userLimit
+ }
+
+ async isCloud() {
+ let adminStore = get(admin)
+ if (!adminStore.loaded) {
+ await admin.init()
+ adminStore = get(admin)
+ }
+ return adminStore.cloud
+ }
+
+ async init() {
+ this.setNavigation()
+ this.setLicense()
+ await this.setQuotaUsage()
+ }
+
+ setNavigation() {
+ const adminStore = get(admin)
+ const authStore = get(auth)
+
+ const upgradeUrl = authStore?.user?.accountPortalAccess
+ ? `${adminStore.accountPortalUrl}/portal/upgrade`
+ : "/builder/portal/account/upgrade"
+
+ const goToUpgradePage = () => {
+ window.location.href = upgradeUrl
+ }
+ const goToPricingPage = () => {
+ window.open("https://budibase.com/pricing/", "_blank")
+ }
+ this.update(state => {
+ return {
+ ...state,
+ goToUpgradePage,
+ goToPricingPage,
+ }
+ })
+ }
+
+ setLicense() {
+ const license = get(auth).user?.license
+ const planType = license?.plan.type
+ const features = license?.features || []
+ const isEnterprisePlan = planType === Constants.PlanType.ENTERPRISE
+ const isFreePlan = planType === Constants.PlanType.FREE
+ const isBusinessPlan = planType === Constants.PlanType.BUSINESS
+ const isEnterpriseTrial =
+ planType === Constants.PlanType.ENTERPRISE_BASIC_TRIAL
+ const groupsEnabled = features.includes(Constants.Features.USER_GROUPS)
+ const backupsEnabled = features.includes(Constants.Features.APP_BACKUPS)
+ const scimEnabled = features.includes(Constants.Features.SCIM)
+ const environmentVariablesEnabled = features.includes(
+ Constants.Features.ENVIRONMENT_VARIABLES
+ )
+ const enforceableSSO = features.includes(Constants.Features.ENFORCEABLE_SSO)
+ const brandingEnabled = features.includes(Constants.Features.BRANDING)
+ const auditLogsEnabled = features.includes(Constants.Features.AUDIT_LOGS)
+ const syncAutomationsEnabled = features.includes(
+ Constants.Features.SYNC_AUTOMATIONS
+ )
+ const triggerAutomationRunEnabled = features.includes(
+ Constants.Features.TRIGGER_AUTOMATION_RUN
+ )
+ const perAppBuildersEnabled = features.includes(
+ Constants.Features.APP_BUILDERS
+ )
+ const budibaseAIEnabled = features.includes(Constants.Features.BUDIBASE_AI)
+ const customAIConfigsEnabled = features.includes(
+ Constants.Features.AI_CUSTOM_CONFIGS
+ )
+ this.update(state => {
+ return {
+ ...state,
+ license,
+ isEnterprisePlan,
+ isFreePlan,
+ isBusinessPlan,
+ isEnterpriseTrial,
+ groupsEnabled,
+ backupsEnabled,
+ brandingEnabled,
+ budibaseAIEnabled,
+ customAIConfigsEnabled,
+ scimEnabled,
+ environmentVariablesEnabled,
+ auditLogsEnabled,
+ enforceableSSO,
+ syncAutomationsEnabled,
+ triggerAutomationRunEnabled,
+ perAppBuildersEnabled,
+ }
+ })
+ }
+
+ async setQuotaUsage() {
+ const quotaUsage = await API.getQuotaUsage()
+ this.update(state => {
+ return {
+ ...state,
+ quotaUsage,
+ }
+ })
+ await this.setUsageMetrics()
+ }
+
+ async setUsageMetrics() {
+ const usage = get(this.store).quotaUsage
+ const license = get(auth).user?.license
+ const now = new Date()
+ if (!license || !usage) {
+ return
+ }
+
+ // Process monthly metrics
+ const monthlyMetrics = [
+ MonthlyQuotaName.QUERIES,
+ MonthlyQuotaName.AUTOMATIONS,
+ ].reduce((acc: MonthlyMetrics, key) => {
+ const limit = license.quotas.usage.monthly[key].value
+ const used = (usage.monthly.current?.[key] || 0 / limit) * 100
+ acc[key] = limit > -1 ? Math.floor(used) : -1
+ return acc
+ }, {})
+
+ // Process static metrics
+ const staticMetrics = [StaticQuotaName.APPS, StaticQuotaName.ROWS].reduce(
+ (acc: StaticMetrics, key) => {
+ const limit = license.quotas.usage.static[key].value
+ const used = (usage.usageQuota[key] || 0 / limit) * 100
+ acc[key] = limit > -1 ? Math.floor(used) : -1
+ return acc
+ },
+ {}
+ )
+
+ const getDaysBetween = (dateStart: Date, dateEnd: Date) => {
+ return dateEnd > dateStart
+ ? Math.round((dateEnd.getTime() - dateStart.getTime()) / ONE_DAY_MILLIS)
+ : 0
+ }
+
+ const quotaResetDate = new Date(usage.quotaReset)
+ const quotaResetDaysRemaining = getDaysBetween(now, quotaResetDate)
+
+ const accountDowngraded =
+ !!license.billing?.subscription?.downgradeAt &&
+ license.billing?.subscription?.downgradeAt <= now.getTime() &&
+ license.billing?.subscription?.status === StripeStatus.PAST_DUE &&
+ license.plan.type === Constants.PlanType.FREE
+
+ const pastDueAtMilliseconds = license.billing?.subscription?.pastDueAt
+ const downgradeAtMilliseconds = license.billing?.subscription?.downgradeAt
+ let pastDueDaysRemaining: number
+ let pastDueEndDate: Date
+
+ if (pastDueAtMilliseconds && downgradeAtMilliseconds) {
+ pastDueEndDate = new Date(downgradeAtMilliseconds)
+ pastDueDaysRemaining = getDaysBetween(
+ new Date(pastDueAtMilliseconds),
+ pastDueEndDate
+ )
+ }
+
+ const userQuota = license.quotas.usage.static.users
+ const userLimit = userQuota.value
+ const userCount = usage.usageQuota.users
+ const userLimitReached = this.usersLimitReached(userCount, userLimit)
+ const userLimitExceeded = this.usersLimitExceeded(userCount, userLimit)
+ const isCloudAccount = await this.isCloud()
+ const errUserLimit =
+ isCloudAccount &&
+ license.plan.model === PlanModel.PER_USER &&
+ userLimitExceeded
+
+ this.update(state => {
+ return {
+ ...state,
+ usageMetrics: { ...monthlyMetrics, ...staticMetrics },
+ quotaResetDaysRemaining,
+ quotaResetDate,
+ accountDowngraded,
+ accountPastDue: pastDueAtMilliseconds != null,
+ pastDueEndDate,
+ pastDueDaysRemaining,
+ // user limits
+ userCount,
+ userLimit,
+ userLimitReached,
+ errUserLimit,
+ }
+ })
+ }
+}
+
+export const licensing = new LicensingStore()
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 9d0bddcc92..16dc3c97e7 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -216,11 +216,11 @@ const deleteRowHandler = async action => {
const triggerAutomationHandler = async action => {
const { fields, notificationOverride, timeout } = action.parameters
try {
- const result = await API.triggerAutomation({
- automationId: action.parameters.automationId,
+ const result = await API.triggerAutomation(
+ action.parameters.automationId,
fields,
- timeout,
- })
+ timeout
+ )
// Value will exist if automation is synchronous, so return it.
if (result.value) {
diff --git a/packages/frontend-core/src/api/groups.ts b/packages/frontend-core/src/api/groups.ts
index c09c5284ec..e6374f257c 100644
--- a/packages/frontend-core/src/api/groups.ts
+++ b/packages/frontend-core/src/api/groups.ts
@@ -1,4 +1,8 @@
-import { SearchUserGroupResponse, UserGroup } from "@budibase/types"
+import {
+ SearchGroupResponse,
+ SearchUserGroupResponse,
+ UserGroup,
+} from "@budibase/types"
import { BaseAPIClient } from "./types"
export interface GroupEndpoints {
@@ -64,9 +68,10 @@ export const buildGroupsEndpoints = (API: BaseAPIClient): GroupEndpoints => {
* Gets all the user groups
*/
getGroups: async () => {
- return await API.get({
+ const res = await API.get({
url: "/api/global/groups",
})
+ return res.data
},
/**
diff --git a/packages/types/src/api/web/global/self.ts b/packages/types/src/api/web/global/self.ts
index 9e879e6c3c..517559d1ca 100644
--- a/packages/types/src/api/web/global/self.ts
+++ b/packages/types/src/api/web/global/self.ts
@@ -1,5 +1,6 @@
+import { License } from "../../../sdk"
+import { Account, DevInfo, User } from "../../../documents"
import { FeatureFlags } from "@budibase/types"
-import { DevInfo, User } from "../../../documents"
export interface GenerateAPIKeyRequest {
userId?: string
@@ -10,4 +11,9 @@ export interface FetchAPIKeyResponse extends DevInfo {}
export interface GetGlobalSelfResponse extends User {
flags?: FeatureFlags
+ account?: Account
+ license: License
+ budibaseAccess: boolean
+ accountPortalAccess: boolean
+ csrfToken: boolean
}
diff --git a/packages/worker/src/api/controllers/global/self.ts b/packages/worker/src/api/controllers/global/self.ts
index eb704ccf02..3464bff88f 100644
--- a/packages/worker/src/api/controllers/global/self.ts
+++ b/packages/worker/src/api/controllers/global/self.ts
@@ -84,15 +84,15 @@ export async function fetchAPIKey(ctx: UserCtx) {
}
/**
- * Add the attributes that are session based to the current user.
+ *
*/
-const addSessionAttributesToUser = (ctx: any) => {
- ctx.body.account = ctx.user.account
- ctx.body.license = ctx.user.license
- ctx.body.budibaseAccess = !!ctx.user.budibaseAccess
- ctx.body.accountPortalAccess = !!ctx.user.accountPortalAccess
- ctx.body.csrfToken = ctx.user.csrfToken
-}
+const getUserSessionAttributes = (ctx: any) => ({
+ account: ctx.user.account,
+ license: ctx.user.license,
+ budibaseAccess: !!ctx.user.budibaseAccess,
+ accountPortalAccess: !!ctx.user.accountPortalAccess,
+ csrfToken: ctx.user.csrfToken,
+})
export async function getSelf(ctx: UserCtx) {
if (!ctx.user) {
@@ -108,12 +108,19 @@ export async function getSelf(ctx: UserCtx) {
// get the main body of the user
const user = await userSdk.db.getUser(userId)
- ctx.body = await groups.enrichUserRolesFromGroups(user)
+ const enrichedUser = await groups.enrichUserRolesFromGroups(user)
+
+ // add the attributes that are session based to the current user
+ const sessionAttributes = getUserSessionAttributes(ctx)
// add the feature flags for this tenant
- ctx.body.flags = await features.flags.fetch()
+ const flags = await features.flags.fetch()
- addSessionAttributesToUser(ctx)
+ ctx.body = {
+ ...enrichedUser,
+ ...sessionAttributes,
+ flags,
+ }
}
export const syncAppFavourites = async (processedAppIds: string[]) => {
diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts
index a028f4fd33..83f2f41b0e 100644
--- a/packages/worker/src/api/controllers/global/users.ts
+++ b/packages/worker/src/api/controllers/global/users.ts
@@ -72,12 +72,14 @@ export const save = async (ctx: UserCtx) => {
const requestUser = ctx.request.body
// Do not allow the account holder role to be changed
- const accountMetadata = await users.getExistingAccounts([requestUser.email])
- if (accountMetadata?.length > 0) {
- if (
- requestUser.admin?.global !== true ||
- requestUser.builder?.global !== true
- ) {
+ if (
+ requestUser.admin?.global !== true ||
+ requestUser.builder?.global !== true
+ ) {
+ const accountMetadata = await users.getExistingAccounts([
+ requestUser.email,
+ ])
+ if (accountMetadata?.length > 0) {
throw Error("Cannot set role of account holder")
}
}
@@ -441,7 +443,6 @@ export const checkInvite = async (ctx: UserCtx) => {
} catch (e) {
console.warn("Error getting invite from code", e)
ctx.throw(400, "There was a problem with the invite")
- return
}
ctx.body = {
email: invite.email,
@@ -472,7 +473,6 @@ export const updateInvite = async (
invite = await cache.invite.getCode(code)
} catch (e) {
ctx.throw(400, "There was a problem with the invite")
- return
}
let updated = {