Improve builder socket
This commit is contained in:
parent
cb9d35f6aa
commit
6ed691be1d
|
@ -2,6 +2,7 @@ import { getFrontendStore } from "./store/frontend"
|
|||
import { getAutomationStore } from "./store/automation"
|
||||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { getUsersStore } from "./store/users"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
@ -12,6 +13,7 @@ export const store = getFrontendStore()
|
|||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
export const usersStore = getUsersStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from "builderStore/dataBinding"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { getComponentFieldOptions } from "helpers/formFields"
|
||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
initialised: false,
|
||||
|
@ -84,10 +85,14 @@ const INITIAL_FRONTEND_STATE = {
|
|||
// Onboarding
|
||||
onboarding: false,
|
||||
tourNodes: null,
|
||||
|
||||
// Multi user collab
|
||||
users: [],
|
||||
}
|
||||
|
||||
export const getFrontendStore = () => {
|
||||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||
let websocket
|
||||
|
||||
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
||||
// 409s. All screen doc mutations (aside from creation) use this function,
|
||||
|
@ -112,9 +117,11 @@ export const getFrontendStore = () => {
|
|||
store.actions = {
|
||||
reset: () => {
|
||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||
websocket?.disconnect()
|
||||
},
|
||||
initialise: async pkg => {
|
||||
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
||||
websocket = createBuilderWebsocket()
|
||||
|
||||
await store.actions.components.refreshDefinitions(application.appId)
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export const getUsersStore = () => {
|
||||
const initialValue = {
|
||||
users: [],
|
||||
}
|
||||
const store = writable(initialValue)
|
||||
|
||||
return store
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import { store } from "builderStore"
|
||||
|
||||
export const createBuilderWebsocket = () => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.emit("get-users", null, response => {
|
||||
console.log("conntected!", response.users)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
users: response.users,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
socket.on("user-update", user => {})
|
||||
|
||||
socket.on("user-disconnect", user => {})
|
||||
|
||||
socket.on("connect_error", err => {
|
||||
console.log("Failed to connect to builder websocket:", err.message)
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
export let users = []
|
||||
</script>
|
||||
|
||||
<div class="avatars">
|
||||
{#each users as user}
|
||||
<div class="avatar" style="background:{user.color};" title={user.email}>
|
||||
{user.email[0]}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatars {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
line-height: 0;
|
||||
}
|
||||
.avatar:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -22,6 +22,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 { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||
|
||||
export let application
|
||||
|
@ -125,10 +126,7 @@
|
|||
})
|
||||
|
||||
onDestroy(() => {
|
||||
store.update(state => {
|
||||
state.appId = null
|
||||
return state
|
||||
})
|
||||
store.actions.reset()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -211,6 +209,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="toprightnav">
|
||||
<UserAvatars users={$store.users} />
|
||||
<AppActions {application} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
||||
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
||||
import NewRow from "./NewRow.svelte"
|
||||
import { createWebsocket } from "../lib/websocket"
|
||||
import { createGridWebsocket } from "../lib/websocket"
|
||||
import {
|
||||
MaxCellRenderHeight,
|
||||
MaxCellRenderWidthOverflow,
|
||||
|
@ -98,7 +98,7 @@
|
|||
export const getContext = () => context
|
||||
|
||||
// Initialise websocket for multi-user
|
||||
onMount(() => createWebsocket(context))
|
||||
onMount(() => createGridWebsocket(context))
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
.users {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,24 +1,9 @@
|
|||
import { get } from "svelte/store"
|
||||
import { io } from "socket.io-client"
|
||||
import { createWebsocket } from "../../../utils"
|
||||
|
||||
export const createWebsocket = context => {
|
||||
export const createGridWebsocket = context => {
|
||||
const { rows, tableId, users, userId, focusedCellId } = context
|
||||
|
||||
// Determine connection info
|
||||
const tls = location.protocol === "https:"
|
||||
const proto = tls ? "wss:" : "ws:"
|
||||
const host = location.hostname
|
||||
const port = location.port || (tls ? 443 : 80)
|
||||
const socket = io(`${proto}//${host}:${port}`, {
|
||||
path: "/socket/grid",
|
||||
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
||||
reconnectionAttempts: 3,
|
||||
// Delay reconnection attempt by 5 seconds
|
||||
reconnectionDelay: 5000,
|
||||
reconnectionDelayMax: 5000,
|
||||
// Timeout after 4 seconds so we never stack requests
|
||||
timeout: 4000,
|
||||
})
|
||||
const socket = createWebsocket("/socket/grid")
|
||||
|
||||
const connectToTable = tableId => {
|
||||
if (!socket.connected) {
|
||||
|
|
|
@ -21,35 +21,6 @@ export const createStores = () => {
|
|||
return 0
|
||||
}
|
||||
})
|
||||
// Enrich users with colors
|
||||
.map((user, idx) => {
|
||||
// Generate random colour hue
|
||||
let hue = 1
|
||||
for (let i = 0; i < user.email.length && i < 5; i++) {
|
||||
hue *= user.email.charCodeAt(i + 1)
|
||||
hue /= 17
|
||||
}
|
||||
hue = hue % 360
|
||||
const color =
|
||||
idx === 0
|
||||
? "var(--spectrum-global-color-blue-400)"
|
||||
: `hsl(${hue}, 50%, 40%)`
|
||||
|
||||
// Generate friendly label
|
||||
let label = user.email
|
||||
if (user.firstName) {
|
||||
label = user.firstName
|
||||
if (user.lastName) {
|
||||
label += ` ${user.lastName}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
color,
|
||||
label,
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
[]
|
||||
|
|
|
@ -3,3 +3,4 @@ export * as JSONUtils from "./json"
|
|||
export * as CookieUtils from "./cookies"
|
||||
export * as RoleUtils from "./roles"
|
||||
export * as Utils from "./utils"
|
||||
export { createWebsocket } from "./websocket"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { io } from "socket.io-client"
|
||||
|
||||
export const createWebsocket = path => {
|
||||
if (!path) {
|
||||
throw "A websocket path must be provided"
|
||||
}
|
||||
|
||||
// Determine connection info
|
||||
const tls = location.protocol === "https:"
|
||||
const proto = tls ? "wss:" : "ws:"
|
||||
const host = location.hostname
|
||||
const port = location.port || (tls ? 443 : 80)
|
||||
return io(`${proto}//${host}:${port}`, {
|
||||
path,
|
||||
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
||||
reconnectionAttempts: 3,
|
||||
// Delay reconnection attempt by 5 seconds
|
||||
reconnectionDelay: 5000,
|
||||
reconnectionDelayMax: 5000,
|
||||
// Timeout after 4 seconds so we never stack requests
|
||||
timeout: 4000,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import authorized from "../middleware/authorized"
|
||||
import Socket from "./websocket"
|
||||
import { permissions } from "@budibase/backend-core"
|
||||
import http from "http"
|
||||
import Koa from "koa"
|
||||
import { Table } from "@budibase/types"
|
||||
|
||||
export default class BuilderSocket extends Socket {
|
||||
constructor(app: Koa, server: http.Server) {
|
||||
super(app, server, "/socket/builder", [authorized(permissions.BUILDER)])
|
||||
|
||||
this.io.on("connection", socket => {
|
||||
// Join a room for this app
|
||||
const user = socket.data.user
|
||||
const appId = socket.data.appId
|
||||
socket.join(appId)
|
||||
socket.to(appId).emit("user-update", socket.data.user)
|
||||
console.log(`Builder user connected: ${user?.id}`)
|
||||
|
||||
// Initial identification of connected spreadsheet
|
||||
socket.on("get-users", async (payload, callback) => {
|
||||
const sockets = await this.io.in(appId).fetchSockets()
|
||||
callback({
|
||||
users: sockets.map(socket => socket.data.user),
|
||||
id: user.id,
|
||||
})
|
||||
})
|
||||
|
||||
// Disconnection cleanup
|
||||
socket.on("disconnect", () => {
|
||||
socket.to(appId).emit("user-disconnect", socket.data.user)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emitTableUpdate(ctx: any, table: Table) {
|
||||
this.io.in(ctx.appId).emit("table-update", { id: table._id, table })
|
||||
}
|
||||
|
||||
emitTableDeletion(ctx: any, id: string) {
|
||||
this.io.in(ctx.appId).emit("table-update", { id, table: null })
|
||||
}
|
||||
}
|
|
@ -1,14 +1,17 @@
|
|||
import http from "http"
|
||||
import Koa from "koa"
|
||||
import GridSocket from "./grid"
|
||||
import ClientAppSocket from "./client"
|
||||
import GridSocket from "./grid"
|
||||
import BuilderSocket from "./builder"
|
||||
|
||||
let clientAppSocket: ClientAppSocket
|
||||
let gridSocket: GridSocket
|
||||
let builderSocket: BuilderSocket
|
||||
|
||||
export const initialise = (app: Koa, server: http.Server) => {
|
||||
clientAppSocket = new ClientAppSocket(app, server)
|
||||
gridSocket = new GridSocket(app, server)
|
||||
builderSocket = new BuilderSocket(app, server)
|
||||
}
|
||||
|
||||
export { clientAppSocket, gridSocket }
|
||||
|
|
|
@ -59,13 +59,36 @@ export default class Socket {
|
|||
for (let [idx, middleware] of middlewares.entries()) {
|
||||
await middleware(ctx, () => {
|
||||
if (idx === middlewares.length - 1) {
|
||||
// Middlewares are finished.
|
||||
// Middlewares are finished
|
||||
// Extract some data from our enriched koa context to persist
|
||||
// as metadata for the socket
|
||||
socket.data.user = {
|
||||
id: ctx.user._id,
|
||||
email: ctx.user.email,
|
||||
|
||||
// Add user info, including a deterministic color and friendly
|
||||
// label
|
||||
const { _id, email, firstName, lastName } = ctx.user
|
||||
let hue = 1
|
||||
for (let i = 0; i < email.length && i < 5; i++) {
|
||||
hue *= email.charCodeAt(i + 1)
|
||||
hue /= 17
|
||||
}
|
||||
hue = hue % 360
|
||||
const color = `hsl(${hue}, 50%, 40%)`
|
||||
let label = email
|
||||
if (firstName) {
|
||||
label = firstName
|
||||
if (lastName) {
|
||||
label += ` ${lastName}`
|
||||
}
|
||||
}
|
||||
socket.data.user = {
|
||||
id: _id,
|
||||
email,
|
||||
color,
|
||||
label,
|
||||
}
|
||||
|
||||
// Add app ID to help split sockets into rooms
|
||||
socket.data.appId = ctx.appId
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue