Merge pull request #10789 from Budibase/collaboration-lock-transfer
Transfer lock to next oldest session when disconnecting from the builder
This commit is contained in:
commit
c53b7b0a44
|
@ -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 }) => {
|
||||
|
|
|
@ -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
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -85,6 +85,7 @@ export enum BuilderSocketEvent {
|
|||
SelectApp = "SelectApp",
|
||||
TableChange = "TableChange",
|
||||
DatasourceChange = "DatasourceChange",
|
||||
LockTransfer = "LockTransfer",
|
||||
}
|
||||
|
||||
export const SocketSessionTTL = 60
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,4 +5,5 @@ export interface SocketSession {
|
|||
lastName?: string
|
||||
sessionId: string
|
||||
room?: string
|
||||
connectedAt: number
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue