feat: save inline scripts of apps to workspace + search by path

This commit is contained in:
Ruben Fiszel
2023-12-03 15:13:48 +01:00
parent 8409547cd1
commit f31cdc46ff
22 changed files with 257 additions and 53 deletions
@@ -0,0 +1,5 @@
-- Add down migration script here
ALTER TABLE script DROP COLUMN timeout;
ALTER TABLE script DROP COLUMN delete_after_use;
ALTER TABLE script DROP COLUMN restart_unless_cancelled;
ALTER TABLE flow DROP COLUMN timeout;
@@ -0,0 +1,5 @@
-- Add up migration script here
ALTER TABLE script ADD COLUMN timeout INTEGER;
ALTER TABLE flow ADD COLUMN timeout INTEGER;
ALTER TABLE script ADD COLUMN delete_after_use BOOLEAN;
ALTER TABLE script ADD COLUMN restart_unless_cancelled SMALLINT;
+4
View File
@@ -2579,6 +2579,7 @@ async fn test_flow_lock_all(db: Pool<Postgres>) {
ws_error_handler_muted: None,
priority: None,
dedicated_worker: None,
timeout: None,
},
draft_only: None,
},
@@ -3129,6 +3130,9 @@ async fn run_deployed_relative_imports(db: &Pool<Postgres>, script_content: Stri
schema: std::collections::HashMap::new(),
ws_error_handler_muted: Some(false),
priority: None,
delete_after_use: None,
timeout: None,
restart_unless_cancelled: None,
},
).await.unwrap();
+16
View File
@@ -6912,6 +6912,12 @@ components:
type: boolean
priority:
type: integer
restart_unless_cancelled:
type: boolean
timeout:
type: integer
delete_after_use:
type: boolean
required:
- hash
- path
@@ -6990,6 +6996,12 @@ components:
type: boolean
priority:
type: integer
restart_unless_cancelled:
type: boolean
timeout:
type: integer
delete_after_use:
type: boolean
required:
- path
- summary
@@ -8257,6 +8269,8 @@ components:
type: integer
dedicated_worker:
type: boolean
timeout:
type: number
required:
- path
- edited_by
@@ -8279,6 +8293,8 @@ components:
type: integer
dedicated_worker:
type: boolean
timeout:
type: number
required:
- path
+9
View File
@@ -1681,6 +1681,12 @@ struct ScriptMetadata {
priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_after_use: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restart_unless_cancelled: Option<bool>,
}
pub fn is_none_or_false(val: &Option<bool>) -> bool {
@@ -1886,6 +1892,9 @@ async fn tarball_workspace(
ws_error_handler_muted: script.ws_error_handler_muted,
priority: script.priority,
tag: script.tag,
timeout: script.timeout,
delete_after_use: script.delete_after_use,
restart_unless_cancelled: script.restart_unless_cancelled,
};
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
+3
View File
@@ -41,6 +41,8 @@ pub struct Flow {
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ws_error_handler_muted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<i32>,
}
#[derive(Serialize)]
@@ -74,6 +76,7 @@ pub struct NewFlow {
pub tag: Option<String>,
pub ws_error_handler_muted: Option<bool>,
pub dedicated_worker: Option<bool>,
pub timeout: Option<i32>,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
+9
View File
@@ -176,6 +176,12 @@ pub struct Script {
pub priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_ttl: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delete_after_use: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub restart_unless_cancelled: Option<bool>,
}
#[derive(Serialize)]
@@ -234,6 +240,9 @@ pub struct NewScript {
pub dedicated_worker: Option<bool>,
pub ws_error_handler_muted: Option<bool>,
pub priority: Option<i16>,
pub timeout: Option<i32>,
pub delete_after_use: Option<bool>,
pub restart_unless_cancelled: Option<bool>,
}
#[derive(Deserialize)]
@@ -11,15 +11,16 @@
import FlowIcon from './home/FlowIcon.svelte'
import { Alert, Button } from './common'
import { goto } from '$app/navigation'
import YAML from 'yaml'
let search: string = ''
export function open() {
export async function open(nsearch?: string) {
isOpen = true
loadScripts()
loadResources()
loadApps()
loadFlows()
await Promise.all([loadScripts(), loadResources(), loadApps(), loadFlows()])
if (nsearch) {
search = nsearch
}
}
export async function loadScripts() {
@@ -38,6 +39,8 @@
flows = await FlowService.listSearchFlow({ workspace: $workspaceStore ?? '' })
}
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = 'all'
let isOpen = false
let scripts: undefined | { path: string; content: string }[] = undefined
@@ -52,13 +55,19 @@
let apps: undefined | { path: string; value: any }[] = undefined
let filteredAppItems: { path: string; value: any; marked: any }[] = []
let searchKind: 'all' | 'scripts' | 'flows' | 'apps' | 'resources' = 'all'
function getCounts(n: number) {
return ` (${n})`
}
$: counts =
search == '' || !scripts || !resources || !flows || !apps
search == '' ||
!scripts ||
!resources ||
!flows ||
!apps ||
!filteredAppItems ||
!filteredFlowItems ||
!filteredResourceItems ||
!filteredScriptItems
? {
all: '',
apps: '',
@@ -93,7 +102,7 @@
filter={search}
items={resources}
f={(s) => {
return JSON.stringify(s.value, null, 4)
return YAML.stringify(s.value)
}}
bind:filteredItems={filteredResourceItems}
/>
@@ -102,7 +111,7 @@
filter={search}
items={flows}
f={(s) => {
return JSON.stringify(s.value, null, 4)
return YAML.stringify(s.value, null, 4)
}}
bind:filteredItems={filteredFlowItems}
/>
@@ -111,7 +120,7 @@
filter={search}
items={apps}
f={(s) => {
return JSON.stringify(s.value, null, 4)
return YAML.stringify(s.value, null, 4)
}}
bind:filteredItems={filteredAppItems}
/>
@@ -243,6 +252,8 @@
<div class="mt-1 overflow-auto max-h-[80vh]">
{#if !$enterpriseLicense}
<div class="py-1" />
<Alert title="Content Search is an EE feature" type="warning">
Without EE, content search will only search among 10 scripts, 3 flows, 3 apps and 3
resources.
@@ -252,8 +263,8 @@
{#if search.length > 0}
<div class="flex flex-col gap-4">
{#if (searchKind == 'all' || searchKind == 'scripts') && filteredScriptItems.length > 0}
{#each filteredScriptItems as item}
{#if (searchKind == 'all' || searchKind == 'scripts') && filteredScriptItems?.length > 0}
{#each filteredScriptItems ?? [] as item}
<div>
<div class="text-sm font-semibold"
><a href="/scripts/get/{item.path}">Script: {item.path}</a></div
@@ -280,8 +291,8 @@
</div>
{/each}
{/if}
{#if (searchKind == 'all' || searchKind == 'resources') && filteredResourceItems.length > 0}
{#each filteredResourceItems as item}
{#if (searchKind == 'all' || searchKind == 'resources') && filteredResourceItems?.length > 0}
{#each filteredResourceItems ?? [] as item}
<div>
<div class="text-sm font-semibold">Resource: {item.path}</div>
<div class="flex gap-2 justify-between">
@@ -292,8 +303,8 @@
</div>
{/each}
{/if}
{#if (searchKind == 'all' || searchKind == 'flows') && filteredFlowItems.length > 0}
{#each filteredFlowItems as item}
{#if (searchKind == 'all' || searchKind == 'flows') && filteredFlowItems?.length > 0}
{#each filteredFlowItems ?? [] as item}
<div>
<div class="text-sm font-semibold"
><a href="/flows/get/{item.path}">Flow: {item.path}</a></div
@@ -318,8 +329,8 @@
</div>
{/each}
{/if}
{#if (searchKind == 'all' || searchKind == 'apps') && filteredAppItems.length > 0}
{#each filteredAppItems as item}
{#if (searchKind == 'all' || searchKind == 'apps') && filteredAppItems?.length > 0}
{#each filteredAppItems ?? [] as item}
<div>
<div class="text-sm font-semibold"
><a href="/apps/get/{item.path}">App: {item.path}</a></div
+25 -2
View File
@@ -23,7 +23,17 @@
import { sendUserToast } from '$lib/toast'
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
import Toggle from './Toggle.svelte'
import { DollarSign, History, Library, Link, Package, Plus, RotateCw, Users } from 'lucide-svelte'
import {
DollarSign,
History,
Library,
Link,
Package,
Plus,
RotateCw,
Save,
Users
} from 'lucide-svelte'
import { capitalize, toCamel } from '$lib/utils'
import type { Schema, SchemaProperty, SupportedLanguage } from '$lib/common'
import ScriptVersionHistory from './ScriptVersionHistory.svelte'
@@ -54,6 +64,8 @@
export let scriptPath: string | undefined = undefined
export let diffEditor: DiffEditor | undefined = undefined
export let args: Record<string, any>
export let noHistory = false
export let saveToWorkspace = false
let contextualVariablePicker: ItemPicker
let variablePicker: ItemPicker
@@ -557,7 +569,7 @@
</div>
<div class="flex flex-row items-center gap-2">
{#if scriptPath}
{#if scriptPath && !noHistory}
<Button
btnClasses="!font-medium text-tertiary"
size="xs"
@@ -585,6 +597,17 @@
Library
</Button>
{/if}
{#if saveToWorkspace}
<Button
size="xs"
color="light"
startIcon={{ icon: Save }}
on:click={() => dispatch('createScriptFromInlineScript')}
iconOnly={false}
>
Save to workspace
</Button>
{/if}
</div>
</div>
+29 -2
View File
@@ -17,7 +17,7 @@
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import { writable } from 'svelte/store'
import { Button, Drawer, DrawerContent } from './common'
import { Alert, Button, Drawer, DrawerContent } from './common'
import Badge from './common/badge/Badge.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
@@ -25,7 +25,8 @@
import { random_adj } from './random_positive_adjetive'
import Required from './Required.svelte'
import Tooltip from './Tooltip.svelte'
import { Eye, Folder, Plus, User } from 'lucide-svelte'
import { Eye, Folder, Plus, SearchCode, User } from 'lucide-svelte'
import ContentSearch from './ContentSearch.svelte'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app' | 'raw_app'
let meta: Meta | undefined = undefined
@@ -261,8 +262,14 @@
function setDirty() {
!dirty && (dirty = true)
}
let contentSearch: ContentSearch
</script>
{#if kind != 'app' && kind != 'schedule' && initialPath != '' && initialPath != undefined}
<ContentSearch bind:this={contentSearch} />
{/if}
<Drawer bind:this={newFolder}>
<DrawerContent
title="New Folder"
@@ -441,6 +448,26 @@
</div>
<div class="text-red-600 dark:text-red-400 text-2xs">{error}</div>
</div>
{#if kind != 'app' && kind != 'schedule' && initialPath != '' && initialPath != undefined && initialPath != path}
<Alert type="warning" class="mt-4" title="Moving may break other items relying on it">
You are renaming an item that may be depended upon by other items. This may break apps, flows
or resources. Find if it used elsewhere using the content search. Note that linked variables
and resources (having the same path) are automatically moved together.
<div class="flex pt-2">
<Button
variant="border"
color="dark"
on:click={() => {
contentSearch?.open(initialPath)
}}
startIcon={{ icon: SearchCode }}
>
Search
</Button>
</div>
</Alert>
{/if}
</div>
<style>
@@ -39,6 +39,8 @@
export let diffEditor: DiffEditor | undefined = undefined
export let collabMode = false
export let edit = true
export let noHistory = false
export let saveToWorkspace = false
let websocketAlive = {
pyright: false,
@@ -246,12 +248,15 @@
on:collabPopup={() => (showCollabPopup = true)}
{editor}
{lang}
on:createScriptFromInlineScript
{websocketAlive}
collabUsers={peers}
kind={asKind(kind)}
{template}
{diffEditor}
{args}
{noHistory}
{saveToWorkspace}
/>
{#if !noSyncFromGithub}
<div class="py-1">
@@ -159,6 +159,10 @@
{editor}
bind:this={inlineScriptEditorDrawer}
bind:inlineScript
on:createScriptFromInlineScript={() => {
dispatch('createScriptFromInlineScript')
drawerIsOpen = false
}}
/>
{/if}
<div class="h-full flex flex-col gap-1">
@@ -27,17 +27,20 @@
>
{#if inlineScript && inlineScript.language != 'frontend'}
<ScriptEditor
noHistory
noSyncFromGithub
lang={inlineScript.language}
path={inlineScript.path ? inlineScript.path + '_fullscreen' : undefined}
fixedOverflowWidgets={false}
bind:code={inlineScript.content}
bind:schema={inlineScript.schema}
on:createScriptFromInlineScript
tag={undefined}
saveToWorkspace
/>
{/if}
<svelte:fragment slot="actions">
<Button startIcon={{ icon: Save }} disabled>Automatically Saved</Button>
<Button size="xs" startIcon={{ icon: Save }} disabled>Automatically Synced</Button>
</svelte:fragment>
</DrawerContent>
</Drawer>
@@ -4,7 +4,7 @@
import EmptyInlineScript from './EmptyInlineScript.svelte'
import InlineScriptEditor from './InlineScriptEditor.svelte'
import { getContext } from 'svelte'
import { createEventDispatcher, getContext } from 'svelte'
import type { AppViewerContext } from '../../types'
import InlineScriptRunnableByPath from './InlineScriptRunnableByPath.svelte'
@@ -28,6 +28,8 @@
componentInput.runnable = runnable
}
}
const dispatch = createEventDispatcher()
</script>
{#if transformer}
@@ -51,10 +53,18 @@
Selected editor component is a transformer but component has no transformer
</span>
{/if}
{:else if componentInput && componentInput.type == 'runnable'}
{:else if componentInput?.type == 'runnable'}
{#if componentInput?.runnable?.type === 'runnableByName' && componentInput?.runnable?.name !== undefined}
{#if componentInput.runnable.inlineScript}
<InlineScriptEditor
on:createScriptFromInlineScript={() => {
if (
componentInput?.type == 'runnable' &&
componentInput?.runnable?.type === 'runnableByName'
) {
dispatch('createScriptFromInlineScript', componentInput?.runnable)
}
}}
{defaultUserInput}
{id}
{componentType}
@@ -4,7 +4,7 @@
import EmptyInlineScript from './EmptyInlineScript.svelte'
import InlineScriptRunnableByPath from './InlineScriptRunnableByPath.svelte'
import type { Runnable, StaticAppInput } from '../../inputType'
import { getContext } from 'svelte'
import { createEventDispatcher, getContext } from 'svelte'
export let runnable: HiddenRunnable
export let id: string
@@ -24,6 +24,7 @@
recomputeOnInputChanged: true
}
}
const dispatch = createEventDispatcher()
</script>
{#if transformer}
@@ -48,6 +49,7 @@
{/if}
{:else if runnable?.type === 'runnableByName' && runnable.inlineScript}
<InlineScriptEditor
on:createScriptFromInlineScript={() => dispatch('createScriptFromInlineScript', runnable)}
{id}
bind:inlineScript={runnable.inlineScript}
bind:name={runnable.name}
@@ -1,6 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext, AppViewerContext } from '../../types'
import type { AppEditorContext, AppViewerContext, HiddenRunnable } from '../../types'
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import InlineScriptsPanelList from './InlineScriptsPanelList.svelte'
@@ -9,6 +9,11 @@
import { findGridItem } from '../appUtils'
import InlineScriptHiddenRunnable from './InlineScriptHiddenRunnable.svelte'
import { BG_PREFIX } from '../../utils'
import { sendUserToast } from '$lib/toast'
import type { RunnableByName } from '../../inputType'
import { ScriptService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { findNextAvailablePath } from '$lib/path'
const { app, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
const { selectedComponentInEditor } = getContext<AppEditorContext>('AppEditorContext')
@@ -51,6 +56,50 @@
$: unusedInlineScript = $app?.unusedInlineScripts?.findIndex(
(k_, index) => `unused-${index}` === $selectedComponentInEditor
)
async function createScriptFromInlineScript(
id: string,
runnable: HiddenRunnable | RunnableByName
) {
if (runnable.type != 'runnableByName') {
sendUserToast('Only inline scripts can be saved to workspace', true)
return
}
if (!runnable.inlineScript) {
sendUserToast('No inline script found', true)
return
}
let path = `${runnable.inlineScript.path}/inline_${id}`
path = await findNextAvailablePath(path)
let language = runnable.inlineScript.language
if (language == 'frontend') {
sendUserToast('Frontend scripts can not be saved to workspace', true)
return
}
await ScriptService.createScript({
workspace: $workspaceStore!,
requestBody: {
path: path,
summary: runnable.name ?? '',
description: '',
content: runnable.inlineScript.content,
parent_hash: undefined,
schema: runnable.inlineScript.schema,
is_template: false,
language
}
})
Object.assign(runnable, {
type: 'runnableByPath',
schema: runnable.inlineScript.schema,
runType: 'script',
recomputeIds: undefined,
path
})
$app = $app
}
</script>
<SplitPanesWrapper>
@@ -65,11 +114,18 @@
</div>
{:else if gridItem}
{#key gridItem?.id}
<InlineScriptsPanelWithTable bind:gridItem />
<InlineScriptsPanelWithTable
on:createScriptFromInlineScript={(e) => {
createScriptFromInlineScript(gridItem?.id ?? 'unknown', e.detail)
}}
bind:gridItem
/>
{/key}
{:else if unusedInlineScript > -1 && $app.unusedInlineScripts?.[unusedInlineScript]}
{#key unusedInlineScript}
<InlineScriptEditor
on:createScriptFromInlineScript={() =>
sendUserToast('Cannot save to workspace unused scripts', true)}
id={`unused-${unusedInlineScript}`}
bind:name={$app.unusedInlineScripts[unusedInlineScript].name}
bind:inlineScript={$app.unusedInlineScripts[unusedInlineScript].inlineScript}
@@ -84,6 +140,9 @@
{#key hiddenInlineScript}
{#if $app.hiddenInlineScripts?.[hiddenInlineScript]}
<InlineScriptHiddenRunnable
on:createScriptFromInlineScript={(e) => {
createScriptFromInlineScript(BG_PREFIX + hiddenInlineScript, e.detail)
}}
transformer={$selectedComponentInEditor?.endsWith('_transformer')}
on:delete={() => deleteBackgroundScript(hiddenInlineScript)}
id={BG_PREFIX + hiddenInlineScript}
@@ -10,6 +10,7 @@
{#if gridItem?.data?.id === $selectedComponentInEditor || gridItem?.data?.id + '_transformer' === $selectedComponentInEditor}
<InlineScriptEditorPanel
on:createScriptFromInlineScript
defaultUserInput={gridItem.data?.type == 'formcomponent' ||
gridItem?.data?.type == 'formbuttoncomponent'}
id={gridItem.data.id}
@@ -23,6 +24,7 @@
{#each gridItem.data.actionButtons as actionButton, index (index)}
{#if actionButton?.id === $selectedComponentInEditor || actionButton?.id + '_transformer' === $selectedComponentInEditor}
<InlineScriptEditorPanel
on:createScriptFromInlineScript
componentType={actionButton.type}
id={actionButton.id}
transformer={$selectedComponentInEditor?.endsWith('_transformer')}
@@ -36,6 +38,7 @@
{#each gridItem.data.menuItems as actionButton, index (index)}
{#if actionButton?.id === $selectedComponentInEditor || actionButton?.id + '_transformer' === $selectedComponentInEditor}
<InlineScriptEditorPanel
on:createScriptFromInlineScript
componentType={actionButton.type}
id={actionButton.id}
transformer={$selectedComponentInEditor?.endsWith('_transformer')}
@@ -13,10 +13,11 @@ import { userStore, workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/scripts'
import { get, type Writable } from 'svelte/store'
import type { FlowModuleState, FlowState } from './flowState'
import { emptyFlowModuleState, findNextAvailablePath } from './utils'
import { emptyFlowModuleState } from './utils'
import { NEVER_TESTED_THIS_FAR } from './models'
import { loadSchemaFromModule } from './flowInfers'
import { nextId } from './flowModuleNextId'
import { findNextAvailablePath } from '$lib/path'
export async function loadFlowModuleState(flowModule: FlowModule): Promise<FlowModuleState> {
try {
@@ -152,25 +152,3 @@ export function emptyFlowModuleState(): FlowModuleState {
previewResult: NEVER_TESTED_THIS_FAR
}
}
export async function findNextAvailablePath(path: string): Promise<string> {
try {
await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
path
})
const [_, version] = path.split(/.*_([0-9]*)/)
if (version.length > 0) {
path = path.slice(0, -(version.length + 1))
}
path = `${path}_${Number(version) + 1}`
return findNextAvailablePath(path)
} catch (e) {
// Catching an error means the path is available
return path
}
}
@@ -356,7 +356,7 @@
</button>
</div>
<Button
on:click={contentSearch?.open}
on:click={() => contentSearch?.open()}
variant="border"
btnClasses="py-2.5"
size="xs"
+25
View File
@@ -0,0 +1,25 @@
import { get } from 'svelte/store'
import { ScriptService } from './gen'
import { workspaceStore } from './stores'
export async function findNextAvailablePath(path: string): Promise<string> {
try {
await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
path
})
const [_, version] = path.split(/.*_([0-9]*)/)
if (version.length > 0) {
path = path.slice(0, -(version.length + 1))
}
path = `${path}_${Number(version) + 1}`
return findNextAvailablePath(path)
} catch (e) {
// Catching an error means the path is available
return path
}
}
+2
View File
@@ -103,6 +103,8 @@ components:
type: number
timeout:
type: number
delete_after_use:
type: boolean
summary:
type: string
mock: