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:
Faton Ramadani
2022-08-31 18:41:13 +02:00
committed by GitHub
parent 701275f625
commit 2fb5f1a204
22 changed files with 598 additions and 557 deletions

View File

@@ -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,

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -82,7 +82,7 @@
}
}
function handleBlur(e) {
function handleBlur() {
optionsVisibility(false)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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" />

View File

@@ -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'

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -1,3 +0,0 @@
<div class="border-b border-gray-200 flex flex-row space-x-4 mb-4">
<slot />
</div>

View File

@@ -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'

View File

@@ -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'],