feat(frontend): Script editor (#518)

* feat(frontend): Migrate Tabs

* feat(frontend): Generalise Menu

* feat(frontend): Menu component done

* feat(frontend): Fix placements

* feat(frontend): Rework component splitting

* feat(frontend): Clean unused imports

* feat(frontend): Script editor done

* feat(frontend): Code cleanup

* feat(frontend): Fix linting

* feat(frontend): Add running animation

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Faton Ramadani
2022-08-31 18:50:36 +02:00
committed by GitHub
co-authored by Ruben Fiszel
parent 6bb80b803d
commit a2265f7f41
10 changed files with 497 additions and 518 deletions
+45 -39
View File
@@ -2,11 +2,10 @@
import { ResourceService, ScriptService, VariableService } from '$lib/gen'
import { getScriptByPath, loadHubScripts, sendUserToast } from '$lib/utils'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { faCode, faCube, faFile, faRotate, faRotateLeft } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { hubScripts, workspaceStore } from '$lib/stores'
import { Button } from 'flowbite-svelte'
import { Highlight } from 'svelte-highlight'
import { python, typescript } from 'svelte-highlight/languages'
import type Editor from './Editor.svelte'
@@ -170,55 +169,69 @@
</ItemPicker>
<ResourceEditor bind:this={resourceEditor} on:refresh={resourcePicker.openModal} />
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openModal} />
<div class="flex flex-row w-full">
<div class="flex flex-row w-full space-x-2 whitespace-nowrap">
<Button
size="xs"
color="alternative"
<div class="flex divide-x items-center">
<div>
<button
type="button"
class="mr-1 bg-white rounded-md items-center flex hover:bg-gray-100 font-medium text-xs p-2"
on:click={() => {
variablePicker.openModal()
}}
>
<Icon data={faSearch} scale={0.7} class="mr-2" />
Variable picker
</Button>
<Button
size="xs"
color="alternative"
<Icon data={faFile} class="h-4 w-4 mr-2" />
Insert variable
</button>
</div>
<div>
<button
type="button"
class="mx-1 bg-white rounded-md items-center flex hover:bg-gray-100 font-medium text-xs p-2"
on:click={() => {
resourcePicker.openModal()
}}
>
<Icon data={faSearch} scale={0.7} class="mr-2" />
Resource picker
</Button>
<Icon data={faCube} class="h-4 w-4 mr-2" />
Insert resource
</button>
</div>
<Button
size="xs"
color="alternative"
<div>
<button
type="button"
class="mx-1 bg-white rounded-md items-center flex hover:bg-gray-100 font-medium text-xs p-2"
on:click={() => {
scriptPicker.openModal()
}}
>
<Icon data={faSearch} scale={0.7} class="mr-2" />
Script explorer
</Button>
<Icon data={faCode} class="h-4 w-4 mr-2" />
Search script
</button>
</div>
<div class="flex flex-row-reverse gap-x-2 w-full">
<Button
size="xs"
class={!websocketAlive.pyright && !websocketAlive.deno && !websocketAlive.black
? 'bg-red-50'
: ''}
color="alternative"
<div>
<button
type="button"
class="mx-1 bg-white rounded-md items-center flex hover:bg-gray-100 font-medium text-xs p-2"
on:click={() => {
editor.clearContent()
}}
>
<Icon data={faRotateLeft} class="h-4 w-4 mr-2" />
Reset content
</button>
</div>
<div>
<button
type="button"
class="ml-1 bg-white rounded-md items-center flex hover:bg-gray-100 font-medium text-xs p-2"
on:click={() => {
editor.reloadWebsocket()
}}
>
<Icon data={faRotate} class="h-4 w-4 mr-2" />
Reload assistants
<span class="ml-1">
{#if lang == 'deno'}
@@ -229,13 +242,6 @@
<span class={websocketAlive.black ? 'text-green-600' : 'text-red-700'}>Black</span>)
{/if}
</span>
</Button>
<Button
size="xs"
color="alternative"
on:click={() => {
editor.clearContent()
}}>Reset content</Button
>
</button>
</div>
</div>
@@ -16,7 +16,7 @@
import { onDestroy, onMount } from 'svelte'
import Icon from 'svelte-awesome'
import { OFFSET } from './CronInput.svelte'
import Drawer from './Drawer.svelte'
import Drawer from './common/drawer/Drawer.svelte'
import FlowEditor from './FlowEditor.svelte'
import FlowPreviewContent from './FlowPreviewContent.svelte'
import { flowStateStore, flowStateToFlow, type FlowState } from './flows/flowState'
+123 -137
View File
@@ -20,10 +20,10 @@
import Required from './Required.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import CenteredPage from './CenteredPage.svelte'
let editor: ScriptEditor
let scriptSchema: ScriptSchema
$: step = Number($page.url.searchParams.get('step')) || 1
export let script: Script
export let initialPath: string = ''
@@ -32,7 +32,7 @@
let pathError = ''
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
$: step = Number($page.url.searchParams.get('step')) || 1
$: {
if (script.language == 'python3') {
script.is_trigger = false
@@ -103,11 +103,11 @@
})
</script>
<div class="flex flex-col h-screen ">
<div class="flex flex-col h-screen">
<!-- Nav between steps-->
<div class="flex flex-col w-full">
<div class="justify-between flex flex-row drop-shadow-sm w-full mt-4">
<div class="wizard-nav flex flex-row w-full">
<div class="flex flex-col w-full px-4 py-2 border-b shadow-sm">
<div class="justify-between flex flex-row drop-shadow-sm w-full">
<div class="flex flex-row w-full">
<Breadcrumb>
<BreadcrumbItem>
<button on:click={() => changeStep(1)} class={step === 1 ? 'font-bold' : null}>
@@ -141,8 +141,10 @@
class="default-button px-6 max-h-8"
on:click={() => {
changeStep(step + 1)
}}>Next</button
}}
>
Next
</button>
{:else}
<button class="default-button px-6 self-end" on:click={editScript}>Save</button>
{/if}
@@ -151,8 +153,10 @@
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={async () => {
changeStep(step - 1)
}}>Back</button
}}
>
Back
</button>
{/if}
{#if step == 2}
<button
@@ -160,97 +164,68 @@
on:click={async () => {
await inferSchema()
editScript()
}}>Save (commit)</button
}}
>
Save (commit)
</button>
{/if}
</div>
</div>
<div class="flex flex-row justify-between gap-x-20">
<span class="my-1 text-xs text-gray-500">
<a target="_blank" href="https://github.com/windmill-labs/windmill-gh-action-deploy"
>sync from github instead</a
>
</span>
<span class="my-1 text-sm text-gray-500 italic">
{#if script.hash != ''} Editing from {script.hash} with path{/if}
{#if initialPath && initialPath != script.path} {initialPath} &rightarrow; {/if}
{script.path}
</span>
</div>
</div>
<!-- metadata -->
{#if step === 1}
<div class="grid grid-cols-1 gap-6 max-w-7xl">
<Path
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="my_script"
kind="script"
>
<div slot="ownerToolkit">
Script permissions depend on their path. Select the group <span class="font-mono"
>all</span
>
to share your script, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<h3 class="text-gray-700 border-b">Language</h3>
<div class="max-w-md">
<RadioButton
label="Language"
options={[
['Typescript (Deno)', 'deno'],
['Python 3.10', 'python3']
]}
on:change={(e) => initContent(e.detail, template)}
bind:value={script.language}
/>
</div>
{#if script.language == 'deno'}
<h4 class="text-gray-700 border-b">Template</h4>
<CenteredPage>
<div class="space-y-6">
<Path
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="my_script"
kind="script"
>
<div slot="ownerToolkit">
Script permissions depend on their path. Select the group
<span class="font-mono"> all </span>
to share your script, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<h3 class="text-gray-700 border-b">Language</h3>
<div class="max-w-md">
<RadioButton
label="Template"
label="Language"
options={[
['None', undefined],
['PostgreSQL', 'pgsql']
['Typescript (Deno)', 'deno'],
['Python 3.10', 'python3']
]}
on:change={(e) => initContent(script.language, e.detail)}
bind:value={template}
on:change={(e) => initContent(e.detail, template)}
bind:value={script.language}
/>
</div>
{/if}
{#if script.language == 'deno'}
<h4 class="text-gray-700 border-b">Template</h4>
<h3 class="text-gray-700 pb-1 border-b">Metadata</h3>
<div class="max-w-md">
<RadioButton
label="Template"
options={[
['None', undefined],
['PostgreSQL', 'pgsql']
]}
on:change={(e) => initContent(script.language, e.detail)}
bind:value={template}
/>
</div>
{/if}
<label class="block ">
<span class="text-gray-700">Summary <Required required={false} /></span>
<textarea
bind:value={script.summary}
class="
mt-1
block
w-full
rounded-md
border-gray-300
shadow-sm
focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50
"
placeholder="A very short summary of the script displayed when the script is listed"
rows="1"
/>
</label>
<label class="block" for="inp">
<span class="text-gray-700"
>Description<Required required={false} detail="accept markdown formatting" />
<h3 class="text-gray-700 pb-1 border-b">Metadata</h3>
<label class="block ">
<span class="text-gray-700">Summary <Required required={false} /></span>
<textarea
id="inp"
bind:value={script.description}
bind:value={script.summary}
class="
mt-1
block
@@ -260,69 +235,80 @@
shadow-sm
focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50
"
placeholder="A description to help users understand what this script does and how to use it."
rows="3"
placeholder="A very short summary of the script displayed when the script is listed"
rows="1"
/>
</span></label
>
</label>
<label class="block" for="inp">
<span class="text-gray-700"
>Description<Required required={false} detail="accept markdown formatting" />
<textarea
id="inp"
bind:value={script.description}
class="
mt-1
block
w-full
rounded-md
border-gray-300
shadow-sm
focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50
"
placeholder="A description to help users understand what this script does and how to use it."
rows="3"
/>
</span>
</label>
<label class="block">
<span class="text-gray-700 mr-2">Save as template</span>
<input type="checkbox" bind:checked={script.is_template} />
</label>
<label class="block">
<span class="text-gray-700 mr-2">Save as template</span>
<input type="checkbox" bind:checked={script.is_template} />
</label>
<label class="block">
<span class="text-gray-700 mr-2">Save as trigger script</span>
<input
disabled={script.language == 'python3'}
type="checkbox"
bind:checked={script.is_trigger}
on:change={() => {
if (
script.content == DENO_INIT_CODE ||
script.content == DENO_INIT_CODE_TRIGGER ||
script.content == POSTGRES_INIT_CODE
) {
initContent(script.language, template)
}
}}
/>
</label>
<label class="block">
<span class="text-gray-700 mr-2">Save as trigger script</span>
<input
disabled={script.language == 'python3'}
type="checkbox"
bind:checked={script.is_trigger}
on:change={() => {
if (
script.content == DENO_INIT_CODE ||
script.content == DENO_INIT_CODE_TRIGGER ||
script.content == POSTGRES_INIT_CODE
) {
initContent(script.language, template)
}
}}
/>
</label>
<div>
<h3 class="text-gray-700 ">Description rendered</h3>
<div
class="prose mt-5 text-xs shadow-inner shadow-blue p-4 overflow-auto"
style="max-height: 200px;"
>
<SvelteMarkdown source={script.description ?? ''} />
<div>
<h3 class="text-gray-700 ">Description rendered</h3>
<div
class="prose mt-5 text-xs shadow-inner shadow-blue p-4 overflow-auto"
style="max-height: 200px;"
>
<SvelteMarkdown source={script.description ?? ''} />
</div>
</div>
</div>
</div>
</CenteredPage>
{:else if step === 2}
<div class="flex-1 overflow-auto">
<ScriptEditor
bind:this={editor}
bind:schema={script.schema}
path={script.path}
bind:code={script.content}
lang={script.language}
/>
</div>
{:else if step === 3}
<ScriptSchema
bind:summary={script.summary}
bind:description={script.description}
<ScriptEditor
bind:this={editor}
bind:schema={script.schema}
path={script.path}
bind:code={script.content}
lang={script.language}
/>
{:else if step === 3}
<CenteredPage>
<ScriptSchema
bind:summary={script.summary}
bind:description={script.description}
bind:schema={script.schema}
/>
</CenteredPage>
{/if}
</div>
<style>
/* .wizard-nav {
@apply w-1/2 sm:w-1/4;
} */
.wizard-nav button {
max-height: 30px;
}
</style>
+119 -332
View File
@@ -2,52 +2,35 @@
import type { Schema } from '$lib/common'
import { CompletedJob, Job, JobService } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { displayDate, emptySchema } from '$lib/utils'
import { classNames, emptySchema } from '$lib/utils'
import {
faCheck,
faChevronDown,
faChevronUp,
faExclamationTriangle,
faSpinner,
faTimes
faPlay,
faRotateRight
} from '@fortawesome/free-solid-svg-icons'
import { onDestroy, onMount } from 'svelte'
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import { fade } from 'svelte/transition'
import Editor from './Editor.svelte'
import Modal from './Modal.svelte'
import TableCustom from './TableCustom.svelte'
import Tooltip from './Tooltip.svelte'
import { inferArgs } from '$lib/infer'
// @ts-ignore
import type { Preview } from '$lib/gen/models/Preview'
import { Highlight } from 'svelte-highlight'
import { json, python, typescript } from 'svelte-highlight/languages'
import { VSplitPane } from 'svelte-split-pane'
import DisplayResult from './DisplayResult.svelte'
import EditorBar from './EditorBar.svelte'
import SchemaForm from './SchemaForm.svelte'
import LogPanel from './script_editor/LogPanel.svelte'
import { HSplitPane, VSplitPane } from 'svelte-split-pane'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import EditorBar from './EditorBar.svelte'
// Exported
export let schema: Schema = emptySchema()
export let code: string
export let path: string | undefined
export let lang: Preview.language
// Control Editor layout
export let viewPreview = true
export let previewTab: 'logs' | 'input' | 'output' | 'history' | 'last_save' = 'input'
let websocketAlive = { pyright: false, black: false, deno: false }
let modalViewerTitle: string = ''
let modalViewerContent: any
let modalViewerMode: 'logs' | 'result' | 'code' = 'logs'
// Internal state
let editor: Editor
@@ -60,14 +43,11 @@
let previewIntervalId: NodeJS.Timer
let previewJob: Job | undefined
let pastPreviews: CompletedJob[] = []
let modalViewer: Modal
let lastSave: string | null
let syncIteration: number = 0
let ITERATIONS_BEFORE_SLOW_REFRESH = 100
let lastSave: string | null
$: lastSave = localStorage.getItem(path ?? 'last_save')
export function getEditor(): Editor {
@@ -186,6 +166,7 @@
}
let syncCode: NodeJS.Timer
onMount(() => {
syncCode = setInterval(() => {
const newCode = editor?.getCode()
@@ -194,6 +175,7 @@
}
}, 3000)
})
onDestroy(() => {
if (editor) {
code = editor.getCode()
@@ -207,315 +189,120 @@
})
</script>
<Modal bind:this={modalViewer}>
<div slot="title">{modalViewerTitle}</div>
<div slot="content">
{#if modalViewerMode === 'result'}
<Highlight language={json} code={JSON.stringify(modalViewerContent, null, 4)} />
{:else if modalViewerMode === 'logs'}
<pre class="overflow-x-auto break-all relative h-full m-2 text-xs bg-white shadow-inner">
{modalViewerContent}
</pre>
{:else if modalViewerMode === 'code'}
{#if lang == 'python3'}
<Highlight language={python} code={modalViewerContent} />
{:else if lang == 'deno'}
<Highlight language={typescript} code={modalViewerContent} />
{/if}
{/if}
</div></Modal
>
<div class="border-b shadow-sm p-1 pr-4">
<div class="flex justify-between">
<EditorBar {editor} {lang} {websocketAlive} />
<VSplitPane
topPanelSize={viewPreview ? '75%' : '90%'}
downPanelSize={viewPreview ? '25%' : '10%'}
updateCallback={() => {
if (!viewPreview) {
viewPreview = true
}
}}
>
<top slot="top">
<div class="flex flex-col h-full">
<div class="header">
<EditorBar {editor} {lang} {websocketAlive} />
<div class="flex divide-x">
<div>
<a
target="_blank"
href="https://github.com/windmill-labs/windmill-gh-action-deploy"
class="text-gray-800 mx-1 bg-white rounded-md items-center flex border-gray-300 hover:bg-gray-100 font-medium text-xs p-2"
>
<Icon data={faGithub} class="h-4 w-4 mr-2" />
Sync from Github
</a>
</div>
<div
class="flex-1 overflow-hidden border p-2 rounded"
on:mouseleave={() => {
code = getEditor().getCode()
inferSchema()
}}
>
<Editor
{code}
bind:websocketAlive
bind:this={editor}
cmdEnterAction={() => {
runPreview()
viewPreview = true
}}
formatAction={async () => {
code = getEditor().getCode()
await inferSchema()
localStorage.setItem(path ?? 'last_save', code)
lastSave = code
}}
on:blur={() => {
<div>
<button
type="button"
on:click|stopPropagation={() => runPreview()}
disabled={previewIsLoading}
class="text-white ml-1 w-28 bg-blue-500 hover:bg-blue-700 rounded-md flex justify-center items-center focus:outline-none font-medium text-xs p-2"
>
<Icon
data={previewIsLoading ? faRotateRight : faPlay}
class={classNames('h-4 w-4 mr-2', previewIsLoading ? 'animate-spin' : 'animate-none')}
/>
{previewIsLoading ? 'Running' : 'Run preview'}
</button>
</div>
</div>
</div>
</div>
<div class="flex-1 overflow-auto">
<HSplitPane leftPaneSize="60%" rightPaneSize="40%" minLeftPaneSize="50px" minRightPaneSize="50px">
<left slot="left">
<div class="h-full">
<div
class="p-2 h-full"
on:mouseleave={() => {
code = getEditor().getCode()
inferSchema()
}}
class="h-full"
deno={lang == 'deno'}
automaticLayout={true}
/>
</div>
</div>
</top>
<down slot="down" class="flex flex-col h-full">
<div class="header">
<div
class="flex flex-row w-full cursor-pointer h-full"
on:click={() => {
viewPreview = !viewPreview
}}
>
<div class="flex flex-row flex-wrap items-baseline">
<div class="font-base py-0 mr-6 hidden md:block">
Preview <Tooltip
><span class="font-normal"
>Test your script by running a preview, passing inputs as if you were a user</span
></Tooltip
>
<span style="min-width: 15px; display: inline-block;">
{#if previewIsLoading}
<span transition:fade>
<Icon class="animate-spin" data={faSpinner} scale={0.8} />
</span>
{/if}
</span>
</div>
<button
class="font-semibold my-0 py-0 h-full {previewTab === 'input'
? 'underline drop-shadow-md'
: ''}"
on:click|stopPropagation={() => {
previewTab = 'input'
viewPreview = true
>
<Editor
{code}
bind:websocketAlive
bind:this={editor}
cmdEnterAction={() => {
runPreview()
}}
formatAction={async () => {
code = getEditor().getCode()
await inferSchema()
localStorage.setItem(path ?? 'last_save', code)
lastSave = code
}}
on:blur={() => {
code = getEditor().getCode()
inferSchema()
}}
>
Inputs
</button>
<button
class="font-semibold my-0 py-0 h-full ml-3 {previewTab === 'logs' ? 'underline' : ''}"
on:click|stopPropagation={() => {
previewTab = 'logs'
viewPreview = true
}}
>
Logs
</button>
<button
class="font-semibold my-0 py-0 h-full ml-3 {previewTab === 'output' ? 'underline' : ''}"
on:click|stopPropagation={() => {
previewTab = 'output'
viewPreview = true
}}
>
Result
</button>
<button
class="font-semibold my-0 py-0 h-full ml-3 {previewTab === 'history'
? 'underline'
: ''}"
on:click|stopPropagation={() => {
if (pastPreviews.length == 0) {
loadPastPreviews()
}
previewTab = 'history'
viewPreview = true
}}
>
History
</button>
<button
class="font-semibold my-0 py-0 h-full ml-3 {previewTab === 'last_save'
? 'underline'
: ''}"
on:click|stopPropagation={() => {
previewTab = 'last_save'
viewPreview = true
}}
>
Local save
</button>
</div>
<div class="flex flex-row-reverse grow">
<button
class="mb-1 ml-2"
on:click|stopPropagation={() => {
viewPreview = !viewPreview
}}
><Icon data={viewPreview ? faChevronDown : faChevronUp} scale={0.7} />
</button>
<button
class="default-button py-px text-xs mx-2 align-middle max-h-8"
on:click|stopPropagation={() => {
runPreview()
viewPreview = true
previewTab = 'logs'
}}
>Run preview
</button>
<div class="text-xs text-gray-700 min-w-max hidden md:block mx-2">
Shortcuts: <Tooltip>
Cmd/Ctrl+S: autoformat code and overwrite local save <br />
Cmd/Ctrl+Enter: run preview</Tooltip
>
</div>
class="flex flex-1 h-full"
deno={lang == 'deno'}
automaticLayout={true}
/>
</div>
</div>
</div>
<div bind:this={div} class="preview flex-1 p-3">
{#if previewTab === 'logs'}
<pre
class="break-all relative h-full mx-2">{#if previewJob && previewJob.logs}{previewJob.logs}
{:else if previewIsLoading}Starting preview ...
{:else}No preview is available yet{/if}
</pre>
{:else if previewTab === 'input'}
<div class="break-all relative h-full font-sans -mt-2">
<div class="items-baseline text-xs text-gray-700 px-2 ml-8 italic hidden md:block">
<p>
Move the focus outside of the text editor to recompute the input schema from main
signature or press Ctrl/Cmd+S
</p>
<p class="">
{#if isValid}
<Icon data={faCheck} class="text-green-600 mr-1" scale={0.6} /> The current preview input
matches requirements defined in arguments
{:else}
<Icon data={faExclamationTriangle} class="text-yellow-500 mr-1" scale={0.6} />The
current preview input doesn't match requirements defined in arguments{/if}
</p>
</div>
<div class="sm:px-8">
<SchemaForm {schema} bind:args bind:isValid />
</div>
</div>
{:else if previewTab === 'output'}
<pre class="overflow-x-auto break-all relative h-full">
{#if previewJob && 'result' in previewJob && previewJob.result}
<DisplayResult result={previewJob.result} />
{:else if previewIsLoading}
Running...
{:else}
No output is available yet
{/if}
</pre>
{:else if previewTab === 'last_save'}
<div class="m-4">
{#if lastSave}
<a
href="#last_save"
class="text-xs"
on:click={() => {
modalViewerContent = lastSave
modalViewerMode = 'code'
modalViewer.openModal()
}}>View last local save for path {path}</a
>
{:else}No local save{/if}
</div>
{:else if previewTab === 'history'}
<TableCustom>
<tr slot="header-row">
<th class="text-xs">id</th>
<th class="text-xs">created at</th>
<th class="text-xs">success</th>
<th class="text-xs">result</th>
<th class="text-xs">code</th>
<th class="text-xs">logs</th>
</tr>
<tbody slot="body">
{#each pastPreviews as { id, created_at, success, result }}
<tr class="">
<td class="text-xs">
<a class="pr-3" href="/run/{id}" target="_blank">{id.substring(30)}</a>
</td>
<td class="text-xs">{displayDate(created_at)}</td>
<td class="text-xs">
{#if success}
<Icon class="text-green-600" data={check} scale={0.6} />
{:else}
<Icon class="text-red-700" data={faTimes} scale={0.6} />
{/if}
</td>
<td class="text-xs">
<a
href="#result"
class="text-xs"
on:click={() => {
modalViewerContent = result
modalViewerMode = 'result'
modalViewer.openModal()
}}
>{JSON.stringify(result).substring(0, 30)}...
</a>
</td>
<td class="text-xs"
><a
href="#code"
class="text-xs"
on:click={async () => {
modalViewerContent = (
await JobService.getCompletedJob({
workspace: $workspaceStore ?? 'NO_W',
id
})
).raw_code
modalViewerMode = 'code'
modalViewer.openModal()
}}
>View code
</a></td
>
<td
><a
href="#logs"
class="text-xs"
on:click={async () => {
modalViewerContent = (
await JobService.getCompletedJob({
workspace: $workspaceStore ?? 'NO_W',
id
})
).logs
modalViewerMode = 'logs'
modalViewer.openModal()
}}
>View logs
</a></td
>
</tr>
{/each}
</tbody>
</TableCustom>
{/if}
</div>
</down>
</VSplitPane>
<style>
.header {
@apply py-2;
}
.preview {
@apply border;
@apply overflow-auto;
@apply bg-white border-l border-r;
@apply text-xs font-mono;
}
</style>
</left>
<right slot="right">
<div class="h-full">
<VSplitPane topPanelSize="50%" downPanelSize="50%">
<top slot="top">
<div class="h-full overflow-auto">
<div class="p-4">
<div class="break-all relative font-sans">
<div class="items-baseline text-xs text-gray-700 italic hidden md:block">
<p>
Move the focus outside of the text editor to recompute the input schema from
main signature or press Ctrl/Cmd+S
</p>
<p class="mt-4">
{#if isValid}
<Icon data={faCheck} class="text-green-600 mr-1" scale={0.6} />
The current preview input matches requirements defined in arguments
{:else}
<Icon
data={faExclamationTriangle}
class="text-yellow-500 mr-1"
scale={0.6}
/>
The current preview input doesn't match requirements defined in arguments
{/if}
</p>
</div>
<SchemaForm {schema} bind:args bind:isValid />
</div>
</div>
</div>
</top>
<down slot="down">
<div class="pt-1 h-full overflow-auto">
<LogPanel
{path}
{lang}
{previewJob}
{pastPreviews}
{previewIsLoading}
bind:lastSave
/>
</div>
</down>
</VSplitPane>
</div>
</right>
</HSplitPane>
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import { faClose } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
export let title: string | undefined = undefined
const dispatch = createEventDispatcher()
</script>
<div class="divide-y h-screen">
<div class="flex justify-between items-center py-2 px-4">
<span class="text-sm font-bold">{title}</span>
<button on:click={() => dispatch('close')}>
<Icon data={faClose} class="w-4 h-4" />
</button>
</div>
<div class="p-2 h-full">
<slot />
</div>
</div>
@@ -0,0 +1,184 @@
<script lang="ts">
import { CompletedJob, Job, JobService, Preview } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { displayDate } from '$lib/utils'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import Tabs from '../common/tabs/Tabs.svelte'
import Tab from '../common/tabs/Tab.svelte'
import TabContent from '../common/tabs/TabContent.svelte'
import DisplayResult from '../DisplayResult.svelte'
import TableCustom from '../TableCustom.svelte'
import Drawer from '../common/drawer/Drawer.svelte'
import { Highlight } from 'svelte-highlight'
import { json, python, typescript } from 'svelte-highlight/languages'
import DrawerContent from '../common/drawer/DrawerContent.svelte'
export let path: string | undefined
export let lang: Preview.language
export let previewIsLoading = false
export let previewJob: Job | undefined
export let pastPreviews: CompletedJob[] = []
export let lastSave: string | null
type DrawerContent = {
mode: 'json' | Preview.language | 'plain'
title: string
content: string
}
let selectedTab = 'logs'
let drawerOpen: boolean = false
let drawerContent: DrawerContent | undefined = undefined
function openDrawer(newContent: DrawerContent) {
drawerContent = newContent
drawerOpen = true
}
function closeDrawer() {
drawerOpen = false
}
</script>
<Drawer bind:open={drawerOpen} size="800px">
<DrawerContent title={drawerContent?.title} on:close={() => closeDrawer()}>
{#if drawerContent?.mode === 'json'}
<Highlight language={json} code={JSON.stringify(drawerContent.content, null, 4)} />
{:else if drawerContent?.mode === 'plain'}
<pre class="overflow-x-auto break-all relative h-full m-2 text-xs bg-white shadow-inner p-2">
{drawerContent?.content}
</pre>
{:else if drawerContent?.mode === 'deno'}
<Highlight language={typescript} code={drawerContent?.content} />
{:else if drawerContent?.mode === 'python3'}
<Highlight language={python} code={drawerContent?.content} />
{/if}
</DrawerContent>
</Drawer>
<Tabs bind:selected={selectedTab}>
<Tab value="logs"><span class="text-xs">Logs</span></Tab>
<Tab value="results"><span class="text-xs">Results</span></Tab>
<Tab value="history"><span class="text-xs">History</span></Tab>
<Tab value="last_save"><span class="text-xs">Last save</span></Tab>
<svelte:fragment slot="content">
<div class="px-4">
<TabContent value="logs">
<pre class="whitespace-pre-wrap break-all relative h-full bg-gray-50 text-xs p-2">
{#if previewJob && previewJob.logs}
{previewJob.logs}
{:else if previewIsLoading}
Starting preview ...
{:else}
No preview is available yet
{/if}
</pre>
</TabContent>
<TabContent value="results">
<pre class="overflow-x-auto break-all relative h-full">
{#if previewJob && 'result' in previewJob && previewJob.result}
<DisplayResult result={previewJob.result} />
{:else if previewIsLoading}
Running...
{:else}
No output is available yet
{/if}
</pre>
</TabContent>
<TabContent value="history">
<TableCustom>
<tr slot="header-row">
<th class="text-xs">Id</th>
<th class="text-xs">Created at</th>
<th class="text-xs">Success</th>
<th class="text-xs">Result</th>
<th class="text-xs">Code</th>
<th class="text-xs">Logs</th>
</tr>
<tbody slot="body">
{#each pastPreviews as { id, created_at, success, result }}
<tr class="">
<td class="text-xs">
<a class="pr-3" href="/run/{id}" target="_blank">{id.substring(30)}</a>
</td>
<td class="text-xs">{displayDate(created_at)}</td>
<td class="text-xs">
{#if success}
<Icon class="text-green-600" data={check} scale={0.6} />
{:else}
<Icon class="text-red-700" data={faTimes} scale={0.6} />
{/if}
</td>
<td class="text-xs">
<a
href="#result"
class="text-xs"
on:click={() => {
openDrawer({ mode: 'json', content: result, title: 'Result' })
}}
>
{JSON.stringify(result).substring(0, 30)}...
</a>
</td>
<td class="text-xs">
<a
href="#code"
class="text-xs"
on:click={async () => {
const code = (
await JobService.getCompletedJob({
workspace: $workspaceStore ?? 'NO_W',
id
})
).raw_code
openDrawer({ mode: lang, content: String(code), title: `Code ${lang}` })
}}
>
View code
</a>
</td>
<td>
<a
href="#logs"
class="text-xs"
on:click={async () => {
const logs = (
await JobService.getCompletedJob({
workspace: $workspaceStore ?? 'NO_W',
id
})
).logs
openDrawer({ mode: 'plain', content: String(logs), title: `Code ${lang}` })
}}
>
View logs
</a>
</td>
</tr>
{/each}
</tbody>
</TableCustom>
</TabContent>
<TabContent value="last_save">
{#if lastSave}
<a
href="#last_save"
class="text-xs"
on:click={() => {
openDrawer({ mode: lang, content: String(lastSave), title: `Code ${lang}` })
}}
>
View last local save for path {path}
</a>
{:else}
No local save
{/if}
</TabContent>
</div>
</svelte:fragment>
</Tabs>
+2 -2
View File
@@ -93,7 +93,7 @@
<div
class={classNames(
'hidden md:flex md:flex-col md:fixed md:inset-y-0 transition-all ease-in-out duration-200 ',
'hidden md:flex md:flex-col md:fixed md:inset-y-0 transition-all ease-in-out duration-200 shadow-md',
isCollapsed ? 'md:w-12' : 'md:w-48'
)}
>
@@ -136,7 +136,7 @@
)}
>
<main>
<div class="border-b w-full">
<div class="w-full">
<div
class="py-2 px-2 sm:px-4 md:px-8 flex justify-between items-center shadow-sm max-w-6xl mx-auto md:hidden"
>
+1 -3
View File
@@ -77,6 +77,4 @@
}
</script>
<CenteredPage>
<ScriptBuilder {script} />
</CenteredPage>
<ScriptBuilder {script} />
@@ -13,7 +13,6 @@
import { workspaceStore } from '$lib/stores'
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
import { decodeState } from '$lib/utils'
import CenteredPage from '$lib/components/CenteredPage.svelte'
const initialState = $page.url.searchParams.get('state')
let scriptLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
@@ -41,7 +40,5 @@
</script>
{#if script}
<CenteredPage>
<ScriptBuilder {initialPath} {script} />
</CenteredPage>
<ScriptBuilder {initialPath} {script} />
{/if}