feat: add multiplayer support for webeditor (#1562)

This commit is contained in:
Ruben Fiszel
2023-05-30 08:42:27 -07:00
committed by GitHub
parent 1634ee635e
commit 428e0ab2f8
15 changed files with 1222 additions and 200 deletions

73
.github/workflows/build_ws.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
env:
REGISTRY: ghcr.io
ECR_REGISTRY: 976079455550.dkr.ecr.us-east-1.amazonaws.com
IMAGE_NAME: ${{ github.repository }}-multiplayer
name: Publish websocket multiplayer server
on:
workflow_dispatch:
jobs:
publish_multiplayer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: depot/setup-action@v1
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push publicly
uses: depot/build-push-action@v1
with:
context: .
file: ./docker/DockerfileMultiplayer
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ steps.meta.outputs.tags }}
labels: |
${{ steps.meta.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
publish_privately:
needs: [publish_multiplayer]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ECR
uses: docker/login-action@v2
with:
registry: ${{ env.ECR_REGISTRY }}
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Push image to ECR
run: |
docker buildx imagetools create \
--tag ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

View File

@@ -94,7 +94,7 @@ jobs:
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Push image to GHCR
- name: Push image to ECR
run: |
docker buildx imagetools create \
--tag ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \

View File

@@ -1,5 +1,6 @@
{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://lsp:3001
# reverse_proxy /ws_mp/* http://multiplayer:3002
reverse_proxy /* http://windmill_server:8000
}

View File

@@ -18,6 +18,7 @@ services:
retries: 5
windmill_server:
# Use ghcr.io/windmill-labs/windmill-ee:main for the ee
image: ghcr.io/windmill-labs/windmill:main
deploy:
replicas: 1
@@ -39,6 +40,7 @@ services:
# - ./oauth.json/:/usr/src/app/oauth.json
windmill_worker:
# Use ghcr.io/windmill-labs/windmill-ee:main for the ee
image: ghcr.io/windmill-labs/windmill:main
deploy:
replicas: 3
@@ -51,13 +53,11 @@ services:
- NUM_WORKERS=1
- DISABLE_SERVER=true
- KEEP_JOB_DIR=false
- DENO_PATH=/usr/bin/deno
- PYTHON_PATH=/usr/local/bin/python3
- METRICS_ADDR=false
depends_on:
db:
condition: service_healthy
# to mount the worker folder to debug,, KEEP_JOB_DIR=true and mount /tmp/windmill
# to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
- worker_dependency_cache:/tmp/windmill/cache
# mount the docker socket to allow to run docker containers from within the workers
@@ -69,6 +69,13 @@ services:
expose:
- 3001
# Uncomment to enable multiplayer on the enterprise edition
# multiplayer:
# image: ghcr.io/windmill-labs/windmill-multiplayer:latest
# restart: unless-stopped
# expose:
# - 3002
caddy:
image: caddy:2.5.2-alpine
restart: unless-stopped

View File

@@ -0,0 +1,8 @@
FROM node:slim
EXPOSE 3002
ENV PORT 3002
RUN npm install -g y-websocket
CMD ["npx", "y-websocket"]

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,10 @@
"svelte-select": "^5.6.1",
"svelte-timezone-picker": "^2.0.3",
"tailwind-merge": "^1.12.0",
"vscode-ws-jsonrpc": "3.0.0"
"vscode-ws-jsonrpc": "3.0.0",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.0",
"yjs": "^13.6.1"
},
"peerDependencies": {
"@sveltejs/kit": "^1.18.0",

View File

@@ -58,40 +58,45 @@
let seeEditable: boolean = enum_ != undefined || pattern != undefined
const dispatch = createEventDispatcher()
$: validateInput(pattern, value)
let error: string = ''
let el: HTMLTextAreaElement | undefined = undefined
export let editor: SimpleEditor | undefined = undefined
let inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
let rawValue: string | undefined = undefined
$: {
if (rawValue) {
try {
value = JSON.parse(rawValue)
error = ''
} catch (err) {
error = err.toString()
function computeDefaultValue(nvalue?: any, inputCat?: string, defaultValue?: any) {
if (value == undefined || value == null) {
value = defaultValue
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
value = ''
} else if (inputCat == 'enum') {
value = enum_?.[0]
} else if (inputCat == 'boolean') {
value = false
} else if (inputCat == 'list') {
value = []
}
} else if (inputCat === 'object') {
evalValueToRaw()
}
}
}
$: {
error = ''
if (inputCat === 'object') {
evalValueToRaw()
validateInput(pattern, value)
}
computeDefaultValue()
$: computeDefaultValue(value, inputCat, defaultValue)
function evalValueToRaw() {
rawValue = inputCat === 'object' ? JSON.stringify(value, null, 2) : undefined
}
export function evalValueToRaw() {
if (value) {
rawValue = JSON.stringify(value, null, 2)
}
}
evalValueToRaw()
function fileChanged(e: any, cb: (v: string | undefined) => void) {
let t = e.target
@@ -144,29 +149,14 @@
}
e.stopPropagation()
}
$: {
if (value == undefined || value == null) {
value = defaultValue
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
value = ''
} else if (inputCat == 'enum') {
value = enum_?.[0]
} else if (inputCat == 'boolean') {
value = false
} else if (inputCat == 'list') {
value = []
}
}
}
}
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
let redraw = 0
let itemsLimit = 50
let customValue = false
$: validateInput(pattern, value)
</script>
<!-- svelte-ignore a11y-autofocus -->

View File

@@ -23,6 +23,7 @@
import { MonacoLanguageClient, initServices } from 'monaco-languageclient'
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'
import { CloseAction, ErrorAction, RequestType } from 'vscode-languageclient'
import { MonacoBinding } from 'y-monaco'
languages.typescript.typescriptDefaults.setModeConfiguration({
completionItems: false,
@@ -59,6 +60,7 @@
import { buildWorkerDefinition } from './build_workers'
import { workspaceStore } from '$lib/stores'
import { UserService } from '$lib/gen'
import type { Text } from 'yjs'
let divEl: HTMLDivElement | null = null
let editor: meditor.IStandaloneCodeEditor
@@ -79,6 +81,8 @@
export let shouldBindKey: boolean = true
export let fixedOverflowWidgets = true
export let path: string = randomHash()
export let yContent: Text | undefined = undefined
export let awareness: any | undefined = undefined
if (path == '' || path == undefined || path.startsWith('/')) {
path = randomHash()
@@ -570,8 +574,16 @@
}
let widgets: HTMLElement | undefined = document.getElementById('monaco-widgets-root') ?? undefined
let model: meditor.ITextModel
let monacoBinding: MonacoBinding | undefined = undefined
// @ts-ignore
$: if (yContent && awareness && model && editor) {
monacoBinding && monacoBinding.destroy()
monacoBinding = new MonacoBinding(yContent, model, new Set([editor]), awareness)
}
async function loadMonaco() {
let model: meditor.ITextModel
try {
model = meditor.createModel(code, lang, mUri.parse(uri))
} catch (err) {
@@ -633,6 +645,7 @@
reloadWebsocket()
return () => {
console.log('disposing editor')
try {
closeWebsockets()
model?.dispose()
@@ -676,8 +689,27 @@
<div bind:this={divEl} class="{$$props.class} editor" />
<style lang="postcss">
<style global lang="postcss">
.editor {
@apply p-0 border rounded-md border-gray-50;
}
.yRemoteSelection {
background-color: rgb(250, 129, 0, 0.5);
}
.yRemoteSelectionHead {
position: absolute;
border-left: orange solid 2px;
border-top: orange solid 2px;
border-bottom: orange solid 2px;
height: 100%;
box-sizing: border-box;
}
.yRemoteSelectionHead::after {
position: absolute;
content: ' ';
border: 3px solid orange;
border-radius: 4px;
left: -4px;
top: -5px;
}
</style>

View File

@@ -27,10 +27,12 @@
import PickHubScript from './flows/pickers/PickHubScript.svelte'
import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Popover from './Popover.svelte'
import { SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS } from '$lib/consts'
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
import { getScriptByPath } from '$lib/scripts'
import Toggle from './Toggle.svelte'
import { Link, Users } from 'lucide-svelte'
export let lang: 'python3' | 'deno' | 'go' | 'bash'
export let editor: Editor | undefined
@@ -45,6 +47,9 @@
export let iconOnly: boolean = false
export let validCode: boolean = true
export let kind: 'script' | 'trigger' | 'approval' = 'script'
export let collabMode = false
export let collabLive = false
export let collabUsers: { name: string }[] = []
let contextualVariablePicker: ItemPicker
let variablePicker: ItemPicker
@@ -88,6 +93,8 @@
}
let version = __pkg__.version
const dispatch = createEventDispatcher()
</script>
<Drawer bind:this={scriptPicker} size="900px">
@@ -252,120 +259,117 @@
{validCode ? 'Valid' : 'Invalid'}
</Badge>
<div class="flex items-center divide-x">
<Popover
notClickable
placement="bottom"
disappearTimeout={0}
class="pr-1"
disablePopup={!iconOnly}
<Button
title="Add context variable"
color="light"
btnClasses="!font-medium !h-full"
on:click={contextualVariablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
<Button
color="light"
btnClasses="!font-medium !h-full"
on:click={contextualVariablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Context Var
</Button>
<svelte:fragment slot="text">Add context variable</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disappearTimeout={0}
class="px-1"
disablePopup={!iconOnly}
+Context Var
</Button>
<Button
title="Add variable"
color="light"
btnClasses="!font-medium !h-full"
on:click={variablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
<Button
color="light"
btnClasses="!font-medium !h-full"
on:click={variablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Variable
</Button>
<svelte:fragment slot="text">Add variable</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disappearTimeout={0}
class="px-1"
disablePopup={!iconOnly}
+Variable
</Button>
<Button
title="Add resource"
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={resourcePicker.openDrawer}
{iconOnly}
startIcon={{ icon: faCube }}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={resourcePicker.openDrawer}
{iconOnly}
startIcon={{ icon: faCube }}
>
+Resource
</Button>
<svelte:fragment slot="text">Add resource</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disappearTimeout={0}
class="px-1"
disablePopup={!iconOnly}
+Resource
</Button>
<Button
title="Reset Content"
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor?.clearContent}
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor?.clearContent}
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
Reset
</Button>
<svelte:fragment slot="text">Reset</svelte:fragment>
</Popover>
<Popover
notClickable
placement="bottom"
disappearTimeout={0}
class="px-1"
disablePopup={!iconOnly}
Reset
</Button>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor?.reloadWebsocket}
startIcon={{ icon: faRotate }}
title="Reload assistants"
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor?.reloadWebsocket}
startIcon={{ icon: faRotate }}
>
{#if !iconOnly}
Assistant
{#if !iconOnly}
Assistants
{/if}
<span class="ml-1 -my-1">
{#if lang == 'deno'}
(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
{:else if lang == 'go'}
(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
{:else if lang == 'python3'}
(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
<span class={websocketAlive.black ? 'green' : 'text-red-700'}>Black</span>
<span class={websocketAlive.ruff ? 'green' : 'text-red-700'}>Ruff</span>)
{:else if lang == 'bash'}
(<span class={websocketAlive.shellcheck ? 'green' : 'text-red-700'}>Shellcheck</span>)
{/if}
<span class="ml-1 -my-1">
{#if lang == 'deno'}
(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
{:else if lang == 'go'}
(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
{:else if lang == 'python3'}
(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
<span class={websocketAlive.black ? 'green' : 'text-red-700'}>Black</span>
<span class={websocketAlive.ruff ? 'green' : 'text-red-700'}>Ruff</span>)
{:else if lang == 'bash'}
(<span class={websocketAlive.shellcheck ? 'green' : 'text-red-700'}>Shellcheck</span>)
{/if}
</span>
</Button>
<svelte:fragment slot="text">Reload assistant</svelte:fragment>
</Popover>
</span>
</Button>
{#if collabMode}
<div class="flex items-center px-1">
<Toggle
options={{ right: iconOnly ? '' : 'Multiplayer' }}
size="xs"
checked={collabLive}
on:change={() => dispatch('toggleCollabMode')}
/>
{#if iconOnly}
<Users class="ml-1" size={12} />
{/if}
{#if collabLive}
<button
title="Show invite link"
class="p-1 rounded hover:bg-gray-400 mx-1 border"
on:click={() => dispatch('collabPopup')}><Link size={12} /></button
>
<div class="isolate flex -space-x-2 overflow-hidden pl-2">
{#each collabUsers as user}
<span
class="inline-flex h-6 w-6 items-center justify-center rounded-full ring-2 ring-white bg-gray-600"
title={user.name}
>
<span class="text-sm font-medium leading-none text-white"
>{user.name.substring(0, 2).toLocaleUpperCase()}</span
>
</span>
{/each}
</div>
{/if}
</div>
{/if}
<!-- <Popover
notClickable
placement="bottom"
@@ -392,28 +396,21 @@
</Popover> -->
</div>
</div>
<Popover
notClickable
placement="bottom"
disappearTimeout={0}
class="px-1"
disablePopup={!iconOnly}
>
{#if SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS}
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: faEye }}
>
Explore other scripts
</Button>
{/if}
<svelte:fragment slot="text">Script</svelte:fragment>
</Popover>
{#if SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS}
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: faEye }}
title="Explore other scripts"
>
Explore other scripts
</Button>
{/if}
</div>
<style lang="postcss">

View File

@@ -38,7 +38,10 @@
export let showMeta: boolean = false
let metadataOpen =
showMeta || (initialPath == '' && $page.url.searchParams.get('state') == undefined)
showMeta ||
(initialPath == '' &&
$page.url.searchParams.get('state') == undefined &&
$page.url.searchParams.get('collab') == undefined)
let advancedOpen = false
let editor: Editor | undefined = undefined
@@ -100,7 +103,14 @@
let loadingSave = false
let loadingDraft = false
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
$: {
;['collab', 'path'].forEach((x) => {
if ($page.url.searchParams.get(x)) {
$page.url.searchParams.delete(x)
}
})
setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
}
if (script.content == '') {
initContent(script.language, script.kind, template)
@@ -111,6 +121,7 @@
kind: Script.kind | undefined,
template: 'pgsql' | 'mysql' | 'script' | 'docker'
) {
scriptEditor?.disableCollaboration()
script.content = initialCode(language, kind, template)
scriptEditor?.inferSchema(script.content, language)
if (script.content != editor?.getCode()) {
@@ -528,6 +539,8 @@
</DrawerContent>
</Drawer>
<ScriptEditor
collabMode
edit={initialPath != ''}
on:format={() => {
saveDraft()
}}

View File

@@ -1,9 +1,11 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import type { Schema } from '$lib/common'
import { CompletedJob, Job, JobService } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { emptySchema, getModifierKey } from '$lib/utils'
import { faPlay } from '@fortawesome/free-solid-svg-icons'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { copyToClipboard, emptySchema, getModifierKey, sendUserToast } from '$lib/utils'
import { faClipboard, faPlay } from '@fortawesome/free-solid-svg-icons'
import Editor from './Editor.svelte'
import { inferArgs } from '$lib/infer'
import type { Preview } from '$lib/gen/models/Preview'
@@ -13,11 +15,15 @@
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import EditorBar, { EDITOR_BAR_WIDTH_THRESHOLD } from './EditorBar.svelte'
import TestJobLoader from './TestJobLoader.svelte'
import { createEventDispatcher, onMount } from 'svelte'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { Button, Kbd } from './common'
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
import WindmillIcon from './icons/WindmillIcon.svelte'
import * as Y from 'yjs'
import { scriptLangToEditorLang } from '$lib/scripts'
import { WebsocketProvider } from 'y-websocket'
import Modal from './common/modal/Modal.svelte'
import { Icon } from 'svelte-awesome'
// Exported
export let schema: Schema | any = emptySchema()
@@ -30,6 +36,8 @@
export let fixedOverflowWidgets = true
export let noSyncFromGithub = false
export let editor: Editor | undefined = undefined
export let collabMode = false
export let edit = true
let websocketAlive = {
pyright: false,
@@ -54,6 +62,21 @@
let pastPreviews: CompletedJob[] = []
let validCode = true
let wsProvider: WebsocketProvider | undefined = undefined
let yContent: Y.Text | undefined = undefined
let peers: { name: string }[] = []
let showCollabPopup = false
const url = new URL(window.location.toString())
let initialCollab = /true|1/i.test(url.searchParams.get('collab') ?? '0')
if (initialCollab) {
setCollaborationMode()
url.searchParams.delete('collab')
url.searchParams.delete('path')
history.replaceState(null, '', url)
}
function onKeyDown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
event.preventDefault()
@@ -107,11 +130,89 @@
loadPastTests()
})
export function setCollaborationMode() {
if (!$enterpriseLicense) {
sendUserToast(`Multiplayer is an enterprise feature`, true, [
{
label: 'Upgrade',
callback: () => {
window.open('https://www.windmill.dev/pricing', '_blank')
}
}
])
return
}
const ydoc = new Y.Doc()
if (wsProvider) {
wsProvider.destroy()
}
let yContentInit = ydoc.getText('content')
const wsProtocol = BROWSER && window.location.protocol == 'https:' ? 'wss' : 'ws'
console.log(window.location.host)
wsProvider = new WebsocketProvider(
`${wsProtocol}://${window.location.host}/ws_mp/`,
$workspaceStore + '/' + path ?? 'no-room-name',
ydoc,
{ connect: false }
)
wsProvider.on('sync', (isSynced: boolean) => {
if (isSynced && yContentInit?.toJSON() == '') {
showCollabPopup = true
yContentInit?.insert(0, code)
}
yContent = yContentInit
})
wsProvider.on('connection-error', (WSErrorEvent) => {
console.error(WSErrorEvent)
sendUserToast('Multiplayer server connection had an error', true)
})
wsProvider.connect()
const awareness = wsProvider.awareness
awareness.setLocalStateField('user', {
name: $userStore?.username
})
function setPeers() {
peers = Array.from(awareness.getStates().values()).map((x) => x.user)
}
setPeers()
// You can observe when a user updates their awareness information
awareness.on('change', (changes) => {
setPeers()
})
}
export function disableCollaboration() {
if (!wsProvider?.shouldConnect) return
peers = []
console.log('collab mode disabled')
wsProvider?.disconnect()
wsProvider.destroy()
wsProvider = undefined
}
onDestroy(() => {
disableCollaboration()
})
const dispatch = createEventDispatcher()
function asKind(str: string | undefined) {
return str as 'script' | 'approval' | 'trigger' | undefined
}
function collabUrl() {
let url = new URL(window.location.toString())
url.search = ''
return `${url}?collab=1` + (edit ? '' : `&path=${path}`)
}
</script>
<TestJobLoader
@@ -123,14 +224,34 @@
<svelte:window on:keydown={onKeyDown} />
<Modal title="Invite others" bind:open={showCollabPopup}>
<div>Have others join by sharing the following url:</div>
<div class="flex gap-2 pr-4">
<input type="text" disabled value={collabUrl()} />
<button on:click={() => copyToClipboard(collabUrl())} class="text-gray-700 ml-2">
<Icon data={faClipboard} />
</button>
</div>
</Modal>
<div class="border-b-2 shadow-sm px-1 pr-4" bind:clientWidth={width}>
<div class="flex justify-between space-x-2">
<EditorBar
on:toggleCollabMode={() => {
if (wsProvider?.shouldConnect) {
disableCollaboration()
} else {
setCollaborationMode()
}
}}
collabLive={wsProvider?.shouldConnect}
{collabMode}
{validCode}
iconOnly={width < EDITOR_BAR_WIDTH_THRESHOLD}
on:collabPopup={() => (showCollabPopup = true)}
{editor}
{lang}
{websocketAlive}
collabUsers={peers}
kind={asKind(kind)}
/>
{#if !noSyncFromGithub}
@@ -161,6 +282,8 @@
bind:code
bind:websocketAlive
bind:this={editor}
{yContent}
awareness={wsProvider?.awareness}
on:change={(e) => {
inferSchema(e.detail)
}}
@@ -202,7 +325,9 @@
{:else}
<Button
color="dark"
on:click={runTest}
on:click={() => {
runTest()
}}
btnClasses="w-full"
size="xs"
startIcon={{

View File

@@ -24,6 +24,7 @@
case 'Escape':
event.stopPropagation()
event.preventDefault()
open = false
dispatch('canceled')
break
}
@@ -37,7 +38,9 @@
<svelte:window on:keydown={onKeyDown} />
{#if open}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click={() => (open = false)}
transition:fadeFast|local
class={'absolute top-0 bottom-0 left-0 right-0 z-50'}
role="dialog"
@@ -52,6 +55,7 @@
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div
on:click|stopPropagation
class={twMerge(
'relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
c,
@@ -72,8 +76,15 @@
</div>
</div>
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
<Button on:click={() => dispatch('canceled')} color="light" size="sm">
<span class="gap-2">Cancel <Badge color="dark-gray">Escape</Badge></span>
<Button
on:click={() => {
dispatch('canceled')
open = false
}}
color="light"
size="sm"
>
<span class="inline-flex gap-2">Cancel <Badge color="dark-gray">Escape</Badge></span>
</Button>
</div>
</div>

View File

@@ -15,16 +15,18 @@
const hubPath = $page.url.searchParams.get('hub')
const showMeta = /true|1/i.test($page.url.searchParams.get('show_meta') ?? '0')
const path = $page.url.searchParams.get('path')
const initialState = $page.url.searchParams.get('state')
let scriptBuilder: ScriptBuilder | undefined = undefined
let script: NewScript =
initialState != undefined
!path && initialState != undefined
? decodeState(initialState)
: {
hash: '',
path: '',
path: path ?? '',
summary: '',
content: '',
schema: schema,

View File

@@ -16,11 +16,15 @@ const config = {
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
// Proxying websockets or socket.io: ws://localhost:5173/socket.io -> ws://localhost:5174/socket.io
'^/ws/.*': {
target: process.env.REMOTE_LSP ?? 'https://app.windmill.dev',
changeOrigin: true,
ws: true
},
'^/ws_mp/.*': {
target: process.env.REMOTE_MP ?? 'https://app.windmill.dev',
changeOrigin: true,
ws: true
}
}
},