diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte
index 6f87c8c362..7d61751364 100644
--- a/packages/builder/src/components/common/NavItem.svelte
+++ b/packages/builder/src/components/common/NavItem.svelte
@@ -2,7 +2,7 @@
import { Icon } from "@budibase/bbui"
import { createEventDispatcher, getContext } from "svelte"
import { helpers } from "@budibase/shared-core"
- import UserAvatars from "../../pages/builder/app/[application]/_components/UserAvatars.svelte"
+ import { UserAvatars } from "@budibase/frontend-core"
export let icon
export let withArrow = false
diff --git a/packages/builder/src/components/start/AppRow.svelte b/packages/builder/src/components/start/AppRow.svelte
index 50e6b8466a..2e7719987d 100644
--- a/packages/builder/src/components/start/AppRow.svelte
+++ b/packages/builder/src/components/start/AppRow.svelte
@@ -2,12 +2,12 @@
import { Heading, Body, Button, Icon } from "@budibase/bbui"
import { processStringSync } from "@budibase/string-templates"
import { goto } from "@roxi/routify"
- import { UserAvatar } from "@budibase/frontend-core"
+ import { UserAvatars } from "@budibase/frontend-core"
export let app
export let lockedAction
- $: editing = app?.lockedBy != null
+ $: editing = app.sessions?.length
const handleDefaultClick = () => {
if (window.innerWidth < 640) {
@@ -41,7 +41,7 @@
{#if editing}
Currently editing
-
+
{:else if app.updatedAt}
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
index 805bc6ed29..ba1a633287 100644
--- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
@@ -26,7 +26,7 @@
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
- import UserAvatars from "./_components/UserAvatars.svelte"
+ import { UserAvatars } from "@budibase/frontend-core"
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
diff --git a/packages/builder/src/pages/builder/app/[application]/_components/UserAvatars.svelte b/packages/frontend-core/src/components/UserAvatars.svelte
similarity index 100%
rename from packages/builder/src/pages/builder/app/[application]/_components/UserAvatars.svelte
rename to packages/frontend-core/src/components/UserAvatars.svelte
diff --git a/packages/frontend-core/src/components/index.js b/packages/frontend-core/src/components/index.js
index 3005c85d01..01a7c78cb8 100644
--- a/packages/frontend-core/src/components/index.js
+++ b/packages/frontend-core/src/components/index.js
@@ -2,4 +2,5 @@ export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte"
export { default as Testimonial } from "./Testimonial.svelte"
export { default as UserAvatar } from "./UserAvatar.svelte"
+export { default as UserAvatars } from "./UserAvatars.svelte"
export { Grid } from "./grid"
diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts
index c068a422b0..a2448a0384 100644
--- a/packages/server/src/api/controllers/application.ts
+++ b/packages/server/src/api/controllers/application.ts
@@ -49,6 +49,7 @@ import {
MigrationType,
PlanType,
Screen,
+ SocketSession,
UserCtx,
} from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
@@ -183,6 +184,7 @@ export async function fetch(ctx: UserCtx) {
const appIds = apps
.filter((app: any) => app.status === "development")
.map((app: any) => app.appId)
+
// get the locks for all the dev apps
if (dev || all) {
const locks = await getLocksById(appIds)
@@ -197,7 +199,10 @@ export async function fetch(ctx: UserCtx) {
}
}
- ctx.body = await checkAppMetadata(apps)
+ // Enrich apps with all builder user sessions
+ const enrichedApps = await sdk.users.sessions.enrichApps(apps)
+
+ ctx.body = await checkAppMetadata(enrichedApps)
}
export async function fetchAppDefinition(ctx: UserCtx) {
diff --git a/packages/server/src/sdk/users/index.ts b/packages/server/src/sdk/users/index.ts
index c8431f7e61..c03eab7c2a 100644
--- a/packages/server/src/sdk/users/index.ts
+++ b/packages/server/src/sdk/users/index.ts
@@ -1,5 +1,7 @@
import * as utils from "./utils"
+import * as sessions from "./sessions"
export default {
...utils,
+ sessions,
}
diff --git a/packages/server/src/sdk/users/sessions.ts b/packages/server/src/sdk/users/sessions.ts
new file mode 100644
index 0000000000..c413242277
--- /dev/null
+++ b/packages/server/src/sdk/users/sessions.ts
@@ -0,0 +1,38 @@
+import { builderSocket } from "../../websockets"
+import { App, SocketSession } from "@budibase/types"
+
+export const enrichApps = async (apps: App[]) => {
+ // Sessions can only exist for dev app IDs
+ const devAppIds = apps
+ .filter((app: any) => app.status === "development")
+ .map((app: any) => app.appId)
+
+ // Get all sessions for all apps and enrich app list
+ const sessions = await builderSocket?.getRoomSessions(devAppIds)
+ if (sessions?.length) {
+ let appSessionMap: Record = {}
+ sessions.forEach(session => {
+ const room = session.room
+ if (!room) {
+ return
+ }
+ if (!appSessionMap[room]) {
+ appSessionMap[room] = []
+ }
+ appSessionMap[room].push(session)
+ })
+ return apps.map(app => {
+ // Shallow clone to avoid mutating original reference
+ let enriched = { ...app }
+ const sessions = appSessionMap[app.appId]
+ if (sessions?.length) {
+ enriched.sessions = sessions
+ } else {
+ delete enriched.sessions
+ }
+ return enriched
+ })
+ } else {
+ return apps
+ }
+}
diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts
index 24cac3c37d..9dea67ef5f 100644
--- a/packages/server/src/websockets/websocket.ts
+++ b/packages/server/src/websockets/websocket.ts
@@ -125,9 +125,18 @@ export class BaseSocket {
}
// Gets an array of all redis keys of users inside a certain room
- async getRoomSessionIds(room: string): Promise {
- const keys = await this.redisClient?.get(this.getRoomKey(room))
- return keys || []
+ async getRoomSessionIds(room: string | string[]): Promise {
+ if (Array.isArray(room)) {
+ const roomKeys = room.map(this.getRoomKey.bind(this))
+ const roomSessionIdMap = await this.redisClient?.bulkGet(roomKeys)
+ let sessionIds: any[] = []
+ Object.values(roomSessionIdMap || {}).forEach(roomSessionIds => {
+ sessionIds = sessionIds.concat(roomSessionIds)
+ })
+ return sessionIds
+ } else {
+ return (await this.redisClient?.get(this.getRoomKey(room))) || []
+ }
}
// Sets the list of redis keys for users inside a certain room.
@@ -137,7 +146,7 @@ export class BaseSocket {
}
// Gets a list of all users inside a certain room
- async getRoomSessions(room?: string): Promise {
+ async getRoomSessions(room?: string | string[]): Promise {
if (room) {
const sessionIds = await this.getRoomSessionIds(room)
const keys = sessionIds.map(this.getSessionKey.bind(this))
diff --git a/packages/types/src/documents/app/app.ts b/packages/types/src/documents/app/app.ts
index 258eaef297..f42422b557 100644
--- a/packages/types/src/documents/app/app.ts
+++ b/packages/types/src/documents/app/app.ts
@@ -1,4 +1,5 @@
import { User, Document } from "../"
+import { SocketSession } from "../../sdk"
export type AppMetadataErrors = { [key: string]: string[] }
@@ -17,6 +18,7 @@ export interface App extends Document {
customTheme?: AppCustomTheme
revertableVersion?: string
lockedBy?: User
+ sessions?: SocketSession[]
navigation?: AppNavigation
automationErrors?: AppMetadataErrors
icon?: AppIcon