mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 00:03:07 +00:00
feat: add homepage connect drawer (#8880)
* feat: add homepage connect drawer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: reset connect drawer state Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: polish home connect button Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use standard home connect button style Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
46b2915a9d
commit
f35e10cc0a
@@ -9,9 +9,19 @@
|
||||
language: LanguageType<string>
|
||||
disabled?: boolean
|
||||
wrap?: boolean
|
||||
copyOnClick?: boolean
|
||||
}
|
||||
|
||||
let { code, language, disabled = false, wrap = false }: Props = $props()
|
||||
let { code, language, disabled = false, wrap = false, copyOnClick = true }: Props = $props()
|
||||
|
||||
function copyCode(event?: MouseEvent) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
copyToClipboard(code)
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
@@ -19,22 +29,24 @@
|
||||
<div
|
||||
class="flex flex-col flex-1 border rounded-md relative bg-surface-input"
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:cursor-pointer={copyOnClick && !disabled}
|
||||
class:cursor-text={!copyOnClick && !disabled}
|
||||
onclick={(e) => {
|
||||
if (disabled) {
|
||||
return
|
||||
if (copyOnClick) {
|
||||
copyCode(e)
|
||||
}
|
||||
e.preventDefault()
|
||||
copyToClipboard(code)
|
||||
}}
|
||||
>
|
||||
<div class="absolute top-2 right-1 z-10 pointer-events-none">
|
||||
<Copy size={14} class="w-8 cursor-pointer pointer-events-auto" />
|
||||
<div class="absolute top-2 right-1 z-10">
|
||||
<div class="w-8 cursor-pointer" onclick={copyCode}>
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 w-full overflow-auto">
|
||||
<Highlight
|
||||
{language}
|
||||
{code}
|
||||
class="pointer-events-none {wrap ? 'whitespace-pre-wrap break-all pr-8' : ''}"
|
||||
class="{copyOnClick ? 'pointer-events-none' : 'select-text'} {wrap ? 'whitespace-pre-wrap break-all pr-8' : ''}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button, Drawer, DrawerContent, Tab, TabContent, Tabs } from '$lib/components/common'
|
||||
import CreateToken from '$lib/components/settings/CreateToken.svelte'
|
||||
import CopyableCodeBlock from '$lib/components/details/CopyableCodeBlock.svelte'
|
||||
import { Bot, ExternalLink, Terminal } from 'lucide-svelte'
|
||||
import { shell } from 'svelte-highlight/languages'
|
||||
|
||||
type ConnectTab = 'cli' | 'mcp'
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
let selectedTab: ConnectTab = $state('cli')
|
||||
let openVersion = $state(0)
|
||||
|
||||
const origin = $derived(typeof window === 'undefined' ? '' : window.location.origin)
|
||||
const workspaceId = $derived($workspaceStore ?? '<workspace>')
|
||||
const cliCommands = $derived(`npm install -g windmill-cli
|
||||
wmill workspace add ${workspaceId} ${workspaceId} ${origin}
|
||||
wmill init
|
||||
wmill sync pull`)
|
||||
|
||||
function noop() {}
|
||||
|
||||
export function openDrawer(tab: ConnectTab = 'cli') {
|
||||
selectedTab = tab
|
||||
openVersion += 1
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
drawer?.closeDrawer()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size="720px">
|
||||
<DrawerContent title="Connect this workspace" on:close={closeDrawer}>
|
||||
<div class="flex flex-col gap-5 pb-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-full">
|
||||
<Tabs values={['cli', 'mcp']} bind:selected={selectedTab} wrapperClass="scrollbar-hidden">
|
||||
<Tab value="cli" label="CLI" icon={Terminal} />
|
||||
<Tab value="mcp" label="MCP" icon={Bot} />
|
||||
{#snippet content()}
|
||||
<div class="pt-4">
|
||||
<TabContent value="cli">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-sm font-semibold text-emphasis">Local setup</h3>
|
||||
<p class="text-xs text-secondary max-w-xl">
|
||||
Run this in your local repo to bind the current workspace, create
|
||||
<code class="rounded bg-surface-secondary px-1 py-0.5 font-mono text-2xs text-emphasis"
|
||||
>wmill.yaml</code
|
||||
>, and pull the latest files.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
href="https://www.windmill.dev/docs/advanced/cli"
|
||||
target="_blank"
|
||||
startIcon={{ icon: ExternalLink }}
|
||||
>
|
||||
CLI docs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CopyableCodeBlock
|
||||
code={cliCommands}
|
||||
language={shell}
|
||||
wrap
|
||||
copyOnClick={false}
|
||||
/>
|
||||
|
||||
<p class="text-2xs text-secondary">
|
||||
<code class="rounded bg-surface-secondary px-1 py-0.5 font-mono text-2xs text-emphasis"
|
||||
>wmill workspace add</code
|
||||
>
|
||||
will handle authentication,
|
||||
<code class="rounded bg-surface-secondary px-1 py-0.5 font-mono text-2xs text-emphasis"
|
||||
>wmill init</code
|
||||
>
|
||||
bootstraps the local config, and
|
||||
<code class="rounded bg-surface-secondary px-1 py-0.5 font-mono text-2xs text-emphasis"
|
||||
>wmill sync pull</code
|
||||
>
|
||||
fetches the workspace content.
|
||||
</p>
|
||||
</div>
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="mcp">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-sm font-semibold text-emphasis">MCP URL</h3>
|
||||
<p class="text-xs text-secondary max-w-xl">
|
||||
Generate an MCP server URL for the current workspace and choose which
|
||||
scripts, flows, and endpoints the client can access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
href="https://www.windmill.dev/docs/core_concepts/mcp"
|
||||
target="_blank"
|
||||
startIcon={{ icon: ExternalLink }}
|
||||
>
|
||||
MCP docs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#key openVersion}
|
||||
<CreateToken
|
||||
mcpOnly
|
||||
lockWorkspace
|
||||
title="Generate MCP URL"
|
||||
defaultNewTokenWorkspace={$workspaceStore}
|
||||
onTokenCreated={noop}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</TabContent>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -15,6 +15,9 @@
|
||||
interface Props {
|
||||
showMcpMode?: boolean
|
||||
openWithMcpMode?: boolean
|
||||
mcpOnly?: boolean
|
||||
lockWorkspace?: boolean
|
||||
title?: string
|
||||
newTokenLabel?: string
|
||||
defaultNewTokenWorkspace?: string
|
||||
scopes?: string[]
|
||||
@@ -24,6 +27,10 @@
|
||||
|
||||
let {
|
||||
showMcpMode = false,
|
||||
openWithMcpMode = false,
|
||||
mcpOnly = false,
|
||||
lockWorkspace = false,
|
||||
title = 'Add a new token',
|
||||
defaultNewTokenWorkspace,
|
||||
scopes,
|
||||
onTokenCreated,
|
||||
@@ -37,6 +44,8 @@
|
||||
let newTokenWorkspace = $state<string | undefined>(untrack(() => defaultNewTokenWorkspace))
|
||||
let mcpCreationMode = $state(false)
|
||||
let mcpScope = $state('mcp:favorites')
|
||||
let lastRequestedMcpMode = $state<boolean | undefined>(undefined)
|
||||
let mcpLabelAutofilled = $state(false)
|
||||
|
||||
let customScopes = $state<string[]>([])
|
||||
let showCustomScopes = $state(false)
|
||||
@@ -55,6 +64,31 @@
|
||||
return [{ id: currentWorkspace, name: currentWorkspace }, ...workspacesList]
|
||||
}
|
||||
|
||||
function enterMcpMode() {
|
||||
mcpCreationMode = true
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = defaultNewTokenWorkspace ?? $workspaceStore
|
||||
newToken = undefined
|
||||
newMcpToken = undefined
|
||||
if (!newTokenLabel) {
|
||||
newTokenLabel = 'MCP token'
|
||||
mcpLabelAutofilled = true
|
||||
} else {
|
||||
mcpLabelAutofilled = false
|
||||
}
|
||||
}
|
||||
|
||||
function exitMcpMode() {
|
||||
mcpCreationMode = false
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = defaultNewTokenWorkspace
|
||||
newMcpToken = undefined
|
||||
if (mcpLabelAutofilled) {
|
||||
newTokenLabel = undefined
|
||||
}
|
||||
mcpLabelAutofilled = false
|
||||
}
|
||||
|
||||
async function createToken(mcpMode: boolean = false): Promise<void> {
|
||||
try {
|
||||
let date: Date | undefined
|
||||
@@ -79,13 +113,17 @@
|
||||
})
|
||||
|
||||
if (mcpMode) {
|
||||
newToken = undefined
|
||||
newMcpToken = `${createdToken}`
|
||||
} else {
|
||||
newMcpToken = undefined
|
||||
newToken = `${createdToken}`
|
||||
}
|
||||
|
||||
onTokenCreated(newToken ?? newMcpToken ?? '')
|
||||
mcpCreationMode = false
|
||||
onTokenCreated(`${createdToken}`)
|
||||
if (!mcpOnly) {
|
||||
mcpCreationMode = false
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create token:', err)
|
||||
}
|
||||
@@ -93,13 +131,34 @@
|
||||
|
||||
const workspaces = $derived(ensureCurrentWorkspaceIncluded($userWorkspaces, $workspaceStore))
|
||||
const mcpBaseUrl = $derived(`${window.location.origin}/api/mcp/w/${newTokenWorkspace}/mcp?token=`)
|
||||
|
||||
$effect(() => {
|
||||
const requestedMcpMode = mcpOnly || openWithMcpMode
|
||||
if (requestedMcpMode === lastRequestedMcpMode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (requestedMcpMode) {
|
||||
enterMcpMode()
|
||||
} else {
|
||||
exitMcpMode()
|
||||
}
|
||||
|
||||
lastRequestedMcpMode = requestedMcpMode
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (mcpLabelAutofilled && newTokenLabel !== 'MCP token') {
|
||||
mcpLabelAutofilled = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 rounded-md mb-6 min-w-min bg-surface-tertiary">
|
||||
<h3 class="pb-2 font-semibold text-emphasis text-sm">Add a new token</h3>
|
||||
<h3 class="pb-2 font-semibold text-emphasis text-sm">{title}</h3>
|
||||
|
||||
{#if showMcpMode}
|
||||
{#if showMcpMode && !mcpOnly}
|
||||
<div
|
||||
class="mb-4 flex flex-row flex-shrink-0"
|
||||
use:triggerableByAI={{
|
||||
@@ -109,15 +168,10 @@
|
||||
>
|
||||
<Toggle
|
||||
on:change={(e) => {
|
||||
mcpCreationMode = e.detail
|
||||
if (e.detail) {
|
||||
newTokenLabel = 'MCP token'
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = $workspaceStore
|
||||
enterMcpMode()
|
||||
} else {
|
||||
newTokenLabel = undefined
|
||||
newTokenExpiration = undefined
|
||||
newTokenWorkspace = defaultNewTokenWorkspace
|
||||
exitMcpMode()
|
||||
}
|
||||
}}
|
||||
checked={mcpCreationMode}
|
||||
@@ -162,70 +216,76 @@
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{#if mcpCreationMode}
|
||||
<div class="col-span-2">
|
||||
<McpScopeSelector
|
||||
workspaceId={newTokenWorkspace || $workspaceStore || ''}
|
||||
bind:scope={mcpScope}
|
||||
/>
|
||||
</div>
|
||||
{#if mcpCreationMode}
|
||||
<div class="col-span-2">
|
||||
<McpScopeSelector
|
||||
workspaceId={newTokenWorkspace || $workspaceStore || ''}
|
||||
bind:scope={mcpScope}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block mb-1 text-emphasis text-xs font-semibold">Workspace</span>
|
||||
<Select
|
||||
bind:value={newTokenWorkspace}
|
||||
items={workspaces.map((w) => ({ label: w.name, value: w.id, subtitle: w.id }))}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !lockWorkspace}
|
||||
<div>
|
||||
<span class="block mb-1 text-emphasis text-xs font-semibold">Workspace</span>
|
||||
<Select
|
||||
bind:value={newTokenWorkspace}
|
||||
items={workspaces.map((w) => ({ label: w.name, value: w.id, subtitle: w.id }))}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<span class="block mb-1 text-emphasis text-xs font-semibold"
|
||||
>Label <span class="text-xs text-primary">(optional)</span></span
|
||||
>
|
||||
<TextInput inputProps={{ type: 'text' }} bind:value={newTokenLabel} class="w-full" />
|
||||
</div>
|
||||
{#if !mcpOnly}
|
||||
<div>
|
||||
<span class="block mb-1 text-emphasis text-xs font-semibold"
|
||||
>Label <span class="text-xs text-primary">(optional)</span></span
|
||||
>
|
||||
<TextInput inputProps={{ type: 'text' }} bind:value={newTokenLabel} class="w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !mcpCreationMode}
|
||||
<div>
|
||||
<span class="block mb-1 text-xs text-emphasis font-semibold"
|
||||
>Expires In <span class="text-xs text-primary">(optional)</span></span
|
||||
>
|
||||
<Select
|
||||
bind:value={newTokenExpiration}
|
||||
placeholder="No expiration"
|
||||
inputClass="w-full"
|
||||
items={[
|
||||
{ label: 'No expiration', value: undefined },
|
||||
{ label: '15 minutes', value: 15 * 60 },
|
||||
{ label: '30 minutes', value: 30 * 60 },
|
||||
{ label: '1 hour', value: 1 * 60 * 60 },
|
||||
{ label: '1 day', value: 1 * 24 * 60 * 60 },
|
||||
{ label: '7 days', value: 7 * 24 * 60 * 60 },
|
||||
{ label: '30 days', value: 30 * 24 * 60 * 60 },
|
||||
{ label: '90 days', value: 90 * 24 * 60 * 60 }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !mcpCreationMode}
|
||||
<div>
|
||||
<span class="block mb-1 text-xs text-emphasis font-semibold"
|
||||
>Expires In <span class="text-xs text-primary">(optional)</span></span
|
||||
>
|
||||
<Select
|
||||
bind:value={newTokenExpiration}
|
||||
placeholder="No expiration"
|
||||
inputClass="w-full"
|
||||
items={[
|
||||
{ label: 'No expiration', value: undefined },
|
||||
{ label: '15 minutes', value: 15 * 60 },
|
||||
{ label: '30 minutes', value: 30 * 60 },
|
||||
{ label: '1 hour', value: 1 * 60 * 60 },
|
||||
{ label: '1 day', value: 1 * 24 * 60 * 60 },
|
||||
{ label: '7 days', value: 7 * 24 * 60 * 60 },
|
||||
{ label: '30 days', value: 30 * 24 * 60 * 60 },
|
||||
{ label: '90 days', value: 90 * 24 * 60 * 60 }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2 flex-row">
|
||||
<Button
|
||||
on:click={() => {
|
||||
mcpCreationMode = false
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{#if !mcpOnly}
|
||||
<Button
|
||||
on:click={() => {
|
||||
exitMcpMode()
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => createToken(mcpCreationMode)}
|
||||
disabled={mcpCreationMode &&
|
||||
(newTokenWorkspace == undefined || !mcpScope || mcpScope.trim().length === 0)}
|
||||
variant="accent"
|
||||
>
|
||||
New token
|
||||
{mcpCreationMode ? 'Generate MCP URL' : 'New token'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
|
||||
import PickHubFlow from '$lib/components/flows/pickers/PickHubFlow.svelte'
|
||||
import HighlightCode from '$lib/components/HighlightCode.svelte'
|
||||
import HomeConnectDrawer from '$lib/components/home/HomeConnectDrawer.svelte'
|
||||
import {
|
||||
Building,
|
||||
ExternalLink,
|
||||
@@ -20,7 +21,8 @@
|
||||
Globe2,
|
||||
Loader2,
|
||||
Code,
|
||||
LayoutDashboard
|
||||
LayoutDashboard,
|
||||
PlugZap
|
||||
} from 'lucide-svelte'
|
||||
import { hubBaseUrlStore } from '$lib/stores'
|
||||
import { base } from '$lib/base'
|
||||
@@ -98,6 +100,7 @@
|
||||
}
|
||||
|
||||
let workspaceTutorials: WorkspaceTutorials | undefined = $state(undefined)
|
||||
let homeConnectDrawer: HomeConnectDrawer | undefined = $state(undefined)
|
||||
|
||||
// Provide workspaceTutorials to child components via a reactive wrapper
|
||||
let workspaceTutorialsContext = $derived(workspaceTutorials)
|
||||
@@ -283,8 +286,18 @@
|
||||
title="Home"
|
||||
childrenWrapperDivClasses="flex-1 flex flex-row gap-4 flex-wrap justify-end items-center"
|
||||
>
|
||||
{#if $userStore?.operator}
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: PlugZap }}
|
||||
btnClasses="whitespace-nowrap"
|
||||
onClick={() => homeConnectDrawer?.openDrawer?.()}
|
||||
>
|
||||
CLI / MCP
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !$userStore?.operator && showCreateButtons}
|
||||
<span class="text-xs font-normal text-primary">Create a</span>
|
||||
<CreateActionsScript aiId="create-script-button" aiDescription="Creates a new script" />
|
||||
{#if HOME_SHOW_CREATE_FLOW}<CreateActionsFlow />{/if}
|
||||
{#if HOME_SHOW_CREATE_APP}<CreateActionsApp />{/if}
|
||||
@@ -296,13 +309,25 @@
|
||||
<NoDirectDeployAlert onUpdateCanEditStatus={(v) => showCreateButtons = v}/>
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<div class="w-full overflow-auto scrollbar-hidden pb-2">
|
||||
<Tabs values={['hub', 'workspace']} hashNavigation bind:selected={tab}>
|
||||
<Tab value="workspace" label="Workspace" icon={Building} />
|
||||
{#if HOME_SHOW_HUB}
|
||||
<Tab value="hub" label="Hub" icon={Globe2} />
|
||||
{/if}
|
||||
</Tabs>
|
||||
<div class="flex w-full items-center gap-3 pb-2">
|
||||
<div class="min-w-0 flex-1 overflow-auto scrollbar-hidden">
|
||||
<Tabs values={['hub', 'workspace']} hashNavigation bind:selected={tab}>
|
||||
<Tab value="workspace" label="Workspace" icon={Building} />
|
||||
{#if HOME_SHOW_HUB}
|
||||
<Tab value="hub" label="Hub" icon={Globe2} />
|
||||
{/if}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
unifiedSize="sm"
|
||||
startIcon={{ icon: PlugZap }}
|
||||
btnClasses="whitespace-nowrap shrink-0"
|
||||
onClick={() => homeConnectDrawer?.openDrawer?.()}
|
||||
>
|
||||
CLI / MCP
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if tab == 'hub'}
|
||||
@@ -374,3 +399,4 @@
|
||||
</div>
|
||||
|
||||
<WorkspaceTutorials bind:this={workspaceTutorials} />
|
||||
<HomeConnectDrawer bind:this={homeConnectDrawer} />
|
||||
|
||||
Reference in New Issue
Block a user