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 { getAutomationStore } from "./store/automation"
|
||||||
import { getTemporalStore } from "./store/temporal"
|
import { getTemporalStore } from "./store/temporal"
|
||||||
import { getThemeStore } from "./store/theme"
|
import { getThemeStore } from "./store/theme"
|
||||||
|
import { getUsersStore } from "./store/users"
|
||||||
import { derived } from "svelte/store"
|
import { derived } from "svelte/store"
|
||||||
import { findComponent, findComponentPath } from "./componentUtils"
|
import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
|
@ -12,6 +13,7 @@ export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
export const themeStore = getThemeStore()
|
export const themeStore = getThemeStore()
|
||||||
export const temporalStore = getTemporalStore()
|
export const temporalStore = getTemporalStore()
|
||||||
|
export const usersStore = getUsersStore()
|
||||||
|
|
||||||
// Setup history for screens
|
// Setup history for screens
|
||||||
export const screenHistoryStore = createHistoryStore({
|
export const screenHistoryStore = createHistoryStore({
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
import { getComponentFieldOptions } from "helpers/formFields"
|
import { getComponentFieldOptions } from "helpers/formFields"
|
||||||
|
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||||
|
|
||||||
const INITIAL_FRONTEND_STATE = {
|
const INITIAL_FRONTEND_STATE = {
|
||||||
initialised: false,
|
initialised: false,
|
||||||
|
@ -84,10 +85,14 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
// Onboarding
|
// Onboarding
|
||||||
onboarding: false,
|
onboarding: false,
|
||||||
tourNodes: null,
|
tourNodes: null,
|
||||||
|
|
||||||
|
// Multi user collab
|
||||||
|
users: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||||
|
let websocket
|
||||||
|
|
||||||
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
// 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,
|
// 409s. All screen doc mutations (aside from creation) use this function,
|
||||||
|
@ -112,9 +117,11 @@ export const getFrontendStore = () => {
|
||||||
store.actions = {
|
store.actions = {
|
||||||
reset: () => {
|
reset: () => {
|
||||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||||
|
websocket?.disconnect()
|
||||||
},
|
},
|
||||||
initialise: async pkg => {
|
initialise: async pkg => {
|
||||||
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
||||||
|
websocket = createBuilderWebsocket()
|
||||||
|
|
||||||
await store.actions.components.refreshDefinitions(application.appId)
|
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 TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||||
|
import UserAvatars from "./_components/UserAvatars.svelte"
|
||||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
@ -125,10 +126,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
store.update(state => {
|
store.actions.reset()
|
||||||
state.appId = null
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -211,6 +209,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
|
<UserAvatars users={$store.users} />
|
||||||
<AppActions {application} />
|
<AppActions {application} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
||||||
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
||||||
import NewRow from "./NewRow.svelte"
|
import NewRow from "./NewRow.svelte"
|
||||||
import { createWebsocket } from "../lib/websocket"
|
import { createGridWebsocket } from "../lib/websocket"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderHeight,
|
||||||
MaxCellRenderWidthOverflow,
|
MaxCellRenderWidthOverflow,
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
export const getContext = () => context
|
export const getContext = () => context
|
||||||
|
|
||||||
// Initialise websocket for multi-user
|
// Initialise websocket for multi-user
|
||||||
onMount(() => createWebsocket(context))
|
onMount(() => createGridWebsocket(context))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -15,6 +15,6 @@
|
||||||
.users {
|
.users {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,24 +1,9 @@
|
||||||
import { get } from "svelte/store"
|
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
|
const { rows, tableId, users, userId, focusedCellId } = context
|
||||||
|
const socket = createWebsocket("/socket/grid")
|
||||||
// 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 connectToTable = tableId => {
|
const connectToTable = tableId => {
|
||||||
if (!socket.connected) {
|
if (!socket.connected) {
|
||||||
|
|
|
@ -21,35 +21,6 @@ export const createStores = () => {
|
||||||
return 0
|
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 CookieUtils from "./cookies"
|
||||||
export * as RoleUtils from "./roles"
|
export * as RoleUtils from "./roles"
|
||||||
export * as Utils from "./utils"
|
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 http from "http"
|
||||||
import Koa from "koa"
|
import Koa from "koa"
|
||||||
import GridSocket from "./grid"
|
|
||||||
import ClientAppSocket from "./client"
|
import ClientAppSocket from "./client"
|
||||||
|
import GridSocket from "./grid"
|
||||||
|
import BuilderSocket from "./builder"
|
||||||
|
|
||||||
let clientAppSocket: ClientAppSocket
|
let clientAppSocket: ClientAppSocket
|
||||||
let gridSocket: GridSocket
|
let gridSocket: GridSocket
|
||||||
|
let builderSocket: BuilderSocket
|
||||||
|
|
||||||
export const initialise = (app: Koa, server: http.Server) => {
|
export const initialise = (app: Koa, server: http.Server) => {
|
||||||
clientAppSocket = new ClientAppSocket(app, server)
|
clientAppSocket = new ClientAppSocket(app, server)
|
||||||
gridSocket = new GridSocket(app, server)
|
gridSocket = new GridSocket(app, server)
|
||||||
|
builderSocket = new BuilderSocket(app, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { clientAppSocket, gridSocket }
|
export { clientAppSocket, gridSocket }
|
||||||
|
|
|
@ -59,13 +59,36 @@ export default class Socket {
|
||||||
for (let [idx, middleware] of middlewares.entries()) {
|
for (let [idx, middleware] of middlewares.entries()) {
|
||||||
await middleware(ctx, () => {
|
await middleware(ctx, () => {
|
||||||
if (idx === middlewares.length - 1) {
|
if (idx === middlewares.length - 1) {
|
||||||
// Middlewares are finished.
|
// Middlewares are finished
|
||||||
// Extract some data from our enriched koa context to persist
|
// Extract some data from our enriched koa context to persist
|
||||||
// as metadata for the socket
|
// as metadata for the socket
|
||||||
socket.data.user = {
|
|
||||||
id: ctx.user._id,
|
// Add user info, including a deterministic color and friendly
|
||||||
email: ctx.user.email,
|
// 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()
|
next()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue