mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
feat(frontend): Menu + Tab components (#517)
* feat(frontend): Migrate Tabs * feat(frontend): Generalise Menu * feat(frontend): Menu component done * feat(frontend): Fix placements * feat(frontend): Clean unused imports * feat(frontend): Fix linting
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { FlowService, Job, ScheduleService, type Flow } from '$lib/gen'
|
||||
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
encodeState,
|
||||
|
||||
@@ -2,27 +2,26 @@
|
||||
import type { CompletedJob } from '$lib/gen'
|
||||
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import Tabs from './tabs/Tabs.svelte'
|
||||
import Tab from './tabs/Tab.svelte'
|
||||
import TabPanel from './tabs/TabPanel.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
|
||||
let value = 0
|
||||
export let job: CompletedJob | undefined
|
||||
</script>
|
||||
|
||||
{#if job}
|
||||
<div>
|
||||
<Tabs>
|
||||
<Tab bind:value index={0}>Results</Tab>
|
||||
<Tab bind:value index={1}>Logs</Tab>
|
||||
</Tabs>
|
||||
<TabPanel bind:value index={0} class="border p-2 h-36 overflow-y-scroll">
|
||||
<DisplayResult result={job.result} />
|
||||
</TabPanel>
|
||||
<TabPanel bind:value index={1} class="border p-2 h-36 overflow-y-scroll">
|
||||
<div class="text-xs p-4 bg-gray-50 overflow-auto max-h-80 border">
|
||||
<pre class="w-full">{job.logs}</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</div>
|
||||
<Tabs selected="results">
|
||||
<Tab value="results">Results</Tab>
|
||||
<Tab value="logs">Logs</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="results" class="border p-2 h-36 overflow-y-scroll">
|
||||
<DisplayResult result={job.result} />
|
||||
</TabContent>
|
||||
<TabContent value="logs" class="border p-2 h-36 overflow-y-scroll">
|
||||
<div class="text-xs p-4 bg-gray-50 overflow-auto max-h-80 border">
|
||||
<pre class="w-full">{job.logs}</pre>
|
||||
</div>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{/if}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import type { Job, JobService, Flow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import type { Flow } from '$lib/gen'
|
||||
import { sendUserToast, truncateRev } from '$lib/utils'
|
||||
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { onDestroy } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { flowStateStore, flowStateToFlow } from './flows/flowState'
|
||||
import { mapJobResultsToFlowState } from './flows/flowStateUtils'
|
||||
import { runFlowPreview } from './flows/utils'
|
||||
import FlowStatusViewer from './FlowStatusViewer.svelte'
|
||||
import RunForm from './RunForm.svelte'
|
||||
import Tabs from './Tabs.svelte'
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
|
||||
export let i: number
|
||||
export let flow: Flow
|
||||
@@ -81,33 +81,34 @@
|
||||
|
||||
{#if viewPreview}
|
||||
{#if i != flow.value.modules.length}
|
||||
<Tabs
|
||||
tabs={[
|
||||
['upto', uptoText],
|
||||
['justthis', 'preview just this step']
|
||||
]}
|
||||
bind:tab
|
||||
/>
|
||||
{/if}
|
||||
<div class="my-2" />
|
||||
{#if tab == 'upto'}
|
||||
<RunForm
|
||||
runnable={truncateFlow(flow)}
|
||||
runAction={(_, args) => runPreview(args)}
|
||||
schedulable={false}
|
||||
buttonText={uptoText}
|
||||
detailed={false}
|
||||
bind:args
|
||||
/>
|
||||
{:else}
|
||||
<RunForm
|
||||
runnable={extractStep(flow)}
|
||||
runAction={(_, args) => runPreview(args)}
|
||||
schedulable={false}
|
||||
buttonText="Preview just this step"
|
||||
detailed={false}
|
||||
args={stepArgs}
|
||||
/>
|
||||
<div class="mt-2">
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab value="upto">{uptoText}</Tab>
|
||||
<Tab value="justthis">Preview just this step</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="upto">
|
||||
<RunForm
|
||||
runnable={truncateFlow(flow)}
|
||||
runAction={(_, args) => runPreview(args)}
|
||||
schedulable={false}
|
||||
buttonText={uptoText}
|
||||
detailed={false}
|
||||
bind:args
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="justthis">
|
||||
<RunForm
|
||||
runnable={extractStep(flow)}
|
||||
runAction={(_, args) => runPreview(args)}
|
||||
schedulable={false}
|
||||
buttonText="Preview just this step"
|
||||
detailed={false}
|
||||
args={stepArgs}
|
||||
/>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if jobId}
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
|
||||
import type { FlowValue } from '$lib/gen'
|
||||
import { slide } from 'svelte/transition'
|
||||
import Tabs from './Tabs.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
|
||||
import SchemaViewer from './SchemaViewer.svelte'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
import InputTransformsViewer from './InputTransformsViewer.svelte'
|
||||
@@ -44,154 +47,161 @@
|
||||
</script>
|
||||
|
||||
{#if !embedded}
|
||||
<Tabs
|
||||
tabs={[
|
||||
['ui', 'Flow rendered'],
|
||||
['json', 'JSON'],
|
||||
['schema', 'Input schema of the flow']
|
||||
]}
|
||||
bind:tab
|
||||
/>
|
||||
{/if}
|
||||
{#if tab == 'ui'}
|
||||
<div class="flow-root w-full pb-4">
|
||||
{#if !embedded}
|
||||
<h2 class="my-4">{flow.summary}</h2>
|
||||
<SvelteMarkdown source={flow.description ?? ''} />
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab value="ui">Flow rendered</Tab>
|
||||
<Tab value="json">Json</Tab>
|
||||
<Tab value="schema">Input schema of the flow</Tab>
|
||||
|
||||
<p class="font-black text-lg w-full my-4">
|
||||
<span>Inputs</span>
|
||||
</p>
|
||||
{#if flow.schema && flow.schema.properties && Object.keys(flow.schema.properties).length > 0 && flow.schema}
|
||||
<ul class="my-2">
|
||||
{#each Object.entries(flow.schema.properties) as [inp, v]}
|
||||
<li class="list-disc flex flex-row">
|
||||
<FieldHeader
|
||||
label={inp}
|
||||
required={flow.schema.required?.includes(inp)}
|
||||
type={toAny(v)?.type}
|
||||
contentEncoding={toAny(v)?.contentEncoding}
|
||||
format={toAny(v)?.format}
|
||||
itemsType={toAny(v)?.itemsType}
|
||||
/><span class="ml-4 mt-2 text-xs"
|
||||
>{toAny(v)?.default != undefined
|
||||
? 'default: ' + JSON.stringify(toAny(v)?.default)
|
||||
: ''}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="text-gray-700 text-xs italic mb-4">No inputs</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="ui">
|
||||
<div class="flow-root w-full pb-4">
|
||||
{#if !embedded}
|
||||
<h2 class="my-4">{flow.summary}</h2>
|
||||
<SvelteMarkdown source={flow.description ?? ''} />
|
||||
|
||||
<p class="font-black text-lg my-6 w-full">
|
||||
<span>{flow?.value?.modules?.length} Step{flow?.value?.modules?.length > 1 ? 's' : ''} </span>
|
||||
<span class="mt-4" />
|
||||
</p>
|
||||
<ul class="-mb-8 w-full">
|
||||
{#each flow?.value?.modules ?? [] as mod, i}
|
||||
<li class="w-full">
|
||||
<div class="relative pb-8 w-full">
|
||||
{#if i < (flow?.value?.modules ?? []).length - 1}
|
||||
<span
|
||||
class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p class="font-black text-lg w-full my-4">
|
||||
<span>Inputs</span>
|
||||
</p>
|
||||
{#if flow.schema && flow.schema.properties && Object.keys(flow.schema.properties).length > 0 && flow.schema}
|
||||
<ul class="my-2">
|
||||
{#each Object.entries(flow.schema.properties) as [inp, v]}
|
||||
<li class="list-disc flex flex-row">
|
||||
<FieldHeader
|
||||
label={inp}
|
||||
required={flow.schema.required?.includes(inp)}
|
||||
type={toAny(v)?.type}
|
||||
contentEncoding={toAny(v)?.contentEncoding}
|
||||
format={toAny(v)?.format}
|
||||
itemsType={toAny(v)?.itemsType}
|
||||
/><span class="ml-4 mt-2 text-xs"
|
||||
>{toAny(v)?.default != undefined
|
||||
? 'default: ' + JSON.stringify(toAny(v)?.default)
|
||||
: ''}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="text-gray-700 text-xs italic mb-4">No inputs</div>
|
||||
{/if}
|
||||
<div class="relative flex space-x-3">
|
||||
<div>
|
||||
<span
|
||||
class="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center ring-8 ring-white text-white"
|
||||
>{i + 1}
|
||||
</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4 w-full">
|
||||
<div class="w-full">
|
||||
<span class="text-black">{mod?.summary ?? ''}</span>
|
||||
<p class="text-sm text-gray-500">
|
||||
{#if mod?.value?.type == 'script'}
|
||||
Script at path <a
|
||||
target="_blank"
|
||||
href={scriptPathToHref(mod?.value?.path ?? '')}
|
||||
class="font-medium text-gray-900"
|
||||
>
|
||||
<IconedPath path={mod?.value?.path ?? ''} />
|
||||
</a>
|
||||
{#if mod?.value?.path?.startsWith('hub/')}
|
||||
<div>
|
||||
<button
|
||||
on:click={async () => {
|
||||
open[i] = !open[i]
|
||||
}}
|
||||
class="mb-2 underline text-black"
|
||||
>
|
||||
View code and inputs {open[i] ? '(-)' : '(+)'}</button
|
||||
>
|
||||
{#if open[i]}
|
||||
<div class="border border-black p-2 bg-gray-50 divide-y">
|
||||
<InputTransformsViewer inputTransforms={mod?.input_transform} />
|
||||
<div class="w-full h-full mt-6">
|
||||
<iframe
|
||||
style="height: 400px;"
|
||||
class="w-full h-full text-sm"
|
||||
title="embedded script from hub"
|
||||
frameborder="0"
|
||||
src="https://hub.windmill.dev/embed/script/{mod?.value?.path?.substring(
|
||||
4
|
||||
)}"
|
||||
{/if}
|
||||
|
||||
<p class="font-black text-lg my-6 w-full">
|
||||
<span
|
||||
>{flow?.value?.modules?.length} Step{flow?.value?.modules?.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
<span class="mt-4" />
|
||||
</p>
|
||||
<ul class="-mb-8 w-full">
|
||||
{#each flow?.value?.modules ?? [] as mod, i}
|
||||
<li class="w-full">
|
||||
<div class="relative pb-8 w-full">
|
||||
{#if i < (flow?.value?.modules ?? []).length - 1}
|
||||
<span
|
||||
class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/if}
|
||||
<div class="relative flex space-x-3">
|
||||
<div>
|
||||
<span
|
||||
class="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center ring-8 ring-white text-white"
|
||||
>{i + 1}
|
||||
</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4 w-full">
|
||||
<div class="w-full">
|
||||
<span class="text-black">{mod?.summary ?? ''}</span>
|
||||
<p class="text-sm text-gray-500">
|
||||
{#if mod?.value?.type == 'script'}
|
||||
Script at path <a
|
||||
target="_blank"
|
||||
href={scriptPathToHref(mod?.value?.path ?? '')}
|
||||
class="font-medium text-gray-900"
|
||||
>
|
||||
<IconedPath path={mod?.value?.path ?? ''} />
|
||||
</a>
|
||||
{#if mod?.value?.path?.startsWith('hub/')}
|
||||
<div>
|
||||
<button
|
||||
on:click={async () => {
|
||||
open[i] = !open[i]
|
||||
}}
|
||||
class="mb-2 underline text-black"
|
||||
>
|
||||
View code and inputs {open[i] ? '(-)' : '(+)'}</button
|
||||
>
|
||||
{#if open[i]}
|
||||
<div class="border border-black p-2 bg-gray-50 divide-y">
|
||||
<InputTransformsViewer inputTransforms={mod?.input_transform} />
|
||||
<div class="w-full h-full mt-6">
|
||||
<iframe
|
||||
style="height: 400px;"
|
||||
class="w-full h-full text-sm"
|
||||
title="embedded script from hub"
|
||||
frameborder="0"
|
||||
src="https://hub.windmill.dev/embed/script/{mod?.value?.path?.substring(
|
||||
4
|
||||
)}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if mod?.value?.type == 'rawscript'}
|
||||
<button
|
||||
on:click={() => (open[i] = !open[i])}
|
||||
class="mb-2 underline text-black"
|
||||
>
|
||||
Raw {mod?.value?.language} script {open[i] ? '(-)' : '(+)'}</button
|
||||
>
|
||||
|
||||
{#if open[i]}
|
||||
<div
|
||||
transition:slide
|
||||
class="border border-black p-2 bg-gray-50 w-full"
|
||||
>
|
||||
<InputTransformsViewer inputTransforms={mod?.input_transform} />
|
||||
|
||||
<Highlight
|
||||
language={mod?.value?.language == 'deno' ? typescript : python}
|
||||
code={mod?.value?.content}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if mod?.value?.type == 'flow'}
|
||||
Flow at path {mod?.value?.path}
|
||||
{:else if mod?.value?.type == 'forloopflow'}
|
||||
For loop over all the elements of the list returned as a result of step {i}:
|
||||
<svelte:self flow={mod?.value} embedded={true} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if mod?.value?.type == 'rawscript'}
|
||||
<button
|
||||
on:click={() => (open[i] = !open[i])}
|
||||
class="mb-2 underline text-black"
|
||||
>
|
||||
Raw {mod?.value?.language} script {open[i] ? '(-)' : '(+)'}</button
|
||||
>
|
||||
|
||||
{#if open[i]}
|
||||
<div transition:slide class="border border-black p-2 bg-gray-50 w-full">
|
||||
<InputTransformsViewer inputTransforms={mod?.input_transform} />
|
||||
|
||||
<Highlight
|
||||
language={mod?.value?.language == 'deno' ? typescript : python}
|
||||
code={mod?.value?.content}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if mod?.value?.type == 'flow'}
|
||||
Flow at path {mod?.value?.path}
|
||||
{:else if mod?.value?.type == 'forloopflow'}
|
||||
For loop over all the elements of the list returned as a result of step {i}:
|
||||
<svelte:self flow={mod?.value} embedded={true} />
|
||||
{/if}
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{:else if tab == 'json'}
|
||||
<div class="relative">
|
||||
<button
|
||||
on:click={async () => {
|
||||
await navigator.clipboard.writeText(JSON.stringify(flowFiltered, null, 4))
|
||||
}}
|
||||
class="absolute default-secondary-button-v2 bg-white/30 right-0 my-2 ml-4"
|
||||
>copy content</button
|
||||
>
|
||||
<Highlight language={json} code={JSON.stringify(flowFiltered, null, 4)} />
|
||||
</div>
|
||||
{:else if tab == 'schema'}
|
||||
<div class="my-4" />
|
||||
<SchemaViewer schema={flow.schema} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="json">
|
||||
<div class="relative">
|
||||
<button
|
||||
on:click={async () => {
|
||||
await navigator.clipboard.writeText(JSON.stringify(flowFiltered, null, 4))
|
||||
}}
|
||||
class="absolute default-secondary-button-v2 bg-white/30 right-0 my-2 ml-4"
|
||||
>copy content</button
|
||||
>
|
||||
<Highlight language={json} code={JSON.stringify(flowFiltered, null, 4)} />
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="schema">
|
||||
<div class="my-4" />
|
||||
<SchemaViewer schema={flow.schema} />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{/if}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur(e) {
|
||||
function handleBlur() {
|
||||
optionsVisibility(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,79 +5,68 @@
|
||||
import Highlight from 'svelte-highlight'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
|
||||
export let schema: Schema | undefined = emptySchema()
|
||||
|
||||
let viewJsonSchema = false
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col sm:flex-row text-base">
|
||||
<button
|
||||
class="text-xs sm:text-base py-1 px-6 block hover:text-blue-500 focus:outline-noneborder-gray-200 {viewJsonSchema
|
||||
? 'text-gray-500 '
|
||||
: 'text-gray-700 font-semibold '}"
|
||||
on:click={() => (viewJsonSchema = false)}
|
||||
>
|
||||
arguments
|
||||
</button><button
|
||||
class="py-1 px-6 block hover:text-blue-500 focus:outline-none border-gray-200 {viewJsonSchema
|
||||
? 'text-gray-700 font-semibold '
|
||||
: 'text-gray-500'}"
|
||||
on:click={() => (viewJsonSchema = true)}
|
||||
>
|
||||
advanced
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!--json schema or table view-->
|
||||
<div class="border-t py-1">
|
||||
<div class={viewJsonSchema ? 'hidden' : ''}>
|
||||
{#if schema && schema.properties && Object.keys(schema.properties).length > 0 && schema.required}
|
||||
<div class="flex flex-row">
|
||||
<TableCustom>
|
||||
<tr slot="header-row" class="underline">
|
||||
<th>name</th>
|
||||
<th>type</th>
|
||||
<th>description</th>
|
||||
<th>default</th>
|
||||
<th>format</th>
|
||||
<th>required</th>
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each Object.entries(schema.properties) as [name, property] (name)}
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td
|
||||
>{#if !property.type} any {:else} {property.type} {/if}</td
|
||||
>
|
||||
<td>{property.description}</td>
|
||||
<td
|
||||
>{property.default == '<function call>'
|
||||
? '<function call>'
|
||||
: property.default
|
||||
? JSON.stringify(property.default)
|
||||
: ''}</td
|
||||
>
|
||||
<td
|
||||
>{property.format ?? ''}
|
||||
{property.contentEncoding ? `(encoding: ${property.contentEncoding})` : ''}</td
|
||||
>
|
||||
<td
|
||||
>{#if schema.required.includes(name)}
|
||||
<span class="text-red-600 font-bold text-lg">*</span>
|
||||
{/if}</td
|
||||
>
|
||||
<Tabs selected="arguments">
|
||||
<Tab value="arguments">Arguments</Tab>
|
||||
<Tab value="advanced">Advanced</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="arguments">
|
||||
{#if schema && schema.properties && Object.keys(schema.properties).length > 0 && schema.required}
|
||||
<div class="flex flex-row">
|
||||
<TableCustom>
|
||||
<tr slot="header-row" class="underline">
|
||||
<th>name</th>
|
||||
<th>type</th>
|
||||
<th>description</th>
|
||||
<th>default</th>
|
||||
<th>format</th>
|
||||
<th>required</th>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-gray-700 text-xs italic">This script has no arguments</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class={viewJsonSchema ? '' : 'hidden'}>
|
||||
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
|
||||
</div>
|
||||
<tbody slot="body">
|
||||
{#each Object.entries(schema.properties) as [name, property] (name)}
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td
|
||||
>{#if !property.type} any {:else} {property.type} {/if}</td
|
||||
>
|
||||
<td>{property.description}</td>
|
||||
<td
|
||||
>{property.default == '<function call>'
|
||||
? '<function call>'
|
||||
: property.default
|
||||
? JSON.stringify(property.default)
|
||||
: ''}</td
|
||||
>
|
||||
<td
|
||||
>{property.format ?? ''}
|
||||
{property.contentEncoding
|
||||
? `(encoding: ${property.contentEncoding})`
|
||||
: ''}</td
|
||||
>
|
||||
<td
|
||||
>{#if schema.required.includes(name)}
|
||||
<span class="text-red-600 font-bold text-lg">*</span>
|
||||
{/if}</td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-gray-700 text-xs italic">This script has no arguments</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
<TabContent value="advanced">
|
||||
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
import PageHeader from './PageHeader.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import Tabs from './Tabs.svelte'
|
||||
import Tab from './common/tabs/Tab.svelte'
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import TabContent from './common/tabs/TabContent.svelte'
|
||||
|
||||
import Highlight from 'svelte-highlight'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
@@ -14,7 +16,6 @@
|
||||
export let description: string | undefined
|
||||
export let synchronizedHeader = true
|
||||
|
||||
let tab: 'ui' | 'jsonschema' = 'ui'
|
||||
export function setSchema(newSchema: Schema) {
|
||||
schema = newSchema
|
||||
}
|
||||
@@ -22,51 +23,51 @@
|
||||
|
||||
<div class="w-full">
|
||||
<PageHeader title="UI customisation" />
|
||||
<Tabs
|
||||
tabs={[
|
||||
['ui', 'UI'],
|
||||
['jsonschema', `Jsonschema`]
|
||||
]}
|
||||
bind:tab
|
||||
/>
|
||||
|
||||
<div class="mt-8" />
|
||||
{#if tab == 'ui'}
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<h2 class="mb-1">Summary</h2>
|
||||
<div class="mb-2 md:mb-3 text-sm">
|
||||
<textarea bind:value={summary} placeholder="Edit summary" />
|
||||
<div class="prose text-sm">
|
||||
{summary != '' ? summary : 'No summary'}
|
||||
<Tabs selected="ui">
|
||||
<Tab value="ui">UI</Tab>
|
||||
<Tab value="jsonschema">JSON Schema</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="ui">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<h2 class="mb-1">Summary</h2>
|
||||
<div class="mb-2 md:mb-3 text-sm">
|
||||
<textarea bind:value={summary} placeholder="Edit summary" />
|
||||
<div class="prose text-sm">
|
||||
{summary != '' ? summary : 'No summary'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<h2 class="mb-1">Description</h2>
|
||||
<div class="mb-2 md:mb-6">
|
||||
<div class="prose text-sm">
|
||||
<textarea bind:value={description} placeholder="Edit description" />
|
||||
<div class="prose text-sm">
|
||||
<SvelteMarkdown
|
||||
source={description && description != '' ? description : 'No description'}
|
||||
/>
|
||||
<div class="col-span-2">
|
||||
<h2 class="mb-1">Description</h2>
|
||||
<div class="mb-2 md:mb-6">
|
||||
<div class="prose text-sm">
|
||||
<textarea bind:value={description} placeholder="Edit description" />
|
||||
<div class="prose text-sm">
|
||||
<SvelteMarkdown
|
||||
source={description && description != '' ? description : 'No description'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if synchronizedHeader}
|
||||
<div class="bg-blue-100 border-l-4 border-blue-600 text-blue-700 p-4 m-4" role="alert">
|
||||
<p class="font-bold">Synchronized with main signature</p>
|
||||
<p>
|
||||
Argument names, being required or not, and default values are derived from the main
|
||||
signature of step 2 and cannot be edited directly. Change the main signature to edit them.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<SchemaForm {schema} editableSchema={true} />
|
||||
{:else}
|
||||
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
|
||||
{/if}
|
||||
{#if synchronizedHeader}
|
||||
<div class="bg-blue-100 border-l-4 border-blue-600 text-blue-700 p-4 m-4" role="alert">
|
||||
<p class="font-bold">Synchronized with main signature</p>
|
||||
<p>
|
||||
Argument names, being required or not, and default values are derived from the main
|
||||
signature of step 2 and cannot be edited directly. Change the main signature to edit
|
||||
them.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<SchemaForm {schema} editableSchema={true} />
|
||||
</TabContent>
|
||||
<TabContent value="jsonschema">
|
||||
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { userStore } from '$lib/stores'
|
||||
import { faPeopleGroup, faShare } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPeopleGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
import Badge from './Badge.svelte'
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { onMount } from 'svelte'
|
||||
import { scale } from 'svelte/transition'
|
||||
|
||||
let show = false
|
||||
let menu: HTMLDivElement
|
||||
|
||||
type Alignment = 'start' | 'end'
|
||||
type Side = 'top' | 'bottom'
|
||||
type Placement = `${Side}-${Alignment}`
|
||||
|
||||
export let placement: Placement = 'bottom-start'
|
||||
|
||||
function handleOutsideClick(event) {
|
||||
if (show && !menu.contains(event.target)) {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscape(event) {
|
||||
if (show && event.key === 'Escape') {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
show = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', handleOutsideClick, false)
|
||||
document.addEventListener('keyup', handleEscape, false)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick, false)
|
||||
document.removeEventListener('keyup', handleEscape, false)
|
||||
}
|
||||
})
|
||||
|
||||
const placementsClasses = {
|
||||
'bottom-start': 'origin-top-left left-0',
|
||||
'bottom-end': 'origin-top-right right-0',
|
||||
'top-start': 'origin-bottom-left left-0 bottom-0',
|
||||
'top-end': 'origin-bottom-right right-0 bottom-0'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative" bind:this={menu}>
|
||||
<div>
|
||||
<div on:click={() => (show = !show)} on:click>
|
||||
<slot name="trigger" />
|
||||
</div>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
in:scale={{ duration: 100, start: 0.95 }}
|
||||
out:scale={{ duration: 75, start: 0.95 }}
|
||||
class={classNames(
|
||||
'z-50 absolute mt-2 w-60 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
|
||||
placementsClasses[placement]
|
||||
)}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<slot {close} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div on:click class="py-1">
|
||||
<span
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { TabsContext } from './Tabs.svelte'
|
||||
|
||||
export let value: string
|
||||
|
||||
const { selected, update } = getContext<TabsContext>('Tabs')
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
value === $selected
|
||||
? 'border-b-2 border-gray-900 text-gray-900 text-sm'
|
||||
: 'text-sm hover:border-b-2 hover:border-gray-300',
|
||||
' py-1 px-4 cursor-pointer transition-all ease-linear font-medium'
|
||||
)}
|
||||
on:click={() => update(value)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { TabsContext } from './Tabs.svelte'
|
||||
|
||||
export let value: string
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
const { selected } = getContext<TabsContext>('Tabs')
|
||||
</script>
|
||||
|
||||
{#if value === $selected}
|
||||
<div class={clazz}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,28 @@
|
||||
<script context="module" lang="ts">
|
||||
export type TabsContext = {
|
||||
selected: Writable<string>
|
||||
update: (value: string) => void
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
|
||||
export let selected: string
|
||||
|
||||
const selectedContent = writable(selected)
|
||||
|
||||
setContext<TabsContext>('Tabs', {
|
||||
selected: selectedContent,
|
||||
update: (value: string) => {
|
||||
selectedContent.set(value)
|
||||
selected = value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="border-b border-gray-200 flex flex-row mb-4">
|
||||
<slot />
|
||||
</div>
|
||||
<slot name="content" />
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { getScriptByPath, valueToTsType } from '$lib/utils'
|
||||
import { getScriptByPath } from '$lib/utils'
|
||||
import {
|
||||
faArrowDown,
|
||||
faClose,
|
||||
@@ -16,7 +16,6 @@
|
||||
import python from 'svelte-highlight/languages/python'
|
||||
import typescript from 'svelte-highlight/languages/typescript'
|
||||
import IconedPath from '../IconedPath.svelte'
|
||||
import IconedResourceType from '../IconedResourceType.svelte'
|
||||
import Modal from '../Modal.svelte'
|
||||
import { isEmptyFlowModule } from './flowStateUtils'
|
||||
import { stepOpened } from './stepOpenedStore'
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { faFileExport, faFileImport, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button, Dropdown, DropdownItem } from 'flowbite-svelte'
|
||||
import { Button } from 'flowbite-svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import Menu from '../common/menu/Menu.svelte'
|
||||
import MenuItem from '../common/menu/MenuItem.svelte'
|
||||
import Editor from '../Editor.svelte'
|
||||
import FlowViewer from '../FlowViewer.svelte'
|
||||
import Modal from '../Modal.svelte'
|
||||
@@ -62,43 +64,45 @@
|
||||
<FlowBox title="Flow Settings">
|
||||
<div slot="header">
|
||||
<div class="flex flex-row-reverse">
|
||||
<Dropdown class="w-fit" placement="bottom-end">
|
||||
<Menu placement="bottom-end">
|
||||
<button slot="trigger" class="text-gray-900 bg-white dark:text-white dark:bg-gray-800">
|
||||
...
|
||||
</button>
|
||||
<DropdownItem
|
||||
on:click={() => {
|
||||
jsonSetter.openModal()
|
||||
}}
|
||||
>
|
||||
<Icon data={faFileImport} scale={0.6} class="inline mr-2" />
|
||||
Import from a JSON OpenFlow
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
on:click={() => {
|
||||
jsonViewer.openModal()
|
||||
}}
|
||||
>
|
||||
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
|
||||
Export to a JSON OpenFlow
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
on:click={() => {
|
||||
const url = new URL('https://hub.windmill.dev/flows/add')
|
||||
const openFlow = {
|
||||
value: $flowStore.value,
|
||||
summary: $flowStore.summary,
|
||||
description: $flowStore.description,
|
||||
schema: $flowStore.schema
|
||||
}
|
||||
url.searchParams.append('flow', btoa(JSON.stringify(openFlow)))
|
||||
window.open(url, '_blank')?.focus()
|
||||
}}
|
||||
>
|
||||
<Icon data={faGlobe} scale={0.6} class="inline mr-2" />
|
||||
Publish to Hub
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
jsonSetter.openModal()
|
||||
}}
|
||||
>
|
||||
<Icon data={faFileImport} scale={0.6} class="inline mr-2" />
|
||||
Import from a JSON OpenFlow
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
jsonViewer.openModal()
|
||||
}}
|
||||
>
|
||||
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
|
||||
Export to a JSON OpenFlow
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
const url = new URL('https://hub.windmill.dev/flows/add')
|
||||
const openFlow = {
|
||||
value: $flowStore.value,
|
||||
summary: $flowStore.summary,
|
||||
description: $flowStore.description,
|
||||
schema: $flowStore.schema
|
||||
}
|
||||
url.searchParams.append('flow', btoa(JSON.stringify(openFlow)))
|
||||
window.open(url, '_blank')?.focus()
|
||||
}}
|
||||
>
|
||||
<Icon data={faGlobe} scale={0.6} class="inline mr-2" />
|
||||
Publish to Hub
|
||||
</MenuItem>
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,100 +3,64 @@
|
||||
|
||||
import { userStore, usersWorkspaceStore, superadmin } from '$lib/stores'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { faCog, faCrown, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCrown, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { onMount } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { scale } from 'svelte/transition'
|
||||
|
||||
let show = false
|
||||
let menu: HTMLDivElement
|
||||
import Menu from '../common/menu/Menu.svelte'
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
|
||||
const handleOutsideClick = (event) => {
|
||||
if (show && !menu.contains(event.target)) {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscape = (event) => {
|
||||
if (show && event.key === 'Escape') {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', handleOutsideClick, false)
|
||||
document.addEventListener('keyup', handleEscape, false)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick, false)
|
||||
document.removeEventListener('keyup', handleEscape, false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="relative" bind:this={menu}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class={classNames(
|
||||
'group w-full flex items-center text-white hover:bg-gray-50 hover:text-gray-900 focus:outline-none px-2 py-2 text-sm font-medium rounded-md h-8 '
|
||||
)}
|
||||
on:click={() => (show = !show)}
|
||||
>
|
||||
<Icon
|
||||
data={faUser}
|
||||
class={classNames('flex-shrink-0 h-4 w-4', isCollapsed ? '-mr-1' : 'mr-2')}
|
||||
/>
|
||||
<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:outline-none px-2 py-2 text-sm font-medium rounded-md h-8 '
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
data={faUser}
|
||||
class={classNames('flex-shrink-0 h-4 w-4', isCollapsed ? '-mr-1' : 'mr-2')}
|
||||
/>
|
||||
|
||||
{#if !isCollapsed}
|
||||
<span class={classNames('whitespace-pre ')}>
|
||||
{$userStore?.username ?? ($superadmin ? $superadmin : '___')}
|
||||
{#if $userStore?.is_admin}
|
||||
<Icon data={faCrown} scale={0.6} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if !isCollapsed}
|
||||
<span class={classNames('whitespace-pre ')}>
|
||||
{$userStore?.username ?? ($superadmin ? $superadmin : '___')}
|
||||
{#if $userStore?.is_admin}
|
||||
<Icon data={faCrown} scale={0.6} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
in:scale={{ duration: 100, start: 0.95 }}
|
||||
out:scale={{ duration: 75, start: 0.95 }}
|
||||
class="z-50 origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 focus:outline-none"
|
||||
role="menu"
|
||||
<div class="divide-y divide-gray-100">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm" role="none">Signed in as</p>
|
||||
<p class="text-sm font-medium text-gray-900 truncate" role="none">
|
||||
{$usersWorkspaceStore?.email}
|
||||
</p>
|
||||
</div>
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="/user/settings"
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm" role="none">Signed in as</p>
|
||||
<p class="text-sm font-medium text-gray-900 truncate" role="none">
|
||||
{$usersWorkspaceStore?.email}
|
||||
</p>
|
||||
</div>
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="/user/settings"
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
Account settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-1" role="none">
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
on:click={() => logout()}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Account settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-1" role="none">
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-700 block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
on:click={() => logout()}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
|
||||
@@ -1,109 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore, usersWorkspaceStore } from '$lib/stores'
|
||||
import { classNames } from '$lib/utils'
|
||||
import {
|
||||
faBuilding,
|
||||
faCog,
|
||||
faFolder,
|
||||
faFolderOpen,
|
||||
faFolderTree
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { onMount } from 'svelte'
|
||||
import { faBuilding } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { scale } from 'svelte/transition'
|
||||
|
||||
let show = false
|
||||
let menu: HTMLDivElement
|
||||
import Menu from '../common/menu/Menu.svelte'
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
|
||||
const handleOutsideClick = (event) => {
|
||||
if (show && !menu.contains(event.target)) {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscape = (event) => {
|
||||
if (show && event.key === 'Escape') {
|
||||
show = false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('click', handleOutsideClick, false)
|
||||
document.addEventListener('keyup', handleEscape, false)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleOutsideClick, false)
|
||||
document.removeEventListener('keyup', handleEscape, false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="relative" bind:this={menu}>
|
||||
<div>
|
||||
<button
|
||||
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 '
|
||||
)}
|
||||
on:click={() => (show = !show)}
|
||||
>
|
||||
<Icon
|
||||
data={faBuilding}
|
||||
class={classNames('flex-shrink-0 h-4 w-4', isCollapsed ? '-mr-1' : 'mr-2')}
|
||||
/>
|
||||
<Menu placement="bottom-start" let:close>
|
||||
<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 '
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
data={faBuilding}
|
||||
class={classNames('flex-shrink-0 h-4 w-4', isCollapsed ? '-mr-1' : 'mr-2')}
|
||||
/>
|
||||
|
||||
{#if !isCollapsed}
|
||||
<span class={classNames('whitespace-pre')}> {$workspaceStore} </span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if !isCollapsed}
|
||||
<span class={classNames('whitespace-pre')}> {$workspaceStore} </span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
in:scale={{ duration: 100, start: 0.95 }}
|
||||
out:scale={{ duration: 75, start: 0.95 }}
|
||||
class="z-50 origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-100 focus:outline-none"
|
||||
role="menu"
|
||||
<div class="divide-y divide-gray-100" role="none">
|
||||
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
|
||||
<button
|
||||
on:click={() => {
|
||||
workspaceStore.set(workspace.id)
|
||||
close()
|
||||
}}
|
||||
class="block px-4 py-2 text-xs text-gray-500 "
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="py-1" role="none">
|
||||
{#each $usersWorkspaceStore?.workspaces ?? [] as workspace}
|
||||
<button
|
||||
on:click={() => {
|
||||
workspaceStore.set(workspace.id)
|
||||
show = false
|
||||
}}
|
||||
class="block px-4 py-2 text-xs text-gray-500 "
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span class="text-gray-300 font-mono pr-1 text-xs">{workspace.id}</span>
|
||||
{workspace.name}
|
||||
</button>
|
||||
{/each}
|
||||
<a
|
||||
href="/user/create_workspace"
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
Create new workspace
|
||||
</a>
|
||||
<a
|
||||
href="/user/workspaces"
|
||||
on:click={() => {
|
||||
localStorage.removeItem('workspace')
|
||||
}}
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
See all workspaces & invites
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="text-gray-300 font-mono pr-1 text-xs">{workspace.id}</span>
|
||||
{workspace.name}
|
||||
</button>
|
||||
{/each}
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="/user/create_workspace"
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
Create new workspace
|
||||
</a>
|
||||
</div>
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href="/user/workspaces"
|
||||
on:click={() => {
|
||||
localStorage.removeItem('workspace')
|
||||
}}
|
||||
class="text-gray-700 block px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
See all workspaces & invites
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let value: number
|
||||
export let index: number
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={value === index
|
||||
? 'border-b-2 border-gray-900 text-gray-900 py-1 px-2 cursor-pointer font-bold text-sm'
|
||||
: 'py-1 px-2 cursor-pointer font-medium text-sm'}
|
||||
on:click={() => (value = index)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let value: number
|
||||
export let index: number
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
</script>
|
||||
|
||||
{#if value === index}
|
||||
<div class={clazz}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,3 +0,0 @@
|
||||
<div class="border-b border-gray-200 flex flex-row space-x-4 mb-4">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
|
||||
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
|
||||
|
||||
@@ -168,7 +168,7 @@ the bearer token they use has less privilege."
|
||||
<option value={undefined}>Show flow jobs regardless of being skipped or not</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="xl:max-w-screen-lg">
|
||||
<div>
|
||||
<Tabs
|
||||
tabs={[
|
||||
['all', 'all'],
|
||||
|
||||
Reference in New Issue
Block a user