mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-22 08:02:19 +00:00
feat: add app presence
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { awarenessStore, userStore } from '$lib/stores'
|
||||
import { BROWSER } from 'esm-env'
|
||||
|
||||
let url = BROWSER ? window.location.pathname : ''
|
||||
</script>
|
||||
|
||||
<div class="isolate flex -space-x-1 overflow-hidden w-20">
|
||||
{#each Object.entries($awarenessStore ?? {}).filter(([a, b]) => b == url && a != $userStore?.username) 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}
|
||||
>
|
||||
<span class="text-sm font-medium leading-none text-white"
|
||||
>{user.substring(0, 2).toLocaleUpperCase()}</span
|
||||
>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -210,7 +210,9 @@
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('initServices failed', e.message)
|
||||
return
|
||||
}
|
||||
|
||||
function createLanguageClient(
|
||||
transports: MessageTransports,
|
||||
name: string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, ScheduleService, type Flow, type FlowModule, DraftService } from '$lib/gen'
|
||||
import { initHistory, redo, undo } from '$lib/history'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import { encodeState, formatCron } from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import { Pen } from 'lucide-svelte'
|
||||
import { loadHubScripts } from '$lib/scripts'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Awareness from './Awareness.svelte'
|
||||
|
||||
export let initialPath: string = ''
|
||||
export let selectedId: string | undefined
|
||||
@@ -400,6 +401,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-2">
|
||||
{#if $enterpriseLicense && initialPath != ''}
|
||||
<Awareness />
|
||||
{/if}
|
||||
|
||||
<FlowImportExportMenu />
|
||||
|
||||
<FlowPreviewButtons />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { page } from '$app/stores'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { userStore, workerTags, workspaceStore } from '$lib/stores'
|
||||
import { enterpriseLicense, userStore, workerTags, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, encodeState, getModifierKey, setQueryWithoutLoad } from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
import ScriptEditor from './ScriptEditor.svelte'
|
||||
@@ -28,6 +28,7 @@
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Awareness from './Awareness.svelte'
|
||||
|
||||
export let script: NewScript
|
||||
export let initialPath: string = ''
|
||||
@@ -452,6 +453,10 @@
|
||||
<LanguageIcon lang={script.language} />
|
||||
</button>
|
||||
</div>
|
||||
{#if $enterpriseLicense && initialPath != ''}
|
||||
<Awareness />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-x-4">
|
||||
<Button
|
||||
color="light"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { AppService, DraftService, Job, Policy } from '$lib/gen'
|
||||
import { redo, undo } from '$lib/history'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
faClipboard,
|
||||
faFileExport,
|
||||
@@ -59,6 +59,7 @@
|
||||
import { Sha256 } from '@aws-crypto/sha256-js'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import DeploymentHistory from './DeploymentHistory.svelte'
|
||||
import Awareness from '$lib/components/Awareness.svelte'
|
||||
|
||||
async function hash(message) {
|
||||
try {
|
||||
@@ -209,7 +210,7 @@
|
||||
await computeTriggerables()
|
||||
await AppService.updateApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path,
|
||||
path,
|
||||
requestBody: {
|
||||
value: $app!,
|
||||
summary: $summary,
|
||||
@@ -702,6 +703,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $enterpriseLicense && appPath != ''}
|
||||
<Awareness />
|
||||
{/if}
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense, userStore, workspaceStore, awarenessStore } from '$lib/stores'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { Users } from 'lucide-svelte'
|
||||
import { BROWSER } from 'esm-env'
|
||||
|
||||
import Menu from '../common/menu/Menu.svelte'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import * as Y from 'yjs'
|
||||
import type { Awareness } from 'y-protocols/awareness'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
const wsProtocol = BROWSER && window.location.protocol == 'https:' ? 'wss' : 'ws'
|
||||
|
||||
let awareness: Awareness | undefined = undefined
|
||||
let wsProvider: WebsocketProvider | undefined = undefined
|
||||
|
||||
function connectWorkspace(workspace: string) {
|
||||
const ydoc = new Y.Doc()
|
||||
wsProvider = new WebsocketProvider(
|
||||
`${wsProtocol}://${window.location.host}/ws_mp/`,
|
||||
workspace,
|
||||
ydoc
|
||||
)
|
||||
awareness = wsProvider.awareness
|
||||
|
||||
awareness?.setLocalState({
|
||||
name: $userStore?.username,
|
||||
url: $page.url.pathname
|
||||
})
|
||||
|
||||
function setPeers() {
|
||||
if (!awareness) return
|
||||
$awarenessStore = Object.fromEntries(
|
||||
Array.from(awareness.getStates().values()).map((x) => [x.name, x.url])
|
||||
)
|
||||
}
|
||||
|
||||
setPeers()
|
||||
// You can observe when a user updates their awareness information
|
||||
awareness.on('change', (changes) => {
|
||||
setPeers()
|
||||
})
|
||||
}
|
||||
$: awareness?.setLocalState({
|
||||
name: $userStore?.username,
|
||||
url: $page.url.pathname
|
||||
})
|
||||
$: $enterpriseLicense && $workspaceStore && connectWorkspace($workspaceStore)
|
||||
|
||||
function showActivity(url: string) {
|
||||
if (url.startsWith('/scripts/add')) {
|
||||
return 'Creating a script'
|
||||
} else if (url.startsWith('/scripts/edit')) {
|
||||
return 'Editing a script'
|
||||
} else if (url.startsWith('/scripts/get')) {
|
||||
return 'Viewing a script'
|
||||
} else if (url.startsWith('/flows/add')) {
|
||||
return 'Creating a flow'
|
||||
} else if (url.startsWith('/flows/edit')) {
|
||||
return 'Editing a flow'
|
||||
} else if (url.startsWith('/flows/get')) {
|
||||
return 'Viewing a flow'
|
||||
} else if (url.startsWith('/apps/add')) {
|
||||
return 'Creating an app'
|
||||
} else if (url.startsWith('/apps/edit')) {
|
||||
return 'Editing an app'
|
||||
} else if (url.startsWith('/runs')) {
|
||||
return 'Exploring runs'
|
||||
} else if (url.startsWith('/variables')) {
|
||||
return 'Exploring variables'
|
||||
} else if (url.startsWith('/resources')) {
|
||||
return 'Exploring runs'
|
||||
} else if (url == '/') {
|
||||
return 'On the home page'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu placement="bottom-start">
|
||||
<button
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class={classNames(
|
||||
'group w-full flex items-center text-white hover:bg-gray-50 hover:text-gray-900 focus:ring-4 focus:outline-none focus:ring-gray-300 px-2 py-2 text-sm font-medium rounded-md h-8 '
|
||||
)}
|
||||
>
|
||||
<div class="center-center mr-2">
|
||||
<Users size={16} />
|
||||
</div>
|
||||
|
||||
{#if !isCollapsed}
|
||||
<span class={classNames('whitespace-pre truncate')}>Live</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="divide-y divide-gray-100" role="none">
|
||||
<div class="py-1 flex flex-col gap-y-1">
|
||||
{#each Object.entries($awarenessStore ?? {}) as [user, url]}
|
||||
<div class="inline-flex gap-2 px-2 items-center">
|
||||
<span
|
||||
class="inline-flex h-6 w-6 px-1 items-center justify-center rounded-full ring-2 ring-white bg-gray-600"
|
||||
title={user}
|
||||
>
|
||||
<span class="text-sm font-medium leading-none text-white"
|
||||
>{user.substring(0, 2).toLocaleUpperCase()}</span
|
||||
>
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-gray-900 truncate">{user}</span>
|
||||
<span class="text-xs text-gray-500 truncate">{showActivity(url)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
@@ -18,6 +18,7 @@ export interface UserExt {
|
||||
|
||||
const persistedWorkspace = BROWSER && localStorage.getItem('workspace')
|
||||
|
||||
export const awarenessStore = writable<Record<string, string>>(undefined)
|
||||
export const enterpriseLicense = writable<string | undefined>(undefined)
|
||||
export const workerTags = writable<string[] | undefined>(undefined)
|
||||
export const usageStore = writable<number>(0)
|
||||
|
||||
@@ -16,7 +16,14 @@
|
||||
|
||||
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
|
||||
import SidebarContent from '$lib/components/sidebar/SidebarContent.svelte'
|
||||
import { starStore, superadmin, usageStore, userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
enterpriseLicense,
|
||||
starStore,
|
||||
superadmin,
|
||||
usageStore,
|
||||
userStore,
|
||||
workspaceStore
|
||||
} from '$lib/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation'
|
||||
import UserSettings from '$lib/components/UserSettings.svelte'
|
||||
@@ -26,6 +33,7 @@
|
||||
import FavoriteMenu from '$lib/components/sidebar/FavoriteMenu.svelte'
|
||||
import { SUPERADMIN_SETTINGS_HASH, USER_SETTINGS_HASH } from '$lib/components/sidebar/settings'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import MultiplayerMenu from '$lib/components/sidebar/MultiplayerMenu.svelte'
|
||||
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
let menuOpen = false
|
||||
@@ -215,6 +223,9 @@
|
||||
<WorkspaceMenu />
|
||||
<UserMenu />
|
||||
<FavoriteMenu {favoriteLinks} />
|
||||
{#if $enterpriseLicense}
|
||||
<MultiplayerMenu />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<SidebarContent {isCollapsed} />
|
||||
@@ -251,6 +262,9 @@
|
||||
<WorkspaceMenu {isCollapsed} />
|
||||
<UserMenu {isCollapsed} />
|
||||
<FavoriteMenu {favoriteLinks} />
|
||||
{#if $enterpriseLicense}
|
||||
<MultiplayerMenu />
|
||||
{/if}
|
||||
</div>
|
||||
<SidebarContent {isCollapsed} />
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
workspaces = $userWorkspaces
|
||||
}
|
||||
}
|
||||
$: list_all_as_super_admin != undefined && handleListWorkspaces()
|
||||
$: list_all_as_super_admin != undefined && $userWorkspaces && handleListWorkspaces()
|
||||
|
||||
$: adminsInstance = workspaces?.find((x) => x.id == 'admins')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user