diff --git a/packages/builder/src/builderStore/websocket.js b/packages/builder/src/builderStore/websocket.js index e27e08e31d..af6d58ee7f 100644 --- a/packages/builder/src/builderStore/websocket.js +++ b/packages/builder/src/builderStore/websocket.js @@ -1,15 +1,18 @@ import { createWebsocket } from "@budibase/frontend-core" -import { userStore } from "builderStore" +import { userStore, store } from "builderStore" import { datasources, tables } from "stores/backend" +import { get } from "svelte/store" +import { auth } from "stores/portal" import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core" +import { notifications } from "@budibase/bbui" export const createBuilderWebsocket = appId => { const socket = createWebsocket("/socket/builder") // Built-in events socket.on("connect", () => { - socket.emit(BuilderSocketEvent.SelectApp, appId, response => { - userStore.actions.init(response.users) + socket.emit(BuilderSocketEvent.SelectApp, { appId }, ({ users }) => { + userStore.actions.init(users) }) }) socket.on("connect_error", err => { @@ -20,8 +23,21 @@ export const createBuilderWebsocket = appId => { }) // User events - socket.onOther(SocketEvent.UserUpdate, userStore.actions.updateUser) - socket.onOther(SocketEvent.UserDisconnect, userStore.actions.removeUser) + socket.onOther(SocketEvent.UserUpdate, ({ user }) => { + userStore.actions.updateUser(user) + }) + socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => { + userStore.actions.removeUser(sessionId) + }) + socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => { + if (userId === get(auth)?.user?._id) { + notifications.success("You can now edit screens and automations") + store.update(state => ({ + ...state, + hasLock: true, + })) + } + }) // Table events socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => { diff --git a/packages/builder/src/helpers/userInitials.js b/packages/builder/src/helpers/userInitials.js deleted file mode 100644 index c87d38c494..0000000000 --- a/packages/builder/src/helpers/userInitials.js +++ /dev/null @@ -1,13 +0,0 @@ -const getUserInitials = user => { - if (user.firstName && user.lastName) { - return user.firstName[0] + user.lastName[0] - } else if (user.firstName) { - return user.firstName[0] - } else if (user.email) { - return user.email[0] - } - - return "U" -} - -export default getUserInitials 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 d6f2cce51f..e2ac6c40b8 100644 --- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte @@ -31,7 +31,6 @@ import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte" import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte" import ScimBanner from "../_components/SCIMBanner.svelte" - import { helpers } from "@budibase/shared-core" export let userId @@ -91,7 +90,6 @@ $: readonly = !$auth.isAdmin || scimEnabled $: privileged = user?.admin?.global || user?.builder?.global $: nameLabel = getNameLabel(user) - $: initials = helpers.getUserInitials(user) $: filteredGroups = getFilteredGroups($groups, searchTerm) $: availableApps = getAvailableApps($apps, privileged, user?.roles) $: userGroups = $groups.filter(x => { diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index 2ab68b11b4..ce64965af7 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -2,7 +2,6 @@ import { derived, writable, get } from "svelte/store" import { API } from "api" import { admin } from "stores/portal" import analytics from "analytics" -import getUserInitials from "helpers/userInitials.js" export function createAuthStore() { const auth = writable({ @@ -14,12 +13,10 @@ export function createAuthStore() { postLogout: false, }) const store = derived(auth, $store => { - let initials = null let isAdmin = false let isBuilder = false if ($store.user) { const user = $store.user - initials = getUserInitials(user) isAdmin = !!user.admin?.global isBuilder = !!user.builder?.global } @@ -30,7 +27,6 @@ export function createAuthStore() { tenantSet: $store.tenantSet, loaded: $store.loaded, postLogout: $store.postLogout, - initials, isAdmin, isBuilder, isSSO: !!$store.user?.provider, diff --git a/packages/frontend-core/src/components/grid/lib/websocket.js b/packages/frontend-core/src/components/grid/lib/websocket.js index 11164b3148..9de0199cfc 100644 --- a/packages/frontend-core/src/components/grid/lib/websocket.js +++ b/packages/frontend-core/src/components/grid/lib/websocket.js @@ -11,10 +11,13 @@ export const createGridWebsocket = context => { return } // Identify which table we are editing - socket.emit(GridSocketEvent.SelectTable, tableId, response => { - // handle initial connection info - users.set(response.users) - }) + socket.emit( + GridSocketEvent.SelectTable, + { tableId }, + ({ users: gridUsers }) => { + users.set(gridUsers) + } + ) } // Built-in events @@ -26,29 +29,29 @@ export const createGridWebsocket = context => { }) // User events - socket.onOther(SocketEvent.UserUpdate, user => { + socket.onOther(SocketEvent.UserUpdate, ({ user }) => { users.actions.updateUser(user) }) - socket.onOther(SocketEvent.UserDisconnect, user => { - users.actions.removeUser(user) + socket.onOther(SocketEvent.UserDisconnect, ({ sessionId }) => { + users.actions.removeUser(sessionId) }) // Row events - socket.onOther(GridSocketEvent.RowChange, async data => { - if (data.id) { - rows.actions.replaceRow(data.id, data.row) - } else if (data.row.id) { + socket.onOther(GridSocketEvent.RowChange, async ({ id, row }) => { + if (id) { + rows.actions.replaceRow(id, row) + } else if (row.id) { // Handle users table edge cased - await rows.actions.refreshRow(data.row.id) + await rows.actions.refreshRow(row.id) } }) // Table events - socket.onOther(GridSocketEvent.TableChange, data => { + socket.onOther(GridSocketEvent.TableChange, ({ table: newTable }) => { // Only update table if one exists. If the table was deleted then we don't // want to know - let the builder navigate away - if (data.table) { - table.set(data.table) + if (newTable) { + table.set(newTable) } }) @@ -57,7 +60,7 @@ export const createGridWebsocket = context => { // Notify selected cell changes focusedCellId.subscribe($focusedCellId => { - socket.emit(GridSocketEvent.SelectCell, $focusedCellId) + socket.emit(GridSocketEvent.SelectCell, { cellId: $focusedCellId }) }) return () => socket?.disconnect() diff --git a/packages/server/src/websockets/builder.ts b/packages/server/src/websockets/builder.ts index 0015b7f601..0580a58b42 100644 --- a/packages/server/src/websockets/builder.ts +++ b/packages/server/src/websockets/builder.ts @@ -5,7 +5,7 @@ import http from "http" import Koa from "koa" import { Datasource, Table, SocketSession, ContextUser } from "@budibase/types" import { gridSocket } from "./index" -import { clearLock } from "../utilities/redis" +import { clearLock, updateLock } from "../utilities/redis" import { Socket } from "socket.io" import { BuilderSocketEvent } from "@budibase/shared-core" @@ -16,7 +16,7 @@ export default class BuilderSocket extends BaseSocket { async onConnect(socket?: Socket) { // Initial identification of selected app - socket?.on(BuilderSocketEvent.SelectApp, async (appId, callback) => { + socket?.on(BuilderSocketEvent.SelectApp, async ({ appId }, callback) => { await this.joinRoom(socket, appId) // Reply with all users in current room @@ -26,7 +26,8 @@ export default class BuilderSocket extends BaseSocket { } async onDisconnect(socket: Socket) { - // Remove app lock from this user if they have no other connections + // Remove app lock from this user if they have no other connections, + // and transfer it to someone else if possible try { // @ts-ignore const session: SocketSession = socket.data @@ -36,9 +37,26 @@ export default class BuilderSocket extends BaseSocket { return _id === otherSession._id && sessionId !== otherSession.sessionId }) if (!hasOtherSession && room) { + // Clear the lock from this user since they had no other sessions // @ts-ignore const user: ContextUser = { _id: socket.data._id } await clearLock(room, user) + + // Transfer lock ownership to the next oldest user + let otherSessions = sessions.filter(x => x._id !== _id).slice() + otherSessions.sort((a, b) => { + return a.connectedAt < b.connectedAt ? -1 : 1 + }) + const nextSession = otherSessions[0] + if (nextSession) { + const { _id, email, firstName, lastName } = nextSession + // @ts-ignore + const nextUser: ContextUser = { _id, email, firstName, lastName } + await updateLock(room, nextUser) + this.io.to(room).emit(BuilderSocketEvent.LockTransfer, { + userId: _id, + }) + } } } catch (e) { // This is fine, just means this user didn't hold the lock diff --git a/packages/server/src/websockets/grid.ts b/packages/server/src/websockets/grid.ts index c12715990e..6731c2d899 100644 --- a/packages/server/src/websockets/grid.ts +++ b/packages/server/src/websockets/grid.ts @@ -15,7 +15,7 @@ export default class GridSocket extends BaseSocket { async onConnect(socket: Socket) { // Initial identification of connected spreadsheet - socket.on(GridSocketEvent.SelectTable, async (tableId, callback) => { + socket.on(GridSocketEvent.SelectTable, async ({ tableId }, callback) => { await this.joinRoom(socket, tableId) // Reply with all users in current room @@ -24,7 +24,7 @@ export default class GridSocket extends BaseSocket { }) // Handle users selecting a new cell - socket.on(GridSocketEvent.SelectCell, cellId => { + socket.on(GridSocketEvent.SelectCell, ({ cellId }) => { this.updateUser(socket, { focusedCellId: cellId }) }) } diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts index 3bf9f27416..d8cc10bda4 100644 --- a/packages/server/src/websockets/websocket.ts +++ b/packages/server/src/websockets/websocket.ts @@ -77,6 +77,7 @@ export class BaseSocket { firstName, lastName, sessionId: socket.id, + connectedAt: Date.now(), } next() } @@ -173,7 +174,9 @@ export class BaseSocket { ) const prunedSessionIds = sessionIds.filter((id, idx) => { if (!sessionsExist[idx]) { - this.io.to(room).emit(SocketEvent.UserDisconnect, sessionIds[idx]) + this.io.to(room).emit(SocketEvent.UserDisconnect, { + sessionId: sessionIds[idx], + }) return false } return true @@ -216,7 +219,9 @@ export class BaseSocket { } // Notify other users - socket.to(room).emit(SocketEvent.UserUpdate, user) + socket.to(room).emit(SocketEvent.UserUpdate, { + user, + }) } // Disconnects a socket from its current room @@ -242,7 +247,7 @@ export class BaseSocket { ) // Notify other users - socket.to(room).emit(SocketEvent.UserDisconnect, sessionId) + socket.to(room).emit(SocketEvent.UserDisconnect, { sessionId }) } // Updates a connected user's metadata, assuming a room change is not required. diff --git a/packages/shared-core/src/constants.ts b/packages/shared-core/src/constants.ts index 5802a6d88a..307285012b 100644 --- a/packages/shared-core/src/constants.ts +++ b/packages/shared-core/src/constants.ts @@ -85,6 +85,7 @@ export enum BuilderSocketEvent { SelectApp = "SelectApp", TableChange = "TableChange", DatasourceChange = "DatasourceChange", + LockTransfer = "LockTransfer", } export const SocketSessionTTL = 60 diff --git a/packages/shared-core/src/helpers/helpers.ts b/packages/shared-core/src/helpers/helpers.ts index 8b8f5a3918..8c4795f226 100644 --- a/packages/shared-core/src/helpers/helpers.ts +++ b/packages/shared-core/src/helpers/helpers.ts @@ -35,7 +35,10 @@ export const getUserInitials = (user: User) => { let initials = "" initials += user.firstName ? user.firstName[0] : "" initials += user.lastName ? user.lastName[0] : "" - return initials === "" ? user.email[0] : initials + if (initials !== "") { + return initials + } + return user.email?.[0] || "U" } /** diff --git a/packages/types/src/sdk/websocket.ts b/packages/types/src/sdk/websocket.ts index 4fa7e155d6..40e2654e82 100644 --- a/packages/types/src/sdk/websocket.ts +++ b/packages/types/src/sdk/websocket.ts @@ -5,4 +5,5 @@ export interface SocketSession { lastName?: string sessionId: string room?: string + connectedAt: number }