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