Skip to content

Commit

Permalink
Allow configuration of collaborative editing features using environme…
Browse files Browse the repository at this point in the history
…nt variable COLLABORATIVE_EDITING
  • Loading branch information
raimohanska committed Feb 26, 2024
1 parent 2e6d8e9 commit c520f32
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 21 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,12 @@ AWS_SECRET_ACCESS_KEY Secret access key
AWS_ASSETS_BUCKET_URL URL to the AWS bucket. For example https://r-board-assets.s3.eu-north-1.amazonaws.com
```

The experimental collaborative editing feature is controlled using environment variables as well:

```
COLLABORATIVE_EDITING `true` to enable for all new boards, `false` to disable for new boards, `opt-in` to allow opt-in on creation, `opt-in-authenticated` to allow opt-in for authenticated users only (default).
```

And finally some more settings you're unlikely to need.

```
Expand Down
16 changes: 15 additions & 1 deletion backend/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import path from "path"
import fs from "fs"
import { authProvider } from "./oauth"
import * as t from "io-ts"
import { optional } from "../../common/src/domain"
import { decodeOrThrow } from "./decodeOrThrow"

export type StorageBackend = Readonly<
{ type: "LOCAL"; directory: string; assetStorageURL: string } | { type: "AWS"; assetStorageURL: string }
>
export type Config = Readonly<{ storageBackend: StorageBackend; authSupported: boolean }>
export type Config = Readonly<{ storageBackend: StorageBackend; authSupported: boolean; crdt: CrdtConfigString }>

const CrdtConfigString = t.union([
t.literal("opt-in"),
t.literal("opt-in-authenticated"),
t.literal("true"),
t.literal("false"),
])
export type CrdtConfigString = t.TypeOf<typeof CrdtConfigString>

export const getConfig = (): Config => {
const storageBackend: StorageBackend = process.env.AWS_ASSETS_BUCKET_URL
Expand All @@ -18,8 +29,11 @@ export const getConfig = (): Config => {
} catch (e) {}
}

const crdt = decodeOrThrow(CrdtConfigString, process.env.COLLABORATIVE_EDITING ?? "opt-in-authenticated")

return {
storageBackend,
authSupported: authProvider !== null,
crdt,
}
}
1 change: 1 addition & 0 deletions backend/src/connection-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const connectionHandler = (socket: WsWrapper, handleMessage: MessageHandl
action: "server.config",
assetStorageURL: config.storageBackend.assetStorageURL,
authSupported: config.authSupported,
crdt: config.crdt,
}),
)
socket.onError(() => {
Expand Down
5 changes: 4 additions & 1 deletion common/src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export function optional<T extends t.Type<any>>(c: T) {
return t.union([c, t.undefined, t.null])
}

type CrdtMode = undefined | 1
export const CrdtDisabled = undefined
export const CrdtEnabled = 1 as const
export type CrdtMode = typeof CrdtDisabled | typeof CrdtEnabled

export type BoardAttributes = {
id: Id
Expand Down Expand Up @@ -210,6 +212,7 @@ export type ServerConfig = {
action: "server.config"
authSupported: boolean
assetStorageURL: string
crdt: "true" | "false" | "opt-in" | "opt-in-authenticated"
}
export type Serial = number
export type AppEvent =
Expand Down
67 changes: 49 additions & 18 deletions frontend/src/dashboard/DashboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,38 @@ import * as L from "lonna"
import * as R from "ramda"
import * as uuid from "uuid"
import {
Board,
BoardAccessPolicy,
BoardStub,
CrdtDisabled,
CrdtEnabled,
EventFromServer,
exampleBoard,
RecentBoard,
ServerConfig,
} from "../../../common/src/domain"
import { BOARD_PATH, createBoardAndNavigate, Routes } from "../board-navigation"
import { localStorageAtom } from "../board/local-storage-atom"
import { IS_TOUCHSCREEN } from "../board/touchScreen"
import { BoardAccessPolicyEditor } from "../components/BoardAccessPolicyEditor"
import { BoardCrdtModeSelector } from "../components/BoardCrdtModeSelector"
import { TextInput } from "../components/components"
import { signIn, signOut } from "../google-auth"
import { Dispatch } from "../store/board-store"
import { RecentBoards } from "../store/recent-boards"
import { canLogin, defaultAccessPolicy, UserSessionState } from "../store/user-session-store"
import { BoardCrdtModeSelector } from "../components/BoardCrdtModeSelector"

export const DashboardView = ({
sessionState,
dispatch,
recentBoards,
eventsFromServer,
serverConfig,
}: {
sessionState: L.Property<UserSessionState>
recentBoards: RecentBoards
dispatch: Dispatch
eventsFromServer: L.EventStream<EventFromServer>
serverConfig: L.Property<ServerConfig | null>
}) => {
const boardName = L.atom("")
return (
Expand Down Expand Up @@ -66,7 +70,9 @@ export const DashboardView = ({
})}
</div>
<main>
<CreateBoard {...{ dispatch, sessionState, boardName, recentBoards, eventsFromServer }} />
<CreateBoard
{...{ dispatch, sessionState, boardName, recentBoards, eventsFromServer, serverConfig }}
/>
<div>
<div className="user-content">
<RecentBoardsView {...{ recentBoards, boardName }} />
Expand Down Expand Up @@ -264,23 +270,33 @@ const CreateBoardOptions = ({
accessPolicy,
sessionState,
useCollaborativeEditing,
serverConfig,
}: {
accessPolicy: L.Atom<BoardAccessPolicy | undefined>
sessionState: L.Property<UserSessionState>
useCollaborativeEditing: L.Atom<boolean>
serverConfig: L.Property<ServerConfig | null>
}) => {
return L.view(sessionState, (s) =>
s.status === "logged-in" ? (
<>
<BoardCrdtModeSelector {...{ useCollaborativeEditing }} />
<BoardAccessPolicyEditor {...{ accessPolicy, user: s }} />
</>
) : (
<small className="anonymousBoardDisclaimer">
Anonymously created boards are accessible to anyone with a link. You may <a onClick={signIn}>sign in</a>{" "}
first in order to restrict access to your new board.
</small>
),
const optInCollaborative = L.view(
sessionState,
serverConfig,
(s, c) => c?.crdt === "opt-in" || (c?.crdt === "opt-in-authenticated" && s.status === "logged-in"),
)

return (
<>
{L.view(optInCollaborative, (optIn) => optIn && <BoardCrdtModeSelector {...{ useCollaborativeEditing }} />)}
{L.view(sessionState, (s) =>
s.status === "logged-in" ? (
<BoardAccessPolicyEditor {...{ accessPolicy, user: s }} />
) : (
<small className="anonymousBoardDisclaimer">
Anonymously created boards are accessible to anyone with a link. You may{" "}
<a onClick={signIn}>sign in</a> first in order to restrict access to your new board.
</small>
),
)}
</>
)
}

Expand All @@ -290,12 +306,14 @@ const CreateBoard = ({
boardName,
recentBoards,
eventsFromServer,
serverConfig,
}: {
dispatch: Dispatch
sessionState: L.Property<UserSessionState>
boardName: L.Atom<string>
recentBoards: RecentBoards
eventsFromServer: L.EventStream<EventFromServer>
serverConfig: L.Property<ServerConfig | null>
}) => {
const disabled = L.view(boardName, (n) => !n)
const navigator = getNavigator<Routes>()
Expand All @@ -304,15 +322,18 @@ const CreateBoard = ({
accessPolicy.set(defaultAccessPolicy(s, false))
})
const hasRecentBoards = L.view(recentBoards.recentboards, (bs) => bs.length > 0)
const useCollaborativeEditing = L.atom(false)
const useCollaborativeEditingAtom = L.atom(false)
const useCollaborativeEditing = L.view(serverConfig, useCollaborativeEditingAtom, (c, u) =>
c?.crdt === "true" ? true : c?.crdt === "false" ? false : u,
)

function onSubmit(e: JSX.FormEvent) {
e.preventDefault()
const newBoard: BoardStub = {
name: boardName.get(),
id: uuid.v4(),
accessPolicy: accessPolicy.get(),
crdt: useCollaborativeEditing.get() ? 1 : undefined,
crdt: useCollaborativeEditing.get() ? CrdtEnabled : CrdtDisabled,
}
createBoardAndNavigate(newBoard, dispatch, navigator, eventsFromServer)
}
Expand All @@ -328,7 +349,17 @@ const CreateBoard = ({
</div>
{L.view(
disabled,
(d) => !d && <CreateBoardOptions {...{ accessPolicy, sessionState, useCollaborativeEditing }} />,
(d) =>
!d && (
<CreateBoardOptions
{...{
accessPolicy,
sessionState,
useCollaborativeEditing: useCollaborativeEditingAtom,
serverConfig,
}}
/>
),
)}
</form>
)
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as H from "harmaja"
import { h } from "harmaja"
import _ from "lodash"
import * as L from "lonna"
import { RecentBoardAttributes } from "../../common/src/domain"
import { EventFromServer, RecentBoardAttributes, ServerConfig } from "../../common/src/domain"
import "./app.scss"
import { BoardNavigation } from "./board-navigation"
import { BoardView } from "./board/BoardView"
Expand All @@ -26,6 +26,11 @@ const App = () => {
const assets = assetStore(connection, L.view(boardStore.state, "board"), boardStore.events)
const title = L.view(boardStore.state, (s) => (s.board && s.board.name ? `${s.board.name} - OurBoard` : "OurBoard"))
title.forEach((t) => (document.querySelector("title")!.textContent = t))
const serverConfig = connection.bufferedServerEvents
.pipe(
L.scan<EventFromServer, ServerConfig | null>(null, (c, e) => (e.action === "server.config" ? e : c)),
)
.applyScope(H.componentScope())

boardStore.state
.pipe(
Expand Down Expand Up @@ -67,6 +72,7 @@ const App = () => {
sessionState: sessionStore.sessionState,
recentBoards,
eventsFromServer: connection.bufferedServerEvents,
serverConfig,
}}
/>
)
Expand Down

0 comments on commit c520f32

Please sign in to comment.