mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-04 08:01:54 +00:00
Merge branch 'main' into fr/flow-triggers-v0
This commit is contained in:
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO windmill_migrations (name) VALUES ('bypassrls_1-2')",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "722a3096f03d25ef94292d53801d41037de4bc69dd434232029c731cbbcbc22f"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'bypassrls_1-2')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "eb1f916f9beea3eea83ce359f5305d0cfb0d6cdba9cc56c6139f57e46345f843"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
CREATE POLICY admin_policy ON account TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON app TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON audit TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON capture TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON completed_job TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON flow TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON folder TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON queue TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON raw_app TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON resource TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON schedule TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON script TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON usr_to_group TO windmill_admin USING (true);
|
||||
CREATE POLICY admin_policy ON variable TO windmill_admin USING (true);
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Add up migration script here
|
||||
DO
|
||||
$$
|
||||
DECLARE
|
||||
tbl_name text;
|
||||
policy_exists boolean;
|
||||
tbl_names text[] := ARRAY['account', 'app', 'audit', 'capture', 'completed_job', 'flow', 'folder', 'http_trigger', 'queue', 'raw_app', 'resource', 'schedule', 'script', 'usr_to_group', 'variable'];
|
||||
BEGIN
|
||||
FOR tbl_name IN SELECT unnest(tbl_names)
|
||||
LOOP
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename = tbl_name
|
||||
AND policyname = 'admin_policy'
|
||||
) INTO policy_exists;
|
||||
|
||||
IF NOT policy_exists THEN
|
||||
EXECUTE format('CREATE POLICY admin_policy ON %I TO windmill_admin USING (true);', tbl_name);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
@@ -199,11 +199,6 @@ pub async fn migrate(db: &DB) -> Result<(), Error> {
|
||||
Err(err) => Err(err),
|
||||
}?;
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
if let Err(e) = windmill_migrations(&mut custom_migrator, db).await {
|
||||
tracing::error!("Could not apply windmill custom migrations: {e:#}")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -497,33 +492,6 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn windmill_migrations(migrator: &mut CustomMigrator, db: &DB) -> Result<(), Error> {
|
||||
if std::env::var("MIGRATION_NO_BYPASSRLS").is_ok() {
|
||||
migrator.lock().await?;
|
||||
let has_done_migration = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT name FROM windmill_migrations WHERE name = 'bypassrls_1-2')",
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if !has_done_migration {
|
||||
let query = include_str!("../../custom_migrations/bypassrls_1.sql");
|
||||
tracing::info!("Applying bypassrls_1.sql");
|
||||
let mut tx: sqlx::Transaction<'_, Postgres> = db.begin().await?;
|
||||
tx.execute(query).await?;
|
||||
tracing::info!("Applied bypassrls_1.sql");
|
||||
sqlx::query!("INSERT INTO windmill_migrations (name) VALUES ('bypassrls_1-2')")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
migrator.unlock().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiAuthed {
|
||||
pub email: String,
|
||||
|
||||
@@ -318,15 +318,33 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
|
||||
POWERSHELL_CACHE_DIR
|
||||
);
|
||||
|
||||
// NOTE: powershell error handling / termination is quite tricky compared to bash
|
||||
// here we're trying to catch terminating errors and propagate the exit code
|
||||
// to the caller such that the job will be marked as failed. It's up to the user
|
||||
// to catch specific errors in their script not caught by the below as there is no
|
||||
// generic set -eu as in bash
|
||||
let strict_termination_start = "$ErrorActionPreference = 'Stop'\n\
|
||||
Set-StrictMode -Version Latest\n\
|
||||
try {\n";
|
||||
|
||||
let strict_termination_end = "\n\
|
||||
} catch {\n\
|
||||
Write-Output \"An error occurred:\n\"\
|
||||
Write-Output $_
|
||||
exit 1\n\
|
||||
}\n";
|
||||
|
||||
// make sure param() is first
|
||||
let param_match = windmill_parser_bash::RE_POWERSHELL_PARAM.find(&content);
|
||||
let content: String = if let Some(param_match) = param_match {
|
||||
let param_match = param_match.as_str();
|
||||
format!(
|
||||
"{}\n{}\n{}",
|
||||
"{}\n{}\n{}\n{}\n{}",
|
||||
param_match,
|
||||
profile,
|
||||
content.replace(param_match, "")
|
||||
strict_termination_start,
|
||||
content.replace(param_match, ""),
|
||||
strict_termination_end
|
||||
)
|
||||
} else {
|
||||
format!("{}\n{}", profile, content)
|
||||
@@ -351,7 +369,8 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
|
||||
$pipe = New-TemporaryFile\n\
|
||||
& \"{}\" -File ./main.ps1 @args 2>&1 | Tee-Object -FilePath $pipe\n\
|
||||
Get-Content -Path $pipe | Select-Object -Last 1 | Set-Content -Path './result2.out'\n\
|
||||
Remove-Item $pipe\n",
|
||||
Remove-Item $pipe\n\
|
||||
exit $LASTEXITCODE\n",
|
||||
POWERSHELL_PATH.as_str()
|
||||
),
|
||||
)?;
|
||||
|
||||
+47
-29
@@ -25,8 +25,8 @@ import {
|
||||
import {
|
||||
add as workspaceSetup,
|
||||
addWorkspace,
|
||||
allWorkspaces,
|
||||
removeWorkspace,
|
||||
setActiveWorkspace,
|
||||
} from "./workspace.ts";
|
||||
import {
|
||||
pushInstanceSettings,
|
||||
@@ -35,8 +35,9 @@ import {
|
||||
pushInstanceConfigs,
|
||||
type SimplifiedSettings,
|
||||
} from "./settings.ts";
|
||||
import { sleep, deepEqual } from "./utils.ts";
|
||||
import { deepEqual } from "./utils.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { getActiveWorkspace } from "./workspace.ts";
|
||||
|
||||
export interface Instance {
|
||||
remote: string;
|
||||
@@ -293,22 +294,18 @@ async function instancePull(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
log.info("No instance-level changes to apply");
|
||||
}
|
||||
|
||||
sleep(1000);
|
||||
|
||||
if (opts.includeWorkspaces) {
|
||||
log.info("\nPulling all workspaces");
|
||||
const rootDir = Deno.cwd();
|
||||
const localWorkspaces = await getLocalWorkspaces(rootDir, instance.prefix);
|
||||
|
||||
const previousActiveWorkspace = await getActiveWorkspace(undefined);
|
||||
const remoteWorkspaces = await wmill.listWorkspacesAsSuperAdmin({
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
});
|
||||
let localWorkspaces = await allWorkspaces();
|
||||
localWorkspaces = localWorkspaces.filter((w) =>
|
||||
w.name.startsWith(instance.prefix + "_")
|
||||
);
|
||||
const rootDir = Deno.cwd();
|
||||
for (const remoteWorkspace of remoteWorkspaces) {
|
||||
log.info("\nPulling workspace " + remoteWorkspace.id);
|
||||
sleep(1000);
|
||||
const workspaceName = instance.prefix + "_" + remoteWorkspace.id;
|
||||
await Deno.mkdir(path.join(rootDir, workspaceName), {
|
||||
recursive: true,
|
||||
@@ -341,7 +338,7 @@ async function instancePull(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
}
|
||||
|
||||
const localWorkspacesToDelete = localWorkspaces.filter(
|
||||
(w) => !remoteWorkspaces.find((r) => r.id === w.workspaceId)
|
||||
(w) => !remoteWorkspaces.find((r) => r.id === w.id)
|
||||
);
|
||||
|
||||
if (localWorkspacesToDelete.length > 0) {
|
||||
@@ -350,20 +347,23 @@ async function instancePull(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
(await Confirm.prompt({
|
||||
message:
|
||||
"Do you want to delete the local copy of workspaces that don't exist anymore on the instance?\n" +
|
||||
localWorkspacesToDelete.map((w) => w.workspaceId).join(", "),
|
||||
localWorkspacesToDelete.map((w) => w).join(", "),
|
||||
default: true,
|
||||
}));
|
||||
|
||||
if (confirmDelete) {
|
||||
for (const workspace of localWorkspacesToDelete) {
|
||||
await removeWorkspace(workspace.name, false, {});
|
||||
await Deno.remove(path.join(rootDir, workspace.name), {
|
||||
await removeWorkspace(workspace.id, false, {});
|
||||
await Deno.remove(path.join(rootDir, workspace.dir), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (previousActiveWorkspace) {
|
||||
await setActiveWorkspace(previousActiveWorkspace?.name);
|
||||
}
|
||||
log.info(colors.green.underline.bold("All workspaces pulled"));
|
||||
}
|
||||
}
|
||||
@@ -421,10 +421,10 @@ async function instancePush(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
log.info("No instance-level changes to apply");
|
||||
}
|
||||
|
||||
sleep(1000);
|
||||
|
||||
if (opts.includeWorkspaces) {
|
||||
instances = await allInstances();
|
||||
const rootDir = Deno.cwd();
|
||||
|
||||
const localPrefix = (await Select.prompt({
|
||||
message: "What is the prefix of the local workspaces you want to sync?",
|
||||
options: [
|
||||
@@ -440,18 +440,18 @@ async function instancePush(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
});
|
||||
let localWorkspaces = await allWorkspaces();
|
||||
localWorkspaces = localWorkspaces.filter((w) =>
|
||||
w.name.startsWith(localPrefix + "_")
|
||||
);
|
||||
|
||||
log.info("\nPushing all workspaces");
|
||||
const rootDir = Deno.cwd();
|
||||
const previousActiveWorkspace = await getActiveWorkspace(undefined);
|
||||
|
||||
const localWorkspaces = await getLocalWorkspaces(rootDir, localPrefix);
|
||||
|
||||
log.info(
|
||||
`\nPushing all workspaces: ${localWorkspaces.map((x) => x.id).join(", ")}`
|
||||
);
|
||||
for (const localWorkspace of localWorkspaces) {
|
||||
log.info("\nPushing workspace " + localWorkspace.workspaceId);
|
||||
sleep(1000);
|
||||
log.info("\nPushing workspace " + localWorkspace.id);
|
||||
try {
|
||||
await Deno.chdir(path.join(rootDir, localWorkspace.name));
|
||||
await Deno.chdir(path.join(rootDir, localWorkspace.dir));
|
||||
} catch (_) {
|
||||
throw new Error(
|
||||
"Workspace folder not found, are you in the right directory?"
|
||||
@@ -471,8 +471,8 @@ async function instancePush(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
createWorkspaceName: workspaceSettings.name,
|
||||
createUsername: undefined,
|
||||
},
|
||||
localWorkspace.name,
|
||||
localWorkspace.workspaceId,
|
||||
localWorkspace.dir,
|
||||
localWorkspace.id,
|
||||
instance.remote
|
||||
);
|
||||
} catch (_) {
|
||||
@@ -482,7 +482,7 @@ async function instancePush(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
continue;
|
||||
}
|
||||
await push({
|
||||
workspace: localWorkspace.name,
|
||||
workspace: localWorkspace.dir,
|
||||
token: undefined,
|
||||
baseUrl: undefined,
|
||||
includeGroups: true,
|
||||
@@ -495,7 +495,7 @@ async function instancePush(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
}
|
||||
|
||||
const workspacesToDelete = remoteWorkspaces.filter(
|
||||
(w) => !localWorkspaces.find((l) => l.workspaceId === w.id)
|
||||
(w) => !localWorkspaces.find((l) => l.id === w.id)
|
||||
);
|
||||
if (workspacesToDelete.length > 0) {
|
||||
const confirmDelete =
|
||||
@@ -514,10 +514,28 @@ async function instancePush(opts: GlobalOptions & InstanceSyncOptions) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previousActiveWorkspace) {
|
||||
await setActiveWorkspace(previousActiveWorkspace?.name);
|
||||
}
|
||||
log.info(colors.green.underline.bold("All workspaces pushed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocalWorkspaces(rootDir: string, localPrefix: string) {
|
||||
const localWorkspaces: { dir: string; id: string }[] = [];
|
||||
|
||||
for await (const dir of Deno.readDir(rootDir)) {
|
||||
const dirName = dir.name;
|
||||
if (dirName.startsWith(localPrefix + "_")) {
|
||||
localWorkspaces.push({
|
||||
dir: dirName,
|
||||
id: dirName.substring(localPrefix.length + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
return localWorkspaces;
|
||||
}
|
||||
|
||||
async function switchI(opts: {}, instanceName: string) {
|
||||
const all = await allInstances();
|
||||
if (all.findIndex((x) => x.name === instanceName) === -1) {
|
||||
|
||||
+12
-9
@@ -34,10 +34,10 @@ export async function allWorkspaces(): Promise<Workspace[]> {
|
||||
}
|
||||
|
||||
async function getActiveWorkspaceName(
|
||||
opts: GlobalOptions
|
||||
opts: GlobalOptions | undefined
|
||||
): Promise<string | undefined> {
|
||||
if (opts.workspace) {
|
||||
return opts.workspace;
|
||||
if (opts?.workspace) {
|
||||
return opts?.workspace;
|
||||
}
|
||||
try {
|
||||
return await Deno.readTextFile((await getRootStore()) + "/activeWorkspace");
|
||||
@@ -47,7 +47,7 @@ async function getActiveWorkspaceName(
|
||||
}
|
||||
|
||||
export async function getActiveWorkspace(
|
||||
opts: GlobalOptions
|
||||
opts: GlobalOptions | undefined
|
||||
): Promise<Workspace | undefined> {
|
||||
const name = await getActiveWorkspaceName(opts);
|
||||
if (!name) {
|
||||
@@ -115,7 +115,12 @@ async function switchC(opts: GlobalOptions, workspaceName: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await Deno.writeTextFile(
|
||||
await setActiveWorkspace(workspaceName);
|
||||
return;
|
||||
}
|
||||
|
||||
export async function setActiveWorkspace(workspaceName: string) {
|
||||
await Deno.writeTextFile(
|
||||
(await getRootStore()) + "/activeWorkspace",
|
||||
workspaceName
|
||||
);
|
||||
@@ -241,10 +246,8 @@ export async function add(
|
||||
},
|
||||
opts
|
||||
);
|
||||
await Deno.writeTextFile(
|
||||
(await getRootStore()) + "/activeWorkspace",
|
||||
workspaceName
|
||||
);
|
||||
await setActiveWorkspace(workspaceName);
|
||||
|
||||
log.info(
|
||||
colors.green.underline(
|
||||
`Added workspace ${workspaceName} for ${workspaceId} on ${remote}!`
|
||||
|
||||
@@ -300,6 +300,14 @@
|
||||
"types": "./package/utils.d.ts",
|
||||
"default": "./package/utils.js"
|
||||
},
|
||||
"./icons/store": {
|
||||
"types": "./package/icons/store.d.ts",
|
||||
"default": "./package/icons/store.js"
|
||||
},
|
||||
"./script_helpers": {
|
||||
"types": "./package/script_helpers.d.ts",
|
||||
"default": "./package/script_helpers.js"
|
||||
},
|
||||
"./infer": {
|
||||
"types": "./package/infer.d.ts",
|
||||
"default": "./package/infer.js"
|
||||
@@ -341,6 +349,11 @@
|
||||
"types": "./package/components/DropdownV2.svelte.d.ts",
|
||||
"svelte": "./package/components/DropdownV2.svelte",
|
||||
"default": "./package/components/DropdownV2.svelte"
|
||||
},
|
||||
"./components/flows/FlowHistoryInner.svelte": {
|
||||
"types": "./package/components/flows/FlowHistoryInner.svelte.d.ts",
|
||||
"svelte": "./package/components/flows/FlowHistoryInner.svelte",
|
||||
"default": "./package/components/flows/FlowHistoryInner.svelte"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -426,6 +439,9 @@
|
||||
"components/EditableSchemaWrapper.svelte": [
|
||||
"./package/components/schema/EditableSchemaWrapper.svelte.d.ts"
|
||||
],
|
||||
"components/flows/FlowHistoryInner.svelte": [
|
||||
"./package/components/flows/FlowHistoryInner.svelte.d.ts"
|
||||
],
|
||||
"utils": [
|
||||
"./package/utils.d.ts"
|
||||
],
|
||||
@@ -461,6 +477,12 @@
|
||||
],
|
||||
"components/DropdownV2.svelte": [
|
||||
"./package/components/DropdownV2.svelte.d.ts"
|
||||
],
|
||||
"script_helpers": [
|
||||
"./package/script_helpers.d.ts"
|
||||
],
|
||||
"icons/store": [
|
||||
"./package/icons/store.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -145,3 +145,23 @@ svelte-virtual-list-contents > * + * {
|
||||
rgba(255, 69, 58, 0.2) 20px
|
||||
);
|
||||
}
|
||||
|
||||
.bg-draggedover {
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(0, 0, 128, 0.2),
|
||||
rgba(0, 0, 192, 0.2) 10px,
|
||||
rgba(0, 0, 128, 0.2) 10px,
|
||||
rgba(0, 0, 192, 0.2) 20px
|
||||
);
|
||||
}
|
||||
|
||||
.bg-draggedover-dark {
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(0, 0, 128, 0.6),
|
||||
rgba(0, 0, 192, 0.6) 10px,
|
||||
rgba(0, 0, 128, 0.6) 10px,
|
||||
rgba(0, 0, 192, 0.6) 20px
|
||||
);
|
||||
}
|
||||
|
||||
+6
-10
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { base } from '$app/paths'
|
||||
|
||||
import AppConnectInner from '$lib/components/AppConnectInner.svelte'
|
||||
@@ -8,7 +7,10 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let resourceType = $page.url.searchParams.get('resource_type') ?? undefined
|
||||
export let resourceType: string | undefined = undefined
|
||||
export let workspace: string
|
||||
export let express = false
|
||||
|
||||
let step = 1
|
||||
let disabled = false
|
||||
let isGoogleSignin = false
|
||||
@@ -17,8 +19,6 @@
|
||||
let appConnect: AppConnectInner | undefined = undefined
|
||||
|
||||
let darkMode: boolean = false
|
||||
const workspace = $page.url.searchParams.get('workspace')
|
||||
const express = $page.url.searchParams.get('express') == 'true'
|
||||
|
||||
if (workspace) {
|
||||
$workspaceStore = workspace
|
||||
@@ -68,11 +68,7 @@
|
||||
bind:isGoogleSignin
|
||||
bind:disabled
|
||||
bind:manual
|
||||
on:error={(e) => {
|
||||
window?.parent?.postMessage({ type: 'error', error: e.detail }, '*')
|
||||
}}
|
||||
on:refresh={(e) => {
|
||||
window?.parent?.postMessage({ type: 'refresh', detail: e.detail }, '*')
|
||||
}}
|
||||
on:error
|
||||
on:refresh
|
||||
/>
|
||||
</div>
|
||||
@@ -89,6 +89,7 @@
|
||||
export let editor: SimpleEditor | undefined = undefined
|
||||
export let orderEditable = false
|
||||
export let shouldDispatchChanges: boolean = false
|
||||
export let noDefaultOnSelectFirst: boolean = false
|
||||
export let helperScript:
|
||||
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
|
||||
| { type: 'hash'; hash: string }
|
||||
@@ -537,11 +538,14 @@
|
||||
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format.split('-').length > 1 && resourceTypes.includes(format.substring('resource-'.length))))}
|
||||
<ObjectResourceInput
|
||||
{defaultValue}
|
||||
selectFirst
|
||||
selectFirst={!noDefaultOnSelectFirst}
|
||||
{disablePortal}
|
||||
{format}
|
||||
bind:value
|
||||
bind:editor
|
||||
on:clear={() => {
|
||||
defaultValue = null
|
||||
}}
|
||||
{showSchemaExplorer}
|
||||
/>
|
||||
{:else if inputCat == 'resource-object' && format.split('-').length > 1 && format
|
||||
@@ -807,7 +811,7 @@
|
||||
</div>
|
||||
{:else if inputCat == 'resource-string'}
|
||||
<ResourcePicker
|
||||
selectFirst
|
||||
selectFirst={noDefaultOnSelectFirst}
|
||||
{disablePortal}
|
||||
bind:value
|
||||
initialValue={defaultValue}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
{#if showTooltip && !disablePopup}
|
||||
<Portal>
|
||||
<Portal name="custom-popover">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
use:popperContent={popperOptions}
|
||||
|
||||
@@ -854,7 +854,7 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Portal>
|
||||
<Portal name="s3filepicker">
|
||||
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
|
||||
</Portal>
|
||||
{/if}
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
export let diffDrawer: DiffDrawer | undefined = undefined
|
||||
export let customUi: FlowBuilderWhitelabelCustomUi = {}
|
||||
export let disableAi: boolean = false
|
||||
export let disabledFlowInputs = false
|
||||
|
||||
$: setContext('customUi', customUi)
|
||||
|
||||
@@ -1333,6 +1334,7 @@
|
||||
<!-- metadata -->
|
||||
{#if $flowStateStore}
|
||||
<FlowEditor
|
||||
{disabledFlowInputs}
|
||||
disableAi={disableAi || customUi?.stepInputs?.ai == false}
|
||||
disableSettings={customUi?.settingsPanel === false}
|
||||
{loading}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
|
||||
export let schema: Schema | { [key: string]: unknown } | undefined
|
||||
</script>
|
||||
|
||||
<ul class="my-2">
|
||||
{#if schema == undefined}
|
||||
<li class="text-secondary text-xs italic mb-4">No inputs</li>
|
||||
{:else}
|
||||
{#each Object.entries(schema.properties ?? {}) as [inp, v]}
|
||||
<li class="list-disc flex flex-row items-center">
|
||||
<FieldHeader
|
||||
label={inp}
|
||||
required={Array.isArray(schema.required) && schema.required?.includes(inp)}
|
||||
type={v?.type}
|
||||
contentEncoding={v?.contentEncoding}
|
||||
format={v?.format}
|
||||
/><span class="ml-4 mt-1 text-xs"
|
||||
>{v?.default != undefined && v?.default != ''
|
||||
? 'default: ' + JSON.stringify(v?.default)
|
||||
: ''}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
@@ -26,7 +26,7 @@
|
||||
{#if job['success'] != undefined}
|
||||
Received job: {displayDate(job.created_at ?? '')}
|
||||
{:else}
|
||||
Received job <TimeAgo withDate date={job.created_at ?? ''} />
|
||||
Received job <TimeAgo date={job.created_at ?? ''} />
|
||||
{/if}
|
||||
<Tooltip small>{job?.created_at}</Tooltip>
|
||||
</span>
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="flex flex-row gap-2 items-center text-sm">
|
||||
<Clock size={SMALL_ICON_SIZE} class="text-secondary min-w-3.5" />
|
||||
<span class="whitespace-nowrap">
|
||||
Started <TimeAgo withDate agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
Started <TimeAgo agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
<Tooltip small>{job?.started_at}</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import type { FlowValue } from '$lib/gen'
|
||||
import { Tab, Tabs, TabContent } from './common'
|
||||
import SchemaViewer from './SchemaViewer.svelte'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
import FlowGraphViewer from './FlowGraphViewer.svelte'
|
||||
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import FlowViewerInner from './FlowViewerInner.svelte'
|
||||
import FlowInputViewer from './FlowInputViewer.svelte'
|
||||
|
||||
export let flow: {
|
||||
summary: string
|
||||
@@ -27,10 +27,6 @@
|
||||
if (initialOpen) {
|
||||
open[initialOpen] = true
|
||||
}
|
||||
|
||||
function toAny(x: unknown): any {
|
||||
return x as any
|
||||
}
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -54,23 +50,7 @@
|
||||
<span>Flow Input</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}
|
||||
/><span class="ml-4 mt-2 text-xs"
|
||||
>{toAny(v)?.default != undefined
|
||||
? 'default: ' + JSON.stringify(toAny(v)?.default)
|
||||
: ''}</span
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<FlowInputViewer schema={flow.schema} />
|
||||
{:else}
|
||||
<div class="text-secondary text-xs italic mb-4">No inputs</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { base } from '$lib/base'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Select from './apps/svelte-select/lib/index'
|
||||
@@ -8,7 +7,7 @@
|
||||
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import { Plus, Loader2 } from 'lucide-svelte'
|
||||
import type { AppViewerContext } from './apps/types'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
@@ -65,23 +64,6 @@
|
||||
refreshCount += 1
|
||||
open = true
|
||||
}
|
||||
|
||||
function processEvent(event: MessageEvent) {
|
||||
if (event.origin !== window.location.origin) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.data.type === 'error') {
|
||||
sendUserToast(event.data.error, true)
|
||||
}
|
||||
if (event.data.type === 'refresh') {
|
||||
window.removeEventListener('message', processEvent)
|
||||
value = event.data.detail
|
||||
valueSelect = { value, label: value }
|
||||
drawer?.closeDrawer?.()
|
||||
open = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
@@ -89,12 +71,15 @@
|
||||
{#if expressOAuthSetup}
|
||||
{#if open}
|
||||
{#key refreshCount}
|
||||
<iframe
|
||||
title="App connection"
|
||||
class="w-full h-full hidden"
|
||||
src="{base}/embed_connect?resource_type={resourceType}&workspace={appViewerContext?.workspace ??
|
||||
$workspaceStore}&express=true"
|
||||
/>
|
||||
{#await import('./AppConnectLightweightResourcePicker.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
workspace={appViewerContext?.workspace ?? $workspaceStore}
|
||||
{resourceType}
|
||||
express={true}
|
||||
/>
|
||||
{/await}
|
||||
{/key}
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -105,12 +90,31 @@
|
||||
tooltip="Resources represent connections to third party systems. Learn more on how to integrate external APIs."
|
||||
documentationLink="https://www.windmill.dev/docs/integrations/integrations_on_windmill"
|
||||
>
|
||||
<iframe
|
||||
{#await import('./AppConnectLightweightResourcePicker.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
workspace={appViewerContext?.workspace ?? $workspaceStore}
|
||||
{resourceType}
|
||||
express={false}
|
||||
on:error={(e) => {
|
||||
sendUserToast(e.detail, true)
|
||||
}}
|
||||
on:refresh={(e) => {
|
||||
value = e.detail
|
||||
valueSelect = { value, label: value }
|
||||
drawer?.closeDrawer?.()
|
||||
open = false
|
||||
}}
|
||||
/>
|
||||
{/await}
|
||||
|
||||
<!-- <iframe
|
||||
title="App connection"
|
||||
class="w-full h-full"
|
||||
src="{base}/embed_connect?resource_type={resourceType}&workspace={appViewerContext?.workspace ??
|
||||
$workspaceStore}&express=false"
|
||||
/>
|
||||
/> -->
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{/if}
|
||||
@@ -142,13 +146,11 @@
|
||||
<Button
|
||||
{disabled}
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
btnClasses="w-8 px-0.5 py-1.5"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
open = true
|
||||
window.removeEventListener('message', processEvent)
|
||||
window.addEventListener('message', processEvent)
|
||||
|
||||
drawer?.openDrawer?.()
|
||||
}}
|
||||
startIcon={{ icon: Plus }}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<slot />
|
||||
</fragment>
|
||||
{#if showTooltip}
|
||||
<Portal>
|
||||
<Portal name="manual-popover">
|
||||
<div
|
||||
use:popperContent
|
||||
class={twMerge('z-[901] rounded-lg shadow-md border p-4 bg-surface', $$props.class)}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
export let selectFirst = false
|
||||
export let defaultValue: any
|
||||
export let editor: SimpleEditor | undefined = undefined
|
||||
|
||||
function isString(value: any) {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
}
|
||||
@@ -19,7 +18,7 @@
|
||||
export let path: string = ''
|
||||
|
||||
function resourceToValue() {
|
||||
if (path) {
|
||||
if (path && path != '') {
|
||||
value = `$res:${path}`
|
||||
} else {
|
||||
value = undefined
|
||||
@@ -50,8 +49,11 @@
|
||||
path = e.detail
|
||||
resourceToValue()
|
||||
}}
|
||||
on:clear
|
||||
bind:value={path}
|
||||
initialValue={defaultValue}
|
||||
initialValue={typeof defaultValue == 'string' && defaultValue.startsWith('$res:')
|
||||
? defaultValue.substr('$res:'.length)
|
||||
: defaultValue}
|
||||
resourceType={format.split('-').length > 1 ? format.substring('resource-'.length) : undefined}
|
||||
{showSchemaExplorer}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
{#if showTooltip && !disablePopup}
|
||||
<Portal>
|
||||
<Portal name="popover">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
use:popperContent={popperOptions}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<script context="module">
|
||||
import { tick } from 'svelte'
|
||||
|
||||
/**
|
||||
* Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}>
|
||||
*
|
||||
* @param {HTMLElement} el
|
||||
* @param {HTMLElement|string} target DOM Element or CSS Selector
|
||||
*/
|
||||
export function portal(el, target = 'body') {
|
||||
export function portal(el, options) {
|
||||
let { target, name } = options
|
||||
let targetEl
|
||||
async function update(newTarget) {
|
||||
target = newTarget
|
||||
@@ -32,6 +27,9 @@
|
||||
if (!el.classList.contains('windmill-app')) {
|
||||
el.classList.add('windmill-app')
|
||||
}
|
||||
if (name && !el.classList.contains(name)) {
|
||||
el.classList.add(name)
|
||||
}
|
||||
targetEl.appendChild(el)
|
||||
el.hidden = false
|
||||
}
|
||||
@@ -56,8 +54,9 @@
|
||||
* @type { HTMLElement|string}
|
||||
*/
|
||||
export let target = 'body'
|
||||
export let name = undefined
|
||||
</script>
|
||||
|
||||
<div use:portal={target} hidden>
|
||||
<div use:portal={{ target, name }} hidden>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
sendUserToast(`Updated resource at ${path}`)
|
||||
dispatch('refresh', path)
|
||||
} else {
|
||||
throw Error('Cannot edit undefined resourceToEdit')
|
||||
throw Error('Cannot edit undefined resource')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,11 +231,10 @@
|
||||
<Skeleton layout={[[4]]} />
|
||||
{:else if !viewJsonSchema && resourceSchema && resourceSchema?.properties}
|
||||
{#if resourceTypeInfo?.format_extension}
|
||||
<h5 class="mt-4 inline-flex items-center gap-4 pb-2">
|
||||
File content ({resourceTypeInfo.format_extension})
|
||||
</h5>
|
||||
<h5 class="mt-4 inline-flex items-center gap-4 pb-2">
|
||||
File content ({resourceTypeInfo.format_extension})
|
||||
</h5>
|
||||
<div class="h-full w-full border p-1 rounded">
|
||||
|
||||
<SimpleEditor
|
||||
autoHeight
|
||||
class="editor"
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full items-start">
|
||||
<div class="flex flex-row gap-x-1 w-full items-center">
|
||||
<div class="flex flex-row w-full items-center">
|
||||
{#if collection?.length > 0}
|
||||
<Select
|
||||
{disabled}
|
||||
@@ -128,9 +128,11 @@
|
||||
valueSelect = e.detail
|
||||
}}
|
||||
on:clear={() => {
|
||||
initialValue = undefined
|
||||
value = undefined
|
||||
valueType = undefined
|
||||
valueSelect = undefined
|
||||
dispatch('clear')
|
||||
}}
|
||||
items={collection}
|
||||
class="text-clip grow min-w-0"
|
||||
@@ -143,14 +145,15 @@
|
||||
{:else if !loading}
|
||||
<div class="text-2xs text-tertiary mr-2">0 found</div>
|
||||
{/if}
|
||||
|
||||
{#if !loading}
|
||||
<div class="mx-0.5" />
|
||||
{#if value && value != ''}
|
||||
<Button
|
||||
{disabled}
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
size="sm"
|
||||
btnClasses="w-8 px-0.5 py-1.5"
|
||||
on:click={() => resourceEditor?.initEdit?.(value ?? '')}
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly
|
||||
@@ -162,8 +165,9 @@
|
||||
<Button
|
||||
{disabled}
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
size="sm"
|
||||
btnClasses="w-8 px-0.5 py-1.5"
|
||||
on:click={() => appConnect?.open?.(rt)}
|
||||
startIcon={{ icon: Plus }}>{rt}</Button
|
||||
>
|
||||
@@ -172,8 +176,9 @@
|
||||
<Button
|
||||
{disabled}
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
size="sm"
|
||||
btnClasses="w-8 px-0.5 py-1.5"
|
||||
on:click={() => appConnect?.open?.(resourceType, expressOAuthSetup)}
|
||||
startIcon={{ icon: Plus }}
|
||||
iconOnly={collection?.length > 0}
|
||||
@@ -185,9 +190,10 @@
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
variant="border"
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="w-8 px-0.5 py-1.5"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
loadResources(resourceType)
|
||||
}}
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-tertiary">
|
||||
{#if runnable}
|
||||
Edited <TimeAgo withDate agoOnlyIfRecent date={runnable.created_at || ''} /> by {runnable.created_by ||
|
||||
Edited <TimeAgo agoOnlyIfRecent date={runnable.created_at || ''} /> by {runnable.created_by ||
|
||||
'unknown'}
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
@@ -364,7 +364,7 @@
|
||||
<div
|
||||
class="whitespace-nowrap col-span-3 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
|
||||
>
|
||||
<TimeAgo date={i.started_at ?? ''} />
|
||||
<TimeAgo noDate date={i.started_at ?? ''} />
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<a
|
||||
@@ -425,7 +425,7 @@
|
||||
<div
|
||||
class="whitespace-nowrap col-span-2 !text-tertiary !text-2xs overflow-hidden text-ellipsis flex-shrink text-center"
|
||||
>
|
||||
<TimeAgo date={i.created_at ?? ''} />
|
||||
<TimeAgo noDate date={i.created_at ?? ''} />
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<a
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import PanelSection from './apps/editor/settingsPanel/common/PanelSection.svelte'
|
||||
import { classNames, emptyString } from '$lib/utils'
|
||||
import { ScriptService, type ScriptHistory } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -63,70 +62,66 @@
|
||||
|
||||
<Splitpanes class="!overflow-visible">
|
||||
<Pane size={20}>
|
||||
<PanelSection title="Past Versions">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if !loading}
|
||||
{#if versions && versions.length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each versions ?? [] as version, versionIndex}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer ',
|
||||
selectedVersion?.script_hash == version.script_hash
|
||||
? 'bg-surface-selected'
|
||||
: '',
|
||||
'hover:bg-surface-hover'
|
||||
)}
|
||||
on:click={() => {
|
||||
selectedVersion = version
|
||||
selectedVersionIndex = versionIndex
|
||||
<div class="flex flex-col gap-2 px-2 pt-2 w-full">
|
||||
{#if !loading}
|
||||
{#if versions && versions.length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each versions ?? [] as version, versionIndex}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer ',
|
||||
selectedVersion?.script_hash == version.script_hash ? 'bg-surface-selected' : '',
|
||||
'hover:bg-surface-hover'
|
||||
)}
|
||||
on:click={() => {
|
||||
selectedVersion = version
|
||||
selectedVersionIndex = versionIndex
|
||||
|
||||
if (showDiff && versions && selectedVersionIndex === versions.length - 1) {
|
||||
showDiff = false
|
||||
}
|
||||
if (showDiff && versions && selectedVersionIndex === versions.length - 1) {
|
||||
showDiff = false
|
||||
}
|
||||
|
||||
const availableVersions = versions?.slice(selectedVersionIndex + 1)
|
||||
const availableVersions = versions?.slice(selectedVersionIndex + 1)
|
||||
|
||||
if (
|
||||
previousHash &&
|
||||
!availableVersions?.find((v) => v.script_hash === previousHash)
|
||||
) {
|
||||
previousHash = availableVersions?.[0]?.script_hash
|
||||
}
|
||||
if (
|
||||
previousHash &&
|
||||
!availableVersions?.find((v) => v.script_hash === previousHash)
|
||||
) {
|
||||
previousHash = availableVersions?.[0]?.script_hash
|
||||
}
|
||||
|
||||
deploymentMsgUpdate = undefined
|
||||
deploymentMsgUpdateMode = false
|
||||
}}
|
||||
>
|
||||
<span class="text-xs truncate">
|
||||
{#if emptyString(version.deployment_msg)}Version {version.script_hash}{:else}{version.deployment_msg}{/if}
|
||||
</span>
|
||||
{#if openDetails}
|
||||
<Button
|
||||
on:click={() => {
|
||||
dispatch('openDetails', { version: version.script_hash })
|
||||
}}
|
||||
class="ml-2 inline-flex gap-1 text-xs items-center"
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
>
|
||||
Run page<ExternalLink size={14} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-tertiary">No items</div>
|
||||
{/if}
|
||||
deploymentMsgUpdate = undefined
|
||||
deploymentMsgUpdateMode = false
|
||||
}}
|
||||
>
|
||||
<span class="text-xs truncate">
|
||||
{#if emptyString(version.deployment_msg)}Version {version.script_hash}{:else}{version.deployment_msg}{/if}
|
||||
</span>
|
||||
{#if openDetails}
|
||||
<Button
|
||||
on:click={() => {
|
||||
dispatch('openDetails', { version: version.script_hash })
|
||||
}}
|
||||
class="ml-2 inline-flex gap-1 text-xs items-center"
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
>
|
||||
Run page<ExternalLink size={14} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
|
||||
<div class="text-sm text-tertiary">No items</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PanelSection>
|
||||
{:else}
|
||||
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={80}>
|
||||
<div class="h-full w-full overflow-auto">
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
export let date: string
|
||||
export let withDate: boolean = false
|
||||
export let agoOnlyIfRecent: boolean = false
|
||||
export let noDate = false
|
||||
|
||||
let computedTimeAgo: string | undefined = undefined
|
||||
|
||||
@@ -63,21 +63,20 @@
|
||||
let dAgo = daysAgo(date)
|
||||
if (dAgo == 0) {
|
||||
return `yesterday at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
} else if (dAgo > 7 && withDate) {
|
||||
} else if (dAgo > 7 && !noDate) {
|
||||
return `${dAgo + 1} days ago at ${date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`
|
||||
} else {
|
||||
return !withDate ? displayDate(dateString, false, withDate) : ''
|
||||
return displayDate(dateString, false, !noDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if withDate}
|
||||
{displayDate(date)}
|
||||
{/if}
|
||||
{#if computedTimeAgo && (!agoOnlyIfRecent || isRecent)}
|
||||
{computedTimeAgo}
|
||||
{:else}
|
||||
{displayDate(date)}
|
||||
{/if}
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
</RunnableWrapper>
|
||||
|
||||
{#if resolvedConfig?.confirmationModal?.selected === 'confirmationModal'}
|
||||
<Portal target="#app-editor-top-level-drawer">
|
||||
<Portal name="app-button" target="#app-editor-top-level-drawer">
|
||||
<ConfirmationModal
|
||||
open={Boolean(confirmedCallback)}
|
||||
title={resolvedConfig?.confirmationModal?.configuration?.confirmationModal?.title ?? ''}
|
||||
|
||||
@@ -695,7 +695,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
<Portal>
|
||||
<Portal name="db-explorer">
|
||||
<Drawer bind:this={insertDrawer} size="800px">
|
||||
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
{option}
|
||||
</div>
|
||||
</MultiSelect>
|
||||
<Portal>
|
||||
<Portal name="app-multiselect">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
{option}
|
||||
</div>
|
||||
</MultiSelect>
|
||||
<Portal>
|
||||
<Portal name="app-multiselect-v2">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
</AlignWrapper>
|
||||
</div>
|
||||
|
||||
<Portal target="#app-editor-top-level-drawer">
|
||||
<Portal target="#app-editor-top-level-drawer" name="app-drawer">
|
||||
<Drawer
|
||||
let:open
|
||||
bind:this={appDrawer}
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
</AlignWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
<Portal target="#app-editor-top-level-drawer">
|
||||
<Portal target="#app-editor-top-level-drawer" name="app-modal">
|
||||
<Disposable
|
||||
{id}
|
||||
let:handleClickAway
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
import AppEditorHeader from './AppEditorHeader.svelte'
|
||||
import GridEditor from './GridEditor.svelte'
|
||||
|
||||
import { Button, Tab } from '$lib/components/common'
|
||||
import { Alert, Button, Tab } from '$lib/components/common'
|
||||
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { classNames, encodeState, sendUserToast } from '$lib/utils'
|
||||
import { classNames, encodeState, getModifierKey, sendUserToast } from '$lib/utils'
|
||||
import AppPreview from './AppPreview.svelte'
|
||||
import ComponentList from './componentsPanel/ComponentList.svelte'
|
||||
import ContextPanel from './contextPanel/ContextPanel.svelte'
|
||||
@@ -693,7 +693,15 @@
|
||||
{#if $componentActive}
|
||||
<div
|
||||
class="absolute z-50 inset-0 h-full w-full bg-surface-secondary [background-size:16px_16px]"
|
||||
/>
|
||||
>
|
||||
<div class="w-min whitespace-nowrap mx-auto pt-0.5 z-50">
|
||||
<Alert
|
||||
title={`Press ${getModifierKey()} to drop component inside a container.`}
|
||||
size="xs"
|
||||
class="h-10 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<SplitPanesWrapper>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
|
||||
import Component from './component/Component.svelte'
|
||||
import { push } from '$lib/history'
|
||||
import { dfs, expandGriditem, findGridItem } from './appUtils'
|
||||
import {
|
||||
dfs,
|
||||
expandGriditem,
|
||||
findGridItem,
|
||||
findGridItemParentGrid,
|
||||
insertNewGridItem,
|
||||
isContainer,
|
||||
subGridIndexKey
|
||||
} from './appUtils'
|
||||
import Grid from '../svelte-grid/Grid.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import ComponentWrapper from './component/ComponentWrapper.svelte'
|
||||
@@ -32,7 +40,8 @@
|
||||
parentWidth,
|
||||
breakpoint,
|
||||
allIdsInPath,
|
||||
bgRuns
|
||||
bgRuns,
|
||||
worldStore
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { history, scale, componentActive } = getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -61,6 +70,53 @@
|
||||
}
|
||||
$app = $app
|
||||
}
|
||||
|
||||
export function moveComponentBetweenSubgrids(
|
||||
componentId: string,
|
||||
parentComponentId: string,
|
||||
subGridIndex: number,
|
||||
position?: { x: number; y: number }
|
||||
) {
|
||||
// Find the component in the source subgrid
|
||||
const component = findGridItem($app, componentId)
|
||||
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
let parentGrid = findGridItemParentGrid($app, component.id)
|
||||
if (parentGrid) {
|
||||
$app.subgrids &&
|
||||
($app.subgrids[parentGrid] = $app.subgrids[parentGrid].filter(
|
||||
(item) => item.id !== component?.id
|
||||
))
|
||||
} else {
|
||||
$app.grid = $app.grid.filter((item) => item.id !== component?.id)
|
||||
}
|
||||
|
||||
const gridItem = component
|
||||
insertNewGridItem(
|
||||
$app,
|
||||
(id) => ({ ...gridItem.data, id }),
|
||||
{ parentComponentId: parentComponentId, subGridIndex: subGridIndex },
|
||||
Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])),
|
||||
component.id,
|
||||
position,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
// Update the app state
|
||||
$app = { ...$app }
|
||||
|
||||
$selectedComponent = [parentComponentId]
|
||||
$focusedGrid = {
|
||||
parentComponentId,
|
||||
subGridIndex
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full z-[1000] overflow-visible h-full">
|
||||
@@ -134,10 +190,37 @@
|
||||
push(history, $app)
|
||||
$app.grid = e.detail
|
||||
}}
|
||||
root
|
||||
let:dataItem
|
||||
let:hidden
|
||||
let:overlapped
|
||||
let:moveMode
|
||||
let:componentDraggedId
|
||||
cols={columnConfiguration}
|
||||
on:dropped={(e) => {
|
||||
const { id, overlapped, x, y } = e.detail
|
||||
|
||||
const overlappedComponent = findGridItem($app, overlapped)
|
||||
|
||||
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
if (id === overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
moveComponentBetweenSubgrids(
|
||||
id,
|
||||
overlapped,
|
||||
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
|
||||
{ x, y }
|
||||
)
|
||||
}}
|
||||
>
|
||||
<ComponentWrapper
|
||||
id={dataItem.id}
|
||||
@@ -177,7 +260,9 @@
|
||||
on:fillHeight={() => {
|
||||
handleFillHeight(dataItem.id)
|
||||
}}
|
||||
overlapped={overlapped !== undefined}
|
||||
{overlapped}
|
||||
{moveMode}
|
||||
{componentDraggedId}
|
||||
/>
|
||||
</GridEditorMenu>
|
||||
</ComponentWrapper>
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
<slot />
|
||||
|
||||
{#if contextMenuVisible}
|
||||
<Portal>
|
||||
<Portal name="grid-editor">
|
||||
<div style="position: fixed; top: {menuY}px; left: {menuX}px; z-index:6000;">
|
||||
<div class="rounded-md bg-surface border shadow-md divide-y w-64">
|
||||
<div class="p-1" use:clickOutside={false}>
|
||||
|
||||
@@ -3,10 +3,19 @@
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import Grid from '../svelte-grid/Grid.svelte'
|
||||
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
|
||||
import { expandGriditem, findGridItem, maxHeight, selectId } from './appUtils'
|
||||
import {
|
||||
expandGriditem,
|
||||
findGridItem,
|
||||
findGridItemParentGrid,
|
||||
insertNewGridItem,
|
||||
isContainer,
|
||||
maxHeight,
|
||||
selectId,
|
||||
subGridIndexKey
|
||||
} from './appUtils'
|
||||
import Component from './component/Component.svelte'
|
||||
import ComponentWrapper from './component/ComponentWrapper.svelte'
|
||||
import GridViewer from './GridViewer.svelte'
|
||||
@@ -15,6 +24,7 @@
|
||||
export let containerHeight: number | undefined = undefined
|
||||
export let containerWidth: number | undefined = undefined
|
||||
let classes = ''
|
||||
|
||||
export { classes as class }
|
||||
export let style = ''
|
||||
export let noPadding = false
|
||||
@@ -34,7 +44,8 @@
|
||||
mode,
|
||||
parentWidth,
|
||||
breakpoint,
|
||||
allIdsInPath
|
||||
allIdsInPath,
|
||||
worldStore
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const editorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -68,6 +79,98 @@
|
||||
let container: HTMLElement | undefined = undefined
|
||||
|
||||
$: maxRow = maxHeight($app.subgrids?.[subGridId] ?? [], containerHeight ?? 0, $breakpoint)
|
||||
|
||||
export function moveComponentBetweenSubgrids(
|
||||
componentId: string,
|
||||
parentComponentId: string,
|
||||
subGridIndex: number,
|
||||
position?: { x: number; y: number }
|
||||
) {
|
||||
// Find the component in the source subgrid
|
||||
const component = findGridItem($app, componentId)
|
||||
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
let parentGrid = findGridItemParentGrid($app, component.id)
|
||||
|
||||
if (parentGrid) {
|
||||
$app.subgrids &&
|
||||
($app.subgrids[parentGrid] = $app.subgrids[parentGrid].filter(
|
||||
(item) => item.id !== component?.id
|
||||
))
|
||||
} else {
|
||||
$app.grid = $app.grid.filter((item) => item.id !== component?.id)
|
||||
}
|
||||
|
||||
const gridItem = component
|
||||
|
||||
insertNewGridItem(
|
||||
$app,
|
||||
(id) => ({ ...gridItem.data, id }),
|
||||
{ parentComponentId: parentComponentId, subGridIndex: subGridIndex },
|
||||
Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])),
|
||||
component.id,
|
||||
position,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
// Update the app state
|
||||
$app = { ...$app }
|
||||
|
||||
if (parentGrid) {
|
||||
$focusedGrid = {
|
||||
parentComponentId,
|
||||
subGridIndex
|
||||
}
|
||||
|
||||
$selectedComponent = [parentComponentId]
|
||||
} else {
|
||||
$focusedGrid = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function moveToRoot(componentId: string, position?: { x: number; y: number }) {
|
||||
// Find the component in the source subgrid
|
||||
const component = findGridItem($app, componentId)
|
||||
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
let parentGrid = findGridItemParentGrid($app, component.id)
|
||||
|
||||
if (parentGrid) {
|
||||
$app.subgrids &&
|
||||
($app.subgrids[parentGrid] = $app.subgrids[parentGrid].filter(
|
||||
(item) => item.id !== component?.id
|
||||
))
|
||||
} else {
|
||||
$app.grid = $app.grid.filter((item) => item.id !== component?.id)
|
||||
}
|
||||
|
||||
const gridItem = component
|
||||
|
||||
insertNewGridItem(
|
||||
$app,
|
||||
(id) => ({ ...gridItem.data, id }),
|
||||
undefined,
|
||||
Object.fromEntries(gridColumns.map((column) => [column, gridItem[column]])),
|
||||
component.id,
|
||||
position,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
// Update the app state
|
||||
$app = { ...$app }
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -109,10 +212,39 @@
|
||||
let:dataItem
|
||||
let:hidden
|
||||
let:overlapped
|
||||
let:moveMode
|
||||
let:componentDraggedId
|
||||
cols={columnConfiguration}
|
||||
scroller={container}
|
||||
parentWidth={$parentWidth - 17}
|
||||
{containerWidth}
|
||||
on:dropped={(e) => {
|
||||
const { id, overlapped, x, y } = e.detail
|
||||
|
||||
if (!overlapped) {
|
||||
moveToRoot(id, { x, y })
|
||||
} else {
|
||||
const overlappedComponent = findGridItem($app, overlapped)
|
||||
|
||||
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
|
||||
return
|
||||
}
|
||||
if (!overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
if (id === overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
moveComponentBetweenSubgrids(
|
||||
id,
|
||||
overlapped,
|
||||
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
|
||||
{ x, y }
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ComponentWrapper
|
||||
id={dataItem.id}
|
||||
@@ -160,6 +292,8 @@
|
||||
}
|
||||
$app = $app
|
||||
}}
|
||||
{moveMode}
|
||||
{componentDraggedId}
|
||||
/>
|
||||
</GridEditorMenu>
|
||||
</ComponentWrapper>
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
import { gridColumns } from '../gridUtils'
|
||||
import { allItems } from '../utils'
|
||||
import type { Output, World } from '../rx'
|
||||
import gridHelp from '../svelte-grid/utils/helper'
|
||||
import type { FilledItem, Size } from '../svelte-grid/types'
|
||||
import type {
|
||||
StaticAppInput,
|
||||
@@ -35,6 +34,7 @@ import { deepMergeWithPriority } from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getNextId } from '$lib/components/flows/idUtils'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import gridHelp from '../svelte-grid/utils/helper'
|
||||
|
||||
export function findComponentSettings(app: App, id: string | undefined) {
|
||||
if (!id) return undefined
|
||||
@@ -245,7 +245,8 @@ export function createNewGridItem(
|
||||
columns?: Record<number, any>,
|
||||
initialPosition: { x: number; y: number } = { x: 0, y: 0 },
|
||||
recOverride?: Record<number, Size>,
|
||||
fixed?: boolean
|
||||
fixed?: boolean,
|
||||
shouldNotFindSpace?: boolean
|
||||
): GridItem {
|
||||
const newComponent = {
|
||||
fixed: fixed ?? false,
|
||||
@@ -272,11 +273,28 @@ export function createNewGridItem(
|
||||
h: rec.h
|
||||
}
|
||||
} else {
|
||||
newItem[column] = columns[column]
|
||||
newItem[column] = {
|
||||
...columns[column],
|
||||
x: initialPosition.x,
|
||||
y: initialPosition.y
|
||||
}
|
||||
}
|
||||
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
|
||||
|
||||
newItem[column] = { ...newItem[column], ...position }
|
||||
let shouldComputePosition: boolean = false
|
||||
|
||||
// Fallback to avoid component disapearing
|
||||
if (initialPosition.x === undefined || initialPosition.y === undefined) {
|
||||
newItem[column].x = 0
|
||||
newItem[column].y = 0
|
||||
shouldComputePosition = true
|
||||
}
|
||||
|
||||
// Either the final position is controlled using initialPosition or the position is computed because the component positions are wrong
|
||||
if (!shouldNotFindSpace || shouldComputePosition) {
|
||||
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
|
||||
|
||||
newItem[column] = { ...newItem[column], ...position }
|
||||
}
|
||||
})
|
||||
|
||||
return newItem
|
||||
@@ -392,7 +410,8 @@ export function insertNewGridItem(
|
||||
initialPosition: { x: number; y: number } = { x: 0, y: 0 },
|
||||
recOverride?: Record<number, Size>,
|
||||
keepSubgrids?: boolean,
|
||||
fixed?: boolean
|
||||
fixed?: boolean,
|
||||
shouldNotFindSpace?: boolean
|
||||
): string {
|
||||
const id = keepId ?? getNextGridItemId(app)
|
||||
|
||||
@@ -439,7 +458,16 @@ export function insertNewGridItem(
|
||||
|
||||
let grid = focusedGrid ? app.subgrids[key!] : app.grid
|
||||
|
||||
const newItem = createNewGridItem(grid, id, data, columns, initialPosition, recOverride, fixed)
|
||||
const newItem = createNewGridItem(
|
||||
grid,
|
||||
id,
|
||||
data,
|
||||
columns,
|
||||
initialPosition,
|
||||
recOverride,
|
||||
fixed,
|
||||
shouldNotFindSpace
|
||||
)
|
||||
grid.push(newItem)
|
||||
return id
|
||||
}
|
||||
@@ -1135,3 +1163,120 @@ export function setUpTopBarComponentContent(id: string, app: App) {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function isContainer(type: string): boolean {
|
||||
return (
|
||||
type === 'containercomponent' ||
|
||||
type === 'tabscomponent' ||
|
||||
type === 'verticalsplitpanescomponent' ||
|
||||
type === 'horizontalsplitpanescomponent' ||
|
||||
type === 'steppercomponent' ||
|
||||
type === 'listcomponent' ||
|
||||
type === 'decisiontreecomponent'
|
||||
)
|
||||
}
|
||||
|
||||
export function subGridIndexKey(type: string | undefined, id: string, world: World): number {
|
||||
switch (type) {
|
||||
case 'containercomponent':
|
||||
case 'verticalsplitpanescomponent':
|
||||
case 'horizontalsplitpanescomponent':
|
||||
case 'listcomponent':
|
||||
return 0
|
||||
case 'tabscomponent': {
|
||||
return (world?.outputsById?.[id]?.selectedTabIndex?.peak() as number) ?? 0
|
||||
}
|
||||
case 'steppercomponent': {
|
||||
return (world?.outputsById?.[id]?.currentStepIndex?.peak() as number) ?? 0
|
||||
}
|
||||
case 'decisiontreecomponent': {
|
||||
return (world?.outputsById?.[id]?.currentNodeIndex?.peak() as number) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export function computePosition(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
xPerPx: number,
|
||||
yPerPx: number,
|
||||
overlapped?: string,
|
||||
element?: HTMLElement
|
||||
) {
|
||||
const overlappedElement = overlapped
|
||||
? document.getElementById(`component-${overlapped}`)
|
||||
: document.getElementById('root-grid')
|
||||
|
||||
const xRelativeToElement = element ? clientX - element.getBoundingClientRect().left : 0
|
||||
const yRelativeToElement = element ? clientY - element.getBoundingClientRect().top : 0
|
||||
|
||||
const xRelativeToOverlappedElement = overlappedElement
|
||||
? clientX - overlappedElement.getBoundingClientRect().left - xRelativeToElement
|
||||
: 0
|
||||
const yRelativeToOverlappedElement = overlappedElement
|
||||
? clientY - overlappedElement.getBoundingClientRect().top - yRelativeToElement
|
||||
: 0
|
||||
|
||||
const gridX = Math.max(Math.round(xRelativeToOverlappedElement / xPerPx) ?? 0, 0)
|
||||
const gridY = Math.max(Math.round(yRelativeToOverlappedElement / yPerPx) ?? 0, 0)
|
||||
|
||||
return {
|
||||
x: gridX,
|
||||
y: gridY
|
||||
}
|
||||
}
|
||||
|
||||
export function getDeltaYByComponent(type: string) {
|
||||
switch (type) {
|
||||
case 'steppercomponent': {
|
||||
return '36px + 0.5rem'
|
||||
}
|
||||
case 'tabscomponent': {
|
||||
return '32px'
|
||||
}
|
||||
default:
|
||||
return '0px'
|
||||
}
|
||||
}
|
||||
|
||||
export function getDeltaXByComponent(type: string) {
|
||||
switch (type) {
|
||||
case 'steppercomponent': {
|
||||
return '0.5rem'
|
||||
}
|
||||
case 'tabscomponent': {
|
||||
return '0px'
|
||||
}
|
||||
default:
|
||||
return '0px'
|
||||
}
|
||||
}
|
||||
|
||||
export type GridShadow = {
|
||||
x: number
|
||||
y: number
|
||||
xPerPx: number
|
||||
yPerPx: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export function areShadowsTheSame(
|
||||
shadow1: GridShadow | undefined,
|
||||
shadow2: GridShadow | undefined
|
||||
) {
|
||||
if (!shadow1 || !shadow2) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
shadow1.x === shadow2.x &&
|
||||
shadow1.y === shadow2.y &&
|
||||
shadow1.xPerPx === shadow2.xPerPx &&
|
||||
shadow1.yPerPx === shadow2.yPerPx &&
|
||||
shadow1.w === shadow2.w &&
|
||||
shadow1.h === shadow2.h
|
||||
)
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
import AppRecomputeAll from '../../components/display/AppRecomputeAll.svelte'
|
||||
import AppUserResource from '../../components/inputs/AppUserResource.svelte'
|
||||
import { Anchor } from 'lucide-svelte'
|
||||
import { findGridItemParentGrid, isContainer } from '../appUtils'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -88,9 +89,10 @@
|
||||
export let render: boolean
|
||||
export let hidden: boolean
|
||||
export let fullHeight: boolean
|
||||
export let overlapped: boolean = false
|
||||
|
||||
const { mode, app, hoverStore, connectingInput, selectedComponent } =
|
||||
export let overlapped: string | undefined = undefined
|
||||
export let moveMode: string | undefined = undefined
|
||||
export let componentDraggedId: string | undefined = undefined
|
||||
const { mode, app, hoverStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const editorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
@@ -121,6 +123,36 @@
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function componentDraggedIsNotChild(componentDraggedId: string, componentId: string) {
|
||||
let parentGrid = findGridItemParentGrid($app, componentDraggedId)
|
||||
|
||||
return !parentGrid?.startsWith(`${componentId}-`)
|
||||
}
|
||||
|
||||
function areOnTheSameSubgrid(componentDraggedId: string, componentId: string) {
|
||||
return (
|
||||
findGridItemParentGrid($app, componentDraggedId) === findGridItemParentGrid($app, componentId)
|
||||
)
|
||||
}
|
||||
|
||||
let cachedComponentDraggedIsNotChild: boolean | undefined
|
||||
let cachedAreOnTheSameSubgrid: boolean | undefined
|
||||
|
||||
function updateCache(componentDraggedId: string | undefined) {
|
||||
if (componentDraggedId) {
|
||||
cachedComponentDraggedIsNotChild = componentDraggedIsNotChild(
|
||||
componentDraggedId,
|
||||
component.id
|
||||
)
|
||||
cachedAreOnTheSameSubgrid = areOnTheSameSubgrid(componentDraggedId, component.id)
|
||||
} else {
|
||||
cachedComponentDraggedIsNotChild = undefined
|
||||
cachedAreOnTheSameSubgrid = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$: updateCache(componentDraggedId)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
@@ -139,18 +171,23 @@
|
||||
hidden && $mode === 'preview' ? 'hidden' : ''
|
||||
)}
|
||||
>
|
||||
{#if locked && componentActive && $componentActive && $selectedComponent?.[0] !== component.id && overlapped}
|
||||
{#if locked && componentActive && $componentActive && moveMode === 'move' && componentDraggedId && componentDraggedId !== component.id && cachedAreOnTheSameSubgrid}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 bg-locked center-center flex-col z-50',
|
||||
overlapped ? 'bg-locked-hover' : ''
|
||||
)}
|
||||
class={twMerge('absolute inset-0 bg-locked center-center flex-col z-50', 'bg-locked-hover')}
|
||||
>
|
||||
<div class="bg-surface p-2 shadow-sm rounded-md flex center-center flex-col gap-2">
|
||||
<Anchor size={24} class="text-primary " />
|
||||
<div class="text-xs">Anchored: The component cannot be moved</div>
|
||||
<div class="text-xs"> Anchored: The component cannot be moved. </div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if moveMode === 'insert' && isContainer(component.type) && componentDraggedId && componentDraggedId !== component.id && cachedComponentDraggedIsNotChild}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
|
||||
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
|
||||
overlapped === component?.id ? 'bg-draggedover dark:bg-draggedover-dark' : ''
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
{#if $mode !== 'preview'}
|
||||
<ComponentHeader
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
const componentDraggedIdStore = writable<string | undefined>(undefined)
|
||||
const componentDraggedParentIdStore = writable<string | undefined>(undefined)
|
||||
const overlappedStore = writable<string | undefined>(undefined)
|
||||
const fakeShadowStore = writable<GridShadow | undefined>(undefined)
|
||||
const isCtrlOrMetaPressedStore = writable<boolean>(false)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import gridHelp from './utils/helper'
|
||||
import type { AppViewerContext, GridItem } from '../types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { getContainerHeight } from './utils/container'
|
||||
import { moveItem, getItemById, specifyUndefinedColumns } from './utils/item'
|
||||
import { onMount, createEventDispatcher } from 'svelte'
|
||||
import { onMount, createEventDispatcher, getContext } from 'svelte'
|
||||
import { getColumn, throttle } from './utils/other'
|
||||
import MoveResize from './MoveResize.svelte'
|
||||
import type { FilledItem } from './types'
|
||||
import { ROW_GAP_X, ROW_GAP_Y, ROW_HEIGHT, sortGridItemsPosition } from '../editor/appUtils'
|
||||
import {
|
||||
areShadowsTheSame,
|
||||
findGridItemParentGrid,
|
||||
getDeltaXByComponent,
|
||||
getDeltaYByComponent,
|
||||
isContainer,
|
||||
ROW_GAP_X,
|
||||
ROW_GAP_Y,
|
||||
ROW_HEIGHT,
|
||||
sortGridItemsPosition,
|
||||
subGridIndexKey,
|
||||
type GridShadow
|
||||
} from '../editor/appUtils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
type T = $$Generic
|
||||
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let items: FilledItem<T>[]
|
||||
export let rowHeight: number = ROW_HEIGHT
|
||||
export let cols: [number, number][]
|
||||
@@ -22,11 +50,10 @@
|
||||
export let containerWidth: number | undefined = undefined
|
||||
export let scroller: HTMLElement | undefined = undefined
|
||||
export let sensor = 20
|
||||
|
||||
export let root: boolean = false
|
||||
export let parentWidth: number | undefined = undefined
|
||||
|
||||
let getComputedCols
|
||||
|
||||
let container
|
||||
|
||||
$: [gapX, gapY] = gap
|
||||
@@ -81,7 +108,21 @@
|
||||
let sortedItems: FilledItem<T>[] = []
|
||||
$: sortedItems = JSON.parse(JSON.stringify(items)).sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
let resizing: boolean = false
|
||||
|
||||
function handleKeyUp(event) {
|
||||
if ((event.key === 'Control' || event.key === 'Meta') && $isCtrlOrMetaPressedStore) {
|
||||
setTimeout(() => {
|
||||
$isCtrlOrMetaPressedStore = false
|
||||
|
||||
$fakeShadowStore = undefined
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
const initialFixedStates = new Map()
|
||||
|
||||
let initItems: FilledItem<T>[] | undefined = undefined
|
||||
|
||||
const updateMatrix = ({ detail }) => {
|
||||
let isPointerUp = detail.isPointerUp
|
||||
let citems: FilledItem<T>[]
|
||||
@@ -114,10 +155,44 @@
|
||||
...shadows[id]
|
||||
}
|
||||
}
|
||||
let { items, overlap } = moveItem(activeItem, sortedItems, getComputedCols)
|
||||
|
||||
sortedItems = items
|
||||
overlapped = overlap ? id : undefined
|
||||
if ($isCtrlOrMetaPressedStore) {
|
||||
if ($componentDraggedParentIdStore === $overlappedStore) {
|
||||
const fixedContainer = sortedItems.map((item) => {
|
||||
if (isContainer(item.data['type'])) {
|
||||
initialFixedStates.set(item.id, {
|
||||
item3Fixed: item[3].fixed,
|
||||
item12Fixed: item[12].fixed
|
||||
})
|
||||
|
||||
item[3].fixed = true
|
||||
item[12].fixed = true
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
let { items } = moveItem(activeItem, fixedContainer, getComputedCols)
|
||||
|
||||
items = items.map((item) => {
|
||||
if (initialFixedStates.has(item.id)) {
|
||||
const initialState = initialFixedStates.get(item.id)
|
||||
|
||||
if (initialState) {
|
||||
item[3].fixed = initialState.item3Fixed
|
||||
item[12].fixed = initialState.item12Fixed
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
sortedItems = items
|
||||
}
|
||||
} else {
|
||||
let { items } = moveItem(activeItem, sortedItems, getComputedCols)
|
||||
|
||||
sortedItems = items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +214,8 @@
|
||||
|
||||
//let hiddenComponents = writable({})
|
||||
|
||||
let lastDetail: { isPointerUp: false; activate: false; id: string | undefined } | undefined =
|
||||
undefined
|
||||
const handleRepaint = ({ detail }) => {
|
||||
if (!detail.isPointerUp) {
|
||||
throttleMatrix({ detail })
|
||||
@@ -156,6 +233,21 @@
|
||||
*/
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if ((event.key === 'Control' || event.key === 'Meta') && !$isCtrlOrMetaPressedStore) {
|
||||
if (resizing) {
|
||||
return
|
||||
}
|
||||
|
||||
$isCtrlOrMetaPressedStore = true
|
||||
|
||||
if (lastDetail) {
|
||||
throttleMatrix({ detail: lastDetail })
|
||||
lastDetail = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let moveResizes: Record<string, MoveResize> = {}
|
||||
let shadows: Record<string, { x: number; y: number; w: number; h: number } | undefined> = {}
|
||||
|
||||
@@ -165,32 +257,199 @@
|
||||
moveResize?.updateMove(JSON.parse(JSON.stringify(detail.cordDiff)), detail.eventY)
|
||||
}
|
||||
})
|
||||
|
||||
lastDetail = detail
|
||||
throttleMatrix({ detail: { isPointerUp: false, activate: false } })
|
||||
|
||||
if (!$isCtrlOrMetaPressedStore) {
|
||||
$overlappedStore = undefined
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
// We don't display the fake shadow if the dragged component is a child of the overlapped component
|
||||
$componentDraggedParentIdStore !== $overlappedStore &&
|
||||
detail.shadow &&
|
||||
// only update the fake shadow if the are different
|
||||
!areShadowsTheSame($fakeShadowStore, detail.shadow)
|
||||
) {
|
||||
const draggedItem = sortedItems.find((item) => item.id === $componentDraggedIdStore)
|
||||
|
||||
if (draggedItem) {
|
||||
draggedItem[getComputedCols].x = detail.shadow.x
|
||||
draggedItem[getComputedCols].y = detail.shadow.y
|
||||
}
|
||||
|
||||
let items: GridItem[] = []
|
||||
|
||||
if ($overlappedStore) {
|
||||
const div = document.getElementById(`component-${$overlappedStore}`)
|
||||
const type = div?.getAttribute('data-componenttype')
|
||||
|
||||
if (!$app.subgrids) {
|
||||
return
|
||||
}
|
||||
|
||||
const index = type ? subGridIndexKey(type, $overlappedStore, $worldStore) : 0
|
||||
|
||||
items = $app.subgrids[`${$overlappedStore}-${index}`] ?? []
|
||||
} else {
|
||||
items = $app.grid ?? []
|
||||
}
|
||||
|
||||
if (!draggedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const freeSpace = gridHelp.findSpace(draggedItem, items, getComputedCols)
|
||||
|
||||
$fakeShadowStore = {
|
||||
x: freeSpace.x,
|
||||
y: freeSpace.y,
|
||||
xPerPx: detail.shadow.xPerPx,
|
||||
yPerPx: detail.shadow.yPerPx,
|
||||
w: detail.shadow.w,
|
||||
h: detail.shadow.h
|
||||
}
|
||||
}
|
||||
|
||||
// When leaving the overlapped component, we clear the fake shadow
|
||||
// to avoid rendering it with the wrong position at the next intersection
|
||||
if (detail.intersectingElement !== $overlappedStore) {
|
||||
$fakeShadowStore = undefined
|
||||
}
|
||||
|
||||
// Update the overlapped component
|
||||
$overlappedStore = detail.intersectingElement
|
||||
}
|
||||
|
||||
export function handleInitMove({ detail }) {
|
||||
export function handleInitMove(id: string) {
|
||||
$componentDraggedIdStore = id
|
||||
$componentDraggedParentIdStore = findGridItemParentGrid($app, id)?.split('-')[0] ?? undefined
|
||||
|
||||
Object.entries(moveResizes).forEach(([id, moveResize]) => {
|
||||
if (selectedIds?.includes(id)) {
|
||||
moveResize?.initmove()
|
||||
}
|
||||
})
|
||||
}
|
||||
let overlapped: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="svlt-grid-container" style="height: {containerHeight}px" bind:this={container}>
|
||||
<svelte:window on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
|
||||
|
||||
<div
|
||||
class="svlt-grid-container"
|
||||
style="height: {containerHeight}px"
|
||||
bind:this={container}
|
||||
id={root ? 'root-grid' : undefined}
|
||||
data-xperpx={xPerPx}
|
||||
>
|
||||
<!-- ROOT SHADOW-->
|
||||
{#if $isCtrlOrMetaPressedStore && root && $overlappedStore !== $componentDraggedParentIdStore}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
|
||||
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
|
||||
$componentDraggedIdStore && $overlappedStore === undefined
|
||||
? 'bg-draggedover dark:bg-draggedover-dark'
|
||||
: ''
|
||||
)}
|
||||
/>
|
||||
{#if $overlappedStore === undefined && $componentDraggedIdStore && $fakeShadowStore}
|
||||
{@const columnGap = gapX}
|
||||
<!-- gap between the columns in px -->
|
||||
{@const containerBorder = 0.5 * 16}
|
||||
<!-- 0.5rem converted to px (1rem = 16px) -->
|
||||
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
|
||||
<!-- subtract borders -->
|
||||
{@const availableWidth = gridTotalWidth - 11 * columnGap}
|
||||
<!-- subtract gaps between the 12 columns (11 gaps) -->
|
||||
{@const columnWidthPx = availableWidth / 12}
|
||||
<!-- divide by the number of columns -->
|
||||
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
|
||||
|
||||
<div class="absolute inset-0">
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
class="absolute bg-blue-300 transition-all"
|
||||
style={`
|
||||
left:${Math.min(maxX, $fakeShadowStore.x) * xPerPx + gapX}px ;
|
||||
top: ${$fakeShadowStore.y * yPerPx + gapY}px;
|
||||
width: ${$fakeShadowStore.w * xPerPx - gapX}px;
|
||||
height: ${$fakeShadowStore.h * yPerPx - gapY}px;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#each sortedItems as item (item.id)}
|
||||
{#if item[getComputedCols] != undefined}
|
||||
{#if $isCtrlOrMetaPressedStore && item.id === $overlappedStore && $componentDraggedIdStore && $componentDraggedParentIdStore !== item.id && $fakeShadowStore}
|
||||
{@const columnGap = gapX}
|
||||
<!-- gap between the columns in px -->
|
||||
{@const containerBorder = 0.5 * 16}
|
||||
<!-- 0.5rem converted to px (1rem = 16px) -->
|
||||
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
|
||||
<!-- subtract borders -->
|
||||
{@const availableWidth = gridTotalWidth - 11 * columnGap}
|
||||
<!-- subtract gaps between the 12 columns (11 gaps) -->
|
||||
{@const columnWidthPx = availableWidth / 12}
|
||||
<!-- divide by the number of columns -->
|
||||
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
|
||||
|
||||
<div
|
||||
class="absolute"
|
||||
style={`
|
||||
left: ${item[getComputedCols].x * xPerPx + gapX}px;
|
||||
top: ${item[getComputedCols].y * yPerPx + gapY}px;
|
||||
`}
|
||||
>
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
class={twMerge('absolute transition-all duration-[50ms] bg-blue-300')}
|
||||
style={`
|
||||
left: calc(${
|
||||
Math.min($fakeShadowStore.x, maxX) * $fakeShadowStore.xPerPx + gapX
|
||||
}px + 0.5rem + ${getDeltaXByComponent(item.data['type'])});
|
||||
top: calc(${
|
||||
$fakeShadowStore.y * $fakeShadowStore.yPerPx + gapY
|
||||
}px + 0.5rem + ${getDeltaYByComponent(item.data['type'])});
|
||||
width: ${$fakeShadowStore.w * $fakeShadowStore.xPerPx - gapX * 2}px;
|
||||
height: ${$fakeShadowStore.h * $fakeShadowStore.yPerPx - gapY * 2}px;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<MoveResize
|
||||
on:initmove={handleInitMove}
|
||||
on:initmove={() => handleInitMove(item.id)}
|
||||
on:move={handleMove}
|
||||
bind:shadow={shadows[item.id]}
|
||||
bind:this={moveResizes[item.id]}
|
||||
on:repaint={handleRepaint}
|
||||
on:resizeStart={() => (resizing = true)}
|
||||
on:resizeEnd={() => (resizing = false)}
|
||||
onTop={Boolean(allIdsInPath?.includes(item.id))}
|
||||
id={item.id}
|
||||
{xPerPx}
|
||||
{yPerPx}
|
||||
fakeShadow={$fakeShadowStore}
|
||||
on:dropped={(e) => {
|
||||
$componentDraggedIdStore = undefined
|
||||
$componentDraggedParentIdStore = undefined
|
||||
$overlappedStore = undefined
|
||||
$fakeShadowStore = undefined
|
||||
lastDetail = undefined
|
||||
|
||||
if (!$isCtrlOrMetaPressedStore) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('dropped', e.detail)
|
||||
}}
|
||||
width={xPerPx == 0
|
||||
? 0
|
||||
: Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx -
|
||||
@@ -205,10 +464,18 @@
|
||||
{sensor}
|
||||
container={scroller}
|
||||
nativeContainer={container}
|
||||
{overlapped}
|
||||
overlapped={$overlappedStore}
|
||||
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
|
||||
type={item.data['type']}
|
||||
>
|
||||
{#if item[getComputedCols]}
|
||||
<slot dataItem={item} hidden={false} {overlapped} />
|
||||
<slot
|
||||
dataItem={item}
|
||||
hidden={false}
|
||||
overlapped={$overlappedStore}
|
||||
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
|
||||
componentDraggedId={$componentDraggedIdStore}
|
||||
/>
|
||||
{/if}
|
||||
</MoveResize>
|
||||
{/if}
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import { writable } from 'svelte/store'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import {
|
||||
computePosition,
|
||||
findGridItemParentGrid,
|
||||
isContainer,
|
||||
type GridShadow
|
||||
} from '../editor/appUtils'
|
||||
import { throttle } from './utils/other'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -28,9 +35,12 @@
|
||||
export let onTop
|
||||
export let shadow: { x: number; y: number; w: number; h: number } | undefined = undefined
|
||||
export let overlapped: string | undefined = undefined
|
||||
export let moveMode: 'move' | 'insert' = 'move'
|
||||
export let type: string | undefined = undefined
|
||||
export let fakeShadow: GridShadow | undefined = undefined
|
||||
|
||||
const ctx = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { mode } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { mode, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const scale = ctx ? ctx.scale : writable(100)
|
||||
|
||||
@@ -75,6 +85,7 @@
|
||||
x: (moveX / $scale) * 100 - initX,
|
||||
y: (moveY / $scale) * 100 - initY
|
||||
}
|
||||
|
||||
dispatch('move', { cordDiff, clientY: clientY })
|
||||
}
|
||||
return x
|
||||
@@ -168,7 +179,7 @@
|
||||
}
|
||||
|
||||
let dragClosure: (() => void) | undefined = undefined
|
||||
const pointerdown = ({ clientX, clientY, pageX, pageY }) => {
|
||||
const pointerdown = ({ clientX, clientY }) => {
|
||||
dragClosure = () => {
|
||||
dragClosure = undefined
|
||||
ctx.componentActive.set(true)
|
||||
@@ -223,16 +234,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Shared state for the shadow position
|
||||
let currentShadowPosition:
|
||||
| { x: number; y: number; xPerPx: number; yPerPx: number; h: number; w: number }
|
||||
| undefined = undefined
|
||||
let currentIntersectingElementId: string | undefined = undefined
|
||||
|
||||
let moving: boolean = false
|
||||
|
||||
const pointermove = (event) => {
|
||||
dragClosure && dragClosure()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
moving = true
|
||||
|
||||
const { clientX, clientY } = event
|
||||
const cordDiff = { x: (clientX / $scale) * 100 - initX, y: (clientY / $scale) * 100 - initY }
|
||||
|
||||
dispatch('move', { cordDiff, clientY })
|
||||
if (moveMode === 'move') {
|
||||
dispatch('move', {
|
||||
cordDiff,
|
||||
clientY,
|
||||
intersectingElement: undefined,
|
||||
shadow: undefined
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
throttledComputeShadow(clientX, clientY)
|
||||
|
||||
dispatch('move', {
|
||||
cordDiff,
|
||||
clientY,
|
||||
intersectingElement: currentIntersectingElementId,
|
||||
shadow: currentShadowPosition,
|
||||
overlapped
|
||||
})
|
||||
}
|
||||
|
||||
export function updateMove(newCoordDiff, clientY) {
|
||||
@@ -274,6 +314,56 @@
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
let element: HTMLElement | undefined = undefined
|
||||
|
||||
function computeShadow(clientX: number, clientY: number) {
|
||||
const elementsAtPoint = document.elementsFromPoint(clientX, clientY)
|
||||
const intersectingElement = elementsAtPoint.find(
|
||||
(el) =>
|
||||
el.id !== divId &&
|
||||
el.classList.contains('svlt-grid-item') &&
|
||||
el.getAttribute('data-iscontainer') === 'true'
|
||||
)
|
||||
|
||||
const newOverlapped = intersectingElement ? intersectingElement?.id.split('-')[1] : undefined
|
||||
|
||||
const container = newOverlapped
|
||||
? intersectingElement?.querySelector('.svlt-grid-container')
|
||||
: document.getElementById('root-grid')
|
||||
|
||||
const xPerPxComputed = Number(container?.getAttribute('data-xperpx')) ?? xPerPx
|
||||
|
||||
const position = computePosition(
|
||||
clientX,
|
||||
clientY,
|
||||
xPerPxComputed,
|
||||
yPerPx,
|
||||
newOverlapped,
|
||||
element
|
||||
)
|
||||
|
||||
if (overlapped !== newOverlapped) {
|
||||
currentShadowPosition = undefined
|
||||
} else {
|
||||
// Update shared state
|
||||
currentShadowPosition = {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
xPerPx: xPerPxComputed,
|
||||
yPerPx,
|
||||
h: item.h,
|
||||
w: item.w
|
||||
}
|
||||
}
|
||||
|
||||
currentIntersectingElementId = newOverlapped
|
||||
}
|
||||
|
||||
const throttledComputeShadow = throttle((clientX, clientY) => {
|
||||
computeShadow(clientX, clientY)
|
||||
}, 50)
|
||||
|
||||
const pointerup = (e) => {
|
||||
ctx.componentActive.set(false)
|
||||
stopAutoscroll()
|
||||
@@ -281,11 +371,29 @@
|
||||
window.removeEventListener('pointerdown', pointerdown)
|
||||
window.removeEventListener('pointermove', pointermove)
|
||||
window.removeEventListener('pointerup', pointerup)
|
||||
|
||||
if (!dragClosure) {
|
||||
repaint(true, true)
|
||||
} else {
|
||||
dragClosure = undefined
|
||||
}
|
||||
|
||||
if (!moving) {
|
||||
return
|
||||
}
|
||||
|
||||
const parent = findGridItemParentGrid($app, id)
|
||||
|
||||
if (overlapped && (overlapped === parent || parent?.startsWith(overlapped))) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('dropped', {
|
||||
id,
|
||||
overlapped,
|
||||
x: fakeShadow?.x,
|
||||
y: fakeShadow?.y
|
||||
})
|
||||
}
|
||||
|
||||
let resizeInitPos = { x: 0, y: 0 }
|
||||
@@ -315,6 +423,7 @@
|
||||
|
||||
window.addEventListener('pointermove', resizePointerMove)
|
||||
window.addEventListener('pointerup', resizePointerUp)
|
||||
dispatch('resizeStart')
|
||||
}
|
||||
|
||||
const resizePointerMove = ({ pageX, pageY }) => {
|
||||
@@ -350,15 +459,36 @@
|
||||
|
||||
window.removeEventListener('pointermove', resizePointerMove)
|
||||
window.removeEventListener('pointerup', resizePointerUp)
|
||||
dispatch('resizeEnd')
|
||||
}
|
||||
|
||||
function shouldDisplayShadow(moveMode: 'insert' | 'move', overlapped: string | undefined) {
|
||||
if (moveMode === 'move') {
|
||||
return true
|
||||
}
|
||||
|
||||
const parent = findGridItemParentGrid($app, id)
|
||||
|
||||
if (parent === undefined) {
|
||||
return overlapped === undefined
|
||||
} else if (overlapped) {
|
||||
return parent.startsWith(overlapped)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={element}
|
||||
draggable="false"
|
||||
on:pointerdown|stopPropagation|preventDefault={pointerdown}
|
||||
id={divId}
|
||||
class="svlt-grid-item"
|
||||
data-iscontainer={type ? isContainer(type) : false}
|
||||
data-componenttype={type}
|
||||
class:svlt-grid-active={active || (trans && rect)}
|
||||
style="width: {xPerPx == 0 ? 0 : active ? newSize.width : width}px; height:{xPerPx == 0
|
||||
? 0
|
||||
@@ -377,16 +507,24 @@
|
||||
} transform: translate(${left}px, ${top}px); `} "
|
||||
>
|
||||
<slot />
|
||||
<div class="svlt-grid-resizer-bottom" on:pointerdown={(e) => resizePointerDown(e, 'vertical')} />
|
||||
<div class="svlt-grid-resizer-side" on:pointerdown={(e) => resizePointerDown(e, 'horizontal')} />
|
||||
<div class="svlt-grid-resizer" on:pointerdown={(e) => resizePointerDown(e, 'both')} />
|
||||
{#if moveMode === 'move'}
|
||||
<div
|
||||
class="svlt-grid-resizer-bottom"
|
||||
on:pointerdown={(e) => resizePointerDown(e, 'vertical')}
|
||||
/>
|
||||
<div
|
||||
class="svlt-grid-resizer-side"
|
||||
on:pointerdown={(e) => resizePointerDown(e, 'horizontal')}
|
||||
/>
|
||||
<div class="svlt-grid-resizer" on:pointerdown={(e) => resizePointerDown(e, 'both')} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if xPerPx > 0 && (active || trans) && shadow}
|
||||
<div
|
||||
class={twMerge(
|
||||
'svlt-grid-shadow shadow-active',
|
||||
overlapped ? 'svlte-grid-shadow-forbidden' : ''
|
||||
shouldDisplayShadow(moveMode, overlapped) ? '' : 'hidden'
|
||||
)}
|
||||
style="width: {shadow.w * xPerPx - gapX * 2}px; height: {shadow.h * yPerPx -
|
||||
gapY * 2}px; transform: translate({shadow.x * xPerPx + gapX}px, {shadow.y * yPerPx +
|
||||
@@ -470,8 +608,4 @@
|
||||
-webkit-backface-visibility: hidden;
|
||||
background: #93c4fdd0;
|
||||
}
|
||||
|
||||
.svlte-grid-shadow-forbidden {
|
||||
background: rgba(255, 99, 71, 0.2) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,6 +31,7 @@ function distance(a, b): number {
|
||||
|
||||
export function findFreeSpaceForItem<T>(matrix: FilledItem<T>[][], item: ItemLayout) {
|
||||
const cols = matrix[0].length
|
||||
|
||||
const w = Math.min(cols, item.w)
|
||||
const h = item.h
|
||||
let xNtime = cols - w + 1
|
||||
@@ -135,8 +136,7 @@ export function moveItem(active, items, cols) {
|
||||
// If found fixed, reset the active to its original position
|
||||
if (fixed) {
|
||||
return {
|
||||
items: items,
|
||||
overlap: closeBlocks.length > 0
|
||||
items: items
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export function moveItem(active, items, cols) {
|
||||
// Return result
|
||||
return {
|
||||
items: tempItems,
|
||||
overlap: false
|
||||
overlap: undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
{#if condition}
|
||||
<Portal {target}><slot /></Portal>
|
||||
<Portal name="conditional-portal-select" {target}><slot /></Portal>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
{#if condition}
|
||||
<Portal><slot /></Portal>
|
||||
<Portal name="conditional-portal-global"><slot /></Portal>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</MenuButton>
|
||||
</span>
|
||||
|
||||
<Portal {target}>
|
||||
<Portal name="button-dropdown" {target}>
|
||||
<div use:popperContent={popperOptions} class="z-[6000]">
|
||||
<Transition
|
||||
show={open}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</script>
|
||||
|
||||
{#if condition}
|
||||
<Portal {target}><slot /></Portal>
|
||||
<Portal name="conditional-portal" {target}><slot /></Portal>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<slot name="trigger" />
|
||||
</MenuButton>
|
||||
</div>
|
||||
<Portal>
|
||||
<Portal name="menu-v2">
|
||||
<div use:floatingContent class="z-[6000]">
|
||||
<Transition
|
||||
{open}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal target="#app-editor-top-level-drawer">
|
||||
<Portal name="always-mounted" target="#app-editor-top-level-drawer">
|
||||
<div
|
||||
class={twMerge(
|
||||
`${
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<slot {pointerup} {pointerdown} name="button" />
|
||||
</div>
|
||||
|
||||
<Portal {target}>
|
||||
<Portal name="popup-v2" {target}>
|
||||
{#if open}
|
||||
<div
|
||||
class="border rounded-lg shadow-lg bg-surface z5000"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</MenuButton>
|
||||
</span>
|
||||
|
||||
<Portal>
|
||||
<Portal name="menu">
|
||||
<div use:popperContent={popperOptions} class="z-[2000]">
|
||||
<Transition
|
||||
show={open}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let disableTutorials = false
|
||||
export let disableAi = false
|
||||
export let disableSettings = false
|
||||
export let disabledFlowInputs = false
|
||||
export let smallErrorHandler = false
|
||||
export let newFlow: boolean = false
|
||||
|
||||
@@ -63,7 +64,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FlowEditorPanel enableAi={!disableAi} {newFlow} />
|
||||
<FlowEditorPanel {disabledFlowInputs} {newFlow} enableAi={!disableAi} />
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
@@ -1,88 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import PanelSection from '../apps/editor/settingsPanel/common/PanelSection.svelte'
|
||||
import { classNames, displayDate, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { type Flow, FlowService, type FlowVersion } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Drawer from '../common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '../common/drawer/DrawerContent.svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import FlowHistoryInner from './FlowHistoryInner.svelte'
|
||||
|
||||
export let path: string
|
||||
let drawer: Drawer
|
||||
let loading: boolean = false
|
||||
|
||||
let versions: FlowVersion[] = []
|
||||
|
||||
let selectedVersion: FlowVersion | undefined = undefined
|
||||
let selected: Flow | undefined = undefined
|
||||
let deploymentMsgUpdateMode = false
|
||||
let deploymentMsgUpdate: string | undefined = undefined
|
||||
|
||||
export function open() {
|
||||
loadVersions()
|
||||
drawer.openDrawer()
|
||||
}
|
||||
|
||||
async function loadFlow(version: number) {
|
||||
selected = await FlowService.getFlowVersion({
|
||||
workspace: $workspaceStore!,
|
||||
version,
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
loading = true
|
||||
versions = await FlowService.getFlowHistory({
|
||||
workspace: $workspaceStore!,
|
||||
path: path
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function updateDeploymentMsg(version: number | undefined) {
|
||||
if (
|
||||
selectedVersion === undefined ||
|
||||
version === undefined ||
|
||||
emptyString(deploymentMsgUpdate)
|
||||
) {
|
||||
return
|
||||
}
|
||||
await FlowService.updateFlowHistory({
|
||||
workspace: $workspaceStore!,
|
||||
version,
|
||||
path,
|
||||
requestBody: {
|
||||
deployment_msg: deploymentMsgUpdate!
|
||||
}
|
||||
})
|
||||
selectedVersion.deployment_msg = deploymentMsgUpdate
|
||||
deploymentMsgUpdateMode = false
|
||||
loadVersions()
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function restoreVersion(flow: Flow | undefined) {
|
||||
if (!flow) return
|
||||
await FlowService.updateFlow({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
...flow,
|
||||
path
|
||||
},
|
||||
path
|
||||
})
|
||||
dispatch('historyRestore')
|
||||
drawer?.closeDrawer()
|
||||
sendUserToast('Flow restored from previous deployment')
|
||||
}
|
||||
|
||||
$: selectedVersion !== undefined && loadFlow(selectedVersion.id)
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={drawer} size="1200px">
|
||||
@@ -91,136 +21,14 @@
|
||||
drawer?.closeDrawer()
|
||||
}}
|
||||
noPadding
|
||||
title="Deployment History"
|
||||
>
|
||||
<Splitpanes class="!overflow-visible">
|
||||
<Pane size={20}>
|
||||
<PanelSection title="Past Deployments">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if !loading}
|
||||
{#if versions.length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each versions ?? [] as version}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-blue-50 hover:text-blue-400',
|
||||
selectedVersion?.id == version.id ? 'bg-blue-100 text-blue-600' : ''
|
||||
)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={() => {
|
||||
selectedVersion = version
|
||||
}}
|
||||
>
|
||||
<span class="text-xs truncate">
|
||||
{#if emptyString(version.deployment_msg)}Version {version.id}{:else}{version.deployment_msg}{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-tertiary">No items</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
|
||||
{/if}
|
||||
</div>
|
||||
</PanelSection>
|
||||
</Pane>
|
||||
<Pane size={80}>
|
||||
<div class="h-full w-full overflow-auto">
|
||||
{#if selectedVersion}
|
||||
{#if selected}
|
||||
<div class="px-2 flex flex-col gap-2">
|
||||
<span class="flex flex-row text-sm p-1 text-tertiary">
|
||||
{#if deploymentMsgUpdateMode}
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={deploymentMsgUpdate}
|
||||
class="!w-auto grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
on:keydown|stopPropagation
|
||||
on:keypress|stopPropagation={({ key }) => {
|
||||
if (key === 'Enter') updateDeploymentMsg(selectedVersion?.id)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Save Deployment Message"
|
||||
on:click={() => {
|
||||
updateDeploymentMsg(selectedVersion?.id)
|
||||
}}
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Abort"
|
||||
on:click={() => {
|
||||
deploymentMsgUpdateMode = false
|
||||
deploymentMsgUpdate = undefined
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
{#if selectedVersion.deployment_msg}
|
||||
{selectedVersion.deployment_msg}
|
||||
{:else}
|
||||
Deployed {displayDate(selected.edited_at)} by {selected.edited_by}
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => {
|
||||
deploymentMsgUpdate = selectedVersion?.deployment_msg
|
||||
deploymentMsgUpdateMode = true
|
||||
}}
|
||||
title="Update commit message"
|
||||
class="flex items-center px-1 rounded-sm hover:text-primary text-secondary h-5"
|
||||
aria-label="Update commit message"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
<div class="flex p-1 gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() =>
|
||||
window.open(
|
||||
`/flows/add?template_id=${selectedVersion?.id}&template=${path}`,
|
||||
'_blank'
|
||||
)}
|
||||
>
|
||||
Restore as fork
|
||||
</Button>
|
||||
<Button size="xs" on:click={() => restoreVersion(selected)}
|
||||
>Redeploy with that version
|
||||
</Button>
|
||||
</div>
|
||||
{#await import('$lib/components/FlowViewer.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default flow={selected} />
|
||||
{/await}
|
||||
</div>
|
||||
{:else}
|
||||
<Skeleton layout={[[40]]} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-sm p-2 text-tertiary"
|
||||
>Select a deployment version to see its details</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<FlowHistoryInner
|
||||
on:historyRestore={() => {
|
||||
drawer.closeDrawer()
|
||||
dispatch('historyRestore')
|
||||
}}
|
||||
{path}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<script lang="ts">
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { classNames, displayDate, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { type Flow, FlowService, type FlowVersion } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let path: string
|
||||
let loading: boolean = false
|
||||
|
||||
let versions: FlowVersion[] = []
|
||||
|
||||
let selectedVersion: FlowVersion | undefined = undefined
|
||||
let selected: Flow | undefined = undefined
|
||||
let deploymentMsgUpdateMode = false
|
||||
let deploymentMsgUpdate: string | undefined = undefined
|
||||
|
||||
async function loadFlow(version: number) {
|
||||
selected = await FlowService.getFlowVersion({
|
||||
workspace: $workspaceStore!,
|
||||
version,
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
loading = true
|
||||
versions = await FlowService.getFlowHistory({
|
||||
workspace: $workspaceStore!,
|
||||
path: path
|
||||
})
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function updateDeploymentMsg(version: number | undefined) {
|
||||
if (
|
||||
selectedVersion === undefined ||
|
||||
version === undefined ||
|
||||
emptyString(deploymentMsgUpdate)
|
||||
) {
|
||||
return
|
||||
}
|
||||
await FlowService.updateFlowHistory({
|
||||
workspace: $workspaceStore!,
|
||||
version,
|
||||
path,
|
||||
requestBody: {
|
||||
deployment_msg: deploymentMsgUpdate!
|
||||
}
|
||||
})
|
||||
selectedVersion.deployment_msg = deploymentMsgUpdate
|
||||
deploymentMsgUpdateMode = false
|
||||
loadVersions()
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function restoreVersion(flow: Flow | undefined) {
|
||||
if (!flow) return
|
||||
await FlowService.updateFlow({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
...flow,
|
||||
path
|
||||
},
|
||||
path
|
||||
})
|
||||
dispatch('historyRestore')
|
||||
sendUserToast('Flow restored from previous deployment')
|
||||
}
|
||||
|
||||
loadVersions()
|
||||
|
||||
$: selectedVersion !== undefined && loadFlow(selectedVersion.id)
|
||||
</script>
|
||||
|
||||
<Splitpanes class="!overflow-visible">
|
||||
<Pane size={20}>
|
||||
<div class="flex flex-col gap-2 w-full px-2 py-2">
|
||||
{#if !loading}
|
||||
{#if versions.length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each versions ?? [] as version}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-surface-hover hover:text-primary',
|
||||
selectedVersion?.id == version.id ? 'bg-surface-selected text-primary' : ''
|
||||
)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={() => {
|
||||
selectedVersion = version
|
||||
}}
|
||||
>
|
||||
<span class="text-xs truncate">
|
||||
{#if emptyString(version.deployment_msg)}Version {version.id}{:else}{version.deployment_msg}{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-tertiary">No items</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[40], [40], [40], [40], [40]]} />
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={80}>
|
||||
<div class="h-full w-full overflow-auto pt-2">
|
||||
{#if selectedVersion}
|
||||
{#if selected}
|
||||
<div class="px-2 flex flex-col gap-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="flex flex-row text-sm p-1 text-tertiary">
|
||||
{#if deploymentMsgUpdateMode}
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={deploymentMsgUpdate}
|
||||
class="!w-auto grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
on:keydown|stopPropagation
|
||||
on:keypress|stopPropagation={({ key }) => {
|
||||
if (key === 'Enter') updateDeploymentMsg(selectedVersion?.id)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Save Deployment Message"
|
||||
on:click={() => {
|
||||
updateDeploymentMsg(selectedVersion?.id)
|
||||
}}
|
||||
>
|
||||
<ArrowRight size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Abort"
|
||||
on:click={() => {
|
||||
deploymentMsgUpdateMode = false
|
||||
deploymentMsgUpdate = undefined
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
{#if selectedVersion.deployment_msg}
|
||||
{selectedVersion.deployment_msg}
|
||||
{:else}
|
||||
Deployed {displayDate(selected.edited_at)} by {selected.edited_by}
|
||||
{/if}
|
||||
<button
|
||||
on:click={() => {
|
||||
deploymentMsgUpdate = selectedVersion?.deployment_msg
|
||||
deploymentMsgUpdateMode = true
|
||||
}}
|
||||
title="Update commit message"
|
||||
class="flex items-center px-1 rounded-sm hover:text-primary text-secondary h-5"
|
||||
aria-label="Update commit message"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
<div class="flex p-1 gap-2">
|
||||
<div class="flex">
|
||||
<Button
|
||||
size="sm"
|
||||
color="dark"
|
||||
on:click={() =>
|
||||
window.open(
|
||||
`/flows/add?template_id=${selectedVersion?.id}&template=${path}`,
|
||||
'_blank'
|
||||
)}
|
||||
>
|
||||
Restore as fork
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<Button size="sm" color="dark" on:click={() => restoreVersion(selected)}
|
||||
>Redeploy with that version
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#await import('$lib/components/FlowViewer.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default flow={selected} />
|
||||
{/await}
|
||||
</div>
|
||||
{:else}
|
||||
<Skeleton layout={[[40]]} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-sm p-2 text-tertiary">Select a deployment version to see its details</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
@@ -16,6 +16,8 @@
|
||||
export let noEditor = false
|
||||
export let enableAi = false
|
||||
export let newFlow = false
|
||||
export let disabledFlowInputs = false
|
||||
|
||||
const { selectedId, flowStore, flowStateStore, flowInputsStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
@@ -58,7 +60,7 @@
|
||||
{#if $selectedId?.startsWith('settings')}
|
||||
<FlowSettings {noEditor} />
|
||||
{:else if $selectedId === 'Input'}
|
||||
<FlowInput {noEditor} />
|
||||
<FlowInput {noEditor} disabled={disabledFlowInputs} />
|
||||
{:else if $selectedId === 'Result'}
|
||||
<p class="p-4 text-secondary">Nothing to show about the result node. Happy flow building!</p>
|
||||
{:else if $selectedId === 'constants'}
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
import SavedInputs from '$lib/components/SavedInputs.svelte'
|
||||
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
|
||||
import AddProperty from '$lib/components/schema/AddProperty.svelte'
|
||||
import FlowInputViewer from '$lib/components/FlowInputViewer.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
export let disabled: boolean
|
||||
|
||||
const { flowStore, flowStateStore, previewArgs, initialPath } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
@@ -42,67 +44,73 @@
|
||||
<CapturePayload bind:this={capturePayload} />
|
||||
|
||||
<FlowCard {noEditor} title="Flow Input">
|
||||
<div class="flex flex-row items-center gap-2 px-4 py-2 border-b">
|
||||
<div>Copy input's schema from</div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
capturePayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A request
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
jsonPayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A JSON
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
inputLibraryDrawer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Past Runs/Input library
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={$flowStore.value.modules.length === 0 ||
|
||||
$flowStore.value.modules[0].value.type == 'identity'}
|
||||
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
|
||||
>
|
||||
First step's inputs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-4 border-b">
|
||||
<AddProperty
|
||||
bind:schema={$flowStore.schema}
|
||||
bind:this={addProperty}
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if !disabled}
|
||||
<div class="flex flex-row items-center gap-2 px-4 py-2 border-b">
|
||||
<div>Copy input's schema from</div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
capturePayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A request
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
jsonPayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A JSON
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
inputLibraryDrawer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Past Runs/Input library
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={$flowStore.value.modules.length === 0 ||
|
||||
$flowStore.value.modules[0].value.type == 'identity'}
|
||||
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
|
||||
>
|
||||
First step's inputs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-4 border-b">
|
||||
<AddProperty
|
||||
bind:schema={$flowStore.schema}
|
||||
bind:this={addProperty}
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EditableSchemaForm
|
||||
bind:schema={$flowStore.schema}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addProperty?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
offset={yOffset}
|
||||
displayWebhookWarning
|
||||
/>
|
||||
<EditableSchemaForm
|
||||
bind:schema={$flowStore.schema}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addProperty?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
offset={yOffset}
|
||||
displayWebhookWarning
|
||||
/>
|
||||
{:else}
|
||||
<div class="p-4 border-b">
|
||||
<FlowInputViewer schema={$flowStore.schema} />
|
||||
</div>
|
||||
{/if}
|
||||
</FlowCard>
|
||||
|
||||
<Drawer bind:this={jsonPayload} size="800px">
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<div class="flex flex-col flex-1 h-full overflow-auto p-2">
|
||||
{#if showDate && date}
|
||||
<span class="text-xs text-tertiary mb-4"><TimeAgo agoOnlyIfRecent withDate {date} /></span>
|
||||
<span class="text-xs text-tertiary mb-4"><TimeAgo agoOnlyIfRecent {date} /></span>
|
||||
{/if}
|
||||
{#if notFound}
|
||||
<div class="text-red-400">script not found at {path} in workspace {$workspaceStore}</div>
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal>
|
||||
<Portal name="flow-module">
|
||||
<ConfirmationModal
|
||||
title="Confirm deleting step with dependents"
|
||||
confirmationText="Delete step"
|
||||
|
||||
@@ -1,114 +1,129 @@
|
||||
<script lang="ts">
|
||||
import { customIcon } from './store'
|
||||
|
||||
export let height = '24px'
|
||||
export let width = '24px'
|
||||
export let white = false
|
||||
export let spin: 'slow' | 'medium' | 'fast' | 'veryfast' | undefined = undefined
|
||||
</script>
|
||||
|
||||
<!-- Generator: Adobe Illustrator 26.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg
|
||||
class={$$props.class}
|
||||
class:animate-[spin_2s_linear_infinite]={spin === 'veryfast'}
|
||||
class:animate-[spin_5s_linear_infinite]={spin === 'fast'}
|
||||
class:animate-[spin_15s_linear_infinite]={spin === 'medium'}
|
||||
class:animate-[spin_50s_linear_infinite]={spin === 'slow'}
|
||||
version="1.1"
|
||||
id="Calque_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 256 256"
|
||||
style="enable-background:new 0 0 256 256;"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.st1 {
|
||||
opacity: 0.4;
|
||||
fill: #ffffff;
|
||||
}
|
||||
.st2 {
|
||||
fill: #bcd4fc;
|
||||
}
|
||||
.st2-gray {
|
||||
fill: #cccccc;
|
||||
}
|
||||
.st3 {
|
||||
fill: #3b82f6;
|
||||
}
|
||||
.st4 {
|
||||
fill: #b3b3b3;
|
||||
}
|
||||
.st5 {
|
||||
fill: url(#SVGID_1_);
|
||||
}
|
||||
.st6 {
|
||||
fill: url(#SVGID_00000021089067129159788970000008246765442136188072_);
|
||||
}
|
||||
.st7 {
|
||||
fill: url(#SVGID_00000117639240116366130650000015074833605515028638_);
|
||||
}
|
||||
.st8 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000101781798616409025840000016567063639337360777_);
|
||||
}
|
||||
.st9 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000052086836598721292040000002033117744178971046_);
|
||||
}
|
||||
.st10 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000159460939004760751800000002448009281983951536_);
|
||||
}
|
||||
.st11 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000013177830667419993080000017721442101626521532_);
|
||||
}
|
||||
.st12 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000152235521444854938490000006526001119318383285_);
|
||||
}
|
||||
.st13 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000119823135212293698520000012774889010992664993_);
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<polygon
|
||||
class:st2={!white}
|
||||
class:st2-gray={white}
|
||||
points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22 "
|
||||
{#if customIcon}
|
||||
{#if white}
|
||||
<img src={customIcon.white} alt="Windmill Custom icon" {width} {height} class={$$props.class} />
|
||||
{:else}
|
||||
<img
|
||||
src={customIcon.normal}
|
||||
alt="Windmill Custom icon"
|
||||
{width}
|
||||
{height}
|
||||
class={$$props.class}
|
||||
/>
|
||||
<polygon
|
||||
class:st3={!white}
|
||||
class:st0={white}
|
||||
points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02 "
|
||||
/>
|
||||
<polygon
|
||||
class:st3={!white}
|
||||
class:st0={white}
|
||||
points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46
|
||||
{/if}
|
||||
{:else}
|
||||
<svg
|
||||
class={$$props.class}
|
||||
class:animate-[spin_2s_linear_infinite]={spin === 'veryfast'}
|
||||
class:animate-[spin_5s_linear_infinite]={spin === 'fast'}
|
||||
class:animate-[spin_15s_linear_infinite]={spin === 'medium'}
|
||||
class:animate-[spin_50s_linear_infinite]={spin === 'slow'}
|
||||
version="1.1"
|
||||
id="Calque_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 256 256"
|
||||
style="enable-background:new 0 0 256 256;"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.st1 {
|
||||
opacity: 0.4;
|
||||
fill: #ffffff;
|
||||
}
|
||||
.st2 {
|
||||
fill: #bcd4fc;
|
||||
}
|
||||
.st2-gray {
|
||||
fill: #cccccc;
|
||||
}
|
||||
.st3 {
|
||||
fill: #3b82f6;
|
||||
}
|
||||
.st4 {
|
||||
fill: #b3b3b3;
|
||||
}
|
||||
.st5 {
|
||||
fill: url(#SVGID_1_);
|
||||
}
|
||||
.st6 {
|
||||
fill: url(#SVGID_00000021089067129159788970000008246765442136188072_);
|
||||
}
|
||||
.st7 {
|
||||
fill: url(#SVGID_00000117639240116366130650000015074833605515028638_);
|
||||
}
|
||||
.st8 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000101781798616409025840000016567063639337360777_);
|
||||
}
|
||||
.st9 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000052086836598721292040000002033117744178971046_);
|
||||
}
|
||||
.st10 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000159460939004760751800000002448009281983951536_);
|
||||
}
|
||||
.st11 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000013177830667419993080000017721442101626521532_);
|
||||
}
|
||||
.st12 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000152235521444854938490000006526001119318383285_);
|
||||
}
|
||||
.st13 {
|
||||
opacity: 0.4;
|
||||
fill: url(#SVGID_00000119823135212293698520000012774889010992664993_);
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<polygon
|
||||
class:st2={!white}
|
||||
class:st2-gray={white}
|
||||
points="134.78,14.22 114.31,48.21 101.33,69.75 158.22,69.75 177.97,36.95 191.67,14.22 "
|
||||
/>
|
||||
<polygon
|
||||
class:st3={!white}
|
||||
class:st0={white}
|
||||
points="227.55,69.75 186.61,69.75 101.33,69.75 129.78,119.02 158.16,119.02 228.61,119.02 256,119.02 "
|
||||
/>
|
||||
<polygon
|
||||
class:st3={!white}
|
||||
class:st0={white}
|
||||
points="136.93,132.47 116.46,167.93 73.82,241.78 130.71,241.78 144.9,217.2 180.13,156.18 193.82,132.46
|
||||
"
|
||||
/>
|
||||
<polygon
|
||||
class:st3={!white}
|
||||
class:st0={white}
|
||||
points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22 "
|
||||
/>
|
||||
<polygon
|
||||
class:st2={!white}
|
||||
class:st2-gray={white}
|
||||
points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21 "
|
||||
/>
|
||||
<polygon
|
||||
class:st2={!white}
|
||||
class:st2-gray={white}
|
||||
points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46 "
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
/>
|
||||
<polygon
|
||||
class:st3={!white}
|
||||
class:st0={white}
|
||||
points="121.7,131.95 101.23,96.49 58.59,22.63 30.15,71.91 44.34,96.49 79.57,157.5 93.26,181.22 "
|
||||
/>
|
||||
<polygon
|
||||
class:st2={!white}
|
||||
class:st2-gray={white}
|
||||
points="64.81,131.95 25.15,131.21 0,130.74 28.44,180.01 66.73,180.72 93.26,181.21 "
|
||||
/>
|
||||
<polygon
|
||||
class:st2={!white}
|
||||
class:st2-gray={white}
|
||||
points="165.38,181.74 184.58,216.46 196.75,238.47 225.19,189.2 206.66,155.69 193.83,132.46 "
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export let customIcon: { normal: string; white: string } | undefined = undefined
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
<Portal>
|
||||
<Portal name="multi-select">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
$: fullyCollapsed = keys.length > 1 && collapsed
|
||||
</script>
|
||||
|
||||
<Portal>
|
||||
<Portal name="object-viewer">
|
||||
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
|
||||
</Portal>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal>
|
||||
<Portal name="run-row">
|
||||
<ScheduleEditor on:update={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
</Portal>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -105,7 +105,7 @@
|
||||
<div class="flex flex-row items-center gap-1 text-gray-500 dark:text-gray-300 text-2xs">
|
||||
{#if job}
|
||||
{#if 'started_at' in job && job.started_at}
|
||||
Started <TimeAgo withDate agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
Started <TimeAgo agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
(Ran in {msToReadableTime(
|
||||
job.duration_ms
|
||||
@@ -121,11 +121,7 @@
|
||||
{:else if `scheduled_for` in job && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
Scheduled for {displayDate(job.scheduled_for)}
|
||||
{:else}
|
||||
Waiting for executor (created <TimeAgo
|
||||
withDate
|
||||
agoOnlyIfRecent
|
||||
date={job.created_at || ''}
|
||||
/>)
|
||||
Waiting for executor (created <TimeAgo agoOnlyIfRecent date={job.created_at || ''} />)
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -341,6 +341,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<ArgInput
|
||||
noDefaultOnSelectFirst
|
||||
{itemPicker}
|
||||
resourceTypes={getResourceTypesFromFormat(format)}
|
||||
bind:value={defaultValue}
|
||||
|
||||
@@ -499,7 +499,7 @@
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<Portal>
|
||||
<Portal name="global-search">
|
||||
<div
|
||||
class={twMerge(
|
||||
`fixed top-0 bottom-0 left-0 right-0 transition-all duration-50 flex items-start justify-center`,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export default `import os
|
||||
|
||||
def main(message: str, name: str, step_id: str):
|
||||
flow_id = os.environ.get("WM_ROOT_FLOW_JOB_ID")
|
||||
print("message", message)
|
||||
print("name", name)
|
||||
print("step_id", step_id)
|
||||
return { "message": message, "flow_id": flow_id, "step_id": step_id, "recover": False }`
|
||||
@@ -1,40 +0,0 @@
|
||||
export default `import os
|
||||
import wmill
|
||||
|
||||
# You can import any PyPi package.
|
||||
# See here for more info: https://www.windmill.dev/docs/advanced/dependencies_in_python
|
||||
|
||||
# you can use typed resources by doing a type alias to dict
|
||||
#postgresql = dict
|
||||
|
||||
def main(
|
||||
no_default: str,
|
||||
#db: postgresql,
|
||||
name="Nicolas Bourbaki",
|
||||
age=42,
|
||||
obj: dict = {"even": "dicts"},
|
||||
l: list = ["or", "lists!"],
|
||||
file_: bytes = bytes(0),
|
||||
):
|
||||
|
||||
print(f"Hello World and a warm welcome especially to {name}")
|
||||
print("and its acolytes..", age, obj, l, len(file_))
|
||||
|
||||
# retrieve variables, resources, states using the wmill client
|
||||
try:
|
||||
secret = wmill.get_variable("f/examples/secret")
|
||||
except:
|
||||
secret = "No secret yet at f/examples/secret !"
|
||||
print(f"The variable at \`f/examples/secret\`: {secret}")
|
||||
|
||||
# Get last state of this script execution by the same trigger/user
|
||||
last_state = wmill.get_state()
|
||||
new_state = {"foo": 42} if last_state is None else last_state
|
||||
new_state["foo"] += 1
|
||||
wmill.set_state(new_state)
|
||||
|
||||
# fetch context variables
|
||||
user = os.environ.get("WM_USERNAME")
|
||||
|
||||
# return value is converted to JSON
|
||||
return {"splitted": name.split(), "user": user, "state": new_state}`
|
||||
@@ -1,5 +0,0 @@
|
||||
export default `# import wmill
|
||||
|
||||
|
||||
def main(x: str):
|
||||
return x`
|
||||
@@ -1,14 +0,0 @@
|
||||
export default `import wmill
|
||||
|
||||
|
||||
def main():
|
||||
# A common trigger script would follow this pattern:
|
||||
# 1. Get the last saved state
|
||||
# state = wmill.get_state()
|
||||
# 2. Get the actual state from the external service
|
||||
# newState = ...
|
||||
# 3. Compare the two states and update the internal state
|
||||
# wmill.setState(newState)
|
||||
# 4. Return the new rows
|
||||
# return range from (state to newState)
|
||||
return [1, 2, 3]`
|
||||
@@ -1,19 +1,79 @@
|
||||
import { type Script } from './gen'
|
||||
|
||||
import PYTHON_INIT_CODE from '$lib/init_scripts/python_init_code'
|
||||
import PYTHON_INIT_CODE_CLEAR from '$lib/init_scripts/python_init_code_clear'
|
||||
import PYTHON_INIT_CODE_TRIGGER from '$lib/init_scripts/python_init_code_trigger'
|
||||
import PYTHON_FAILURE_MODULE_CODE from '$lib/init_scripts/python_failure_module'
|
||||
import type { SupportedLanguage } from './common'
|
||||
|
||||
export {
|
||||
PYTHON_INIT_CODE,
|
||||
PYTHON_INIT_CODE_CLEAR,
|
||||
PYTHON_INIT_CODE_TRIGGER,
|
||||
PYTHON_FAILURE_MODULE_CODE
|
||||
}
|
||||
export let PYTHON_FAILURE_MODULE_CODE = `import os
|
||||
|
||||
export const NATIVETS_INIT_CODE = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
|
||||
def main(message: str, name: str, step_id: str):
|
||||
flow_id = os.environ.get("WM_ROOT_FLOW_JOB_ID")
|
||||
print("message", message)
|
||||
print("name", name)
|
||||
print("step_id", step_id)
|
||||
return { "message": message, "flow_id": flow_id, "step_id": step_id, "recover": False }`
|
||||
|
||||
export let PYTHON_INIT_CODE_CLEAR = `# import wmill
|
||||
|
||||
|
||||
def main(x: str):
|
||||
return x`
|
||||
|
||||
export let PYTHON_INIT_CODE_TRIGGER = `import wmill
|
||||
|
||||
|
||||
def main():
|
||||
# A common trigger script would follow this pattern:
|
||||
# 1. Get the last saved state
|
||||
# state = wmill.get_state()
|
||||
# 2. Get the actual state from the external service
|
||||
# newState = ...
|
||||
# 3. Compare the two states and update the internal state
|
||||
# wmill.setState(newState)
|
||||
# 4. Return the new rows
|
||||
# return range from (state to newState)
|
||||
return [1, 2, 3]`
|
||||
|
||||
export let PYTHON_INIT_CODE = `import os
|
||||
import wmill
|
||||
|
||||
# You can import any PyPi package.
|
||||
# See here for more info: https://www.windmill.dev/docs/advanced/dependencies_in_python
|
||||
|
||||
# you can use typed resources by doing a type alias to dict
|
||||
#postgresql = dict
|
||||
|
||||
def main(
|
||||
no_default: str,
|
||||
#db: postgresql,
|
||||
name="Nicolas Bourbaki",
|
||||
age=42,
|
||||
obj: dict = {"even": "dicts"},
|
||||
l: list = ["or", "lists!"],
|
||||
file_: bytes = bytes(0),
|
||||
):
|
||||
|
||||
print(f"Hello World and a warm welcome especially to {name}")
|
||||
print("and its acolytes..", age, obj, l, len(file_))
|
||||
|
||||
# retrieve variables, resources, states using the wmill client
|
||||
try:
|
||||
secret = wmill.get_variable("f/examples/secret")
|
||||
except:
|
||||
secret = "No secret yet at f/examples/secret !"
|
||||
print(f"The variable at \`f/examples/secret\`: {secret}")
|
||||
|
||||
# Get last state of this script execution by the same trigger/user
|
||||
last_state = wmill.get_state()
|
||||
new_state = {"foo": 42} if last_state is None else last_state
|
||||
new_state["foo"] += 1
|
||||
wmill.set_state(new_state)
|
||||
|
||||
# fetch context variables
|
||||
user = os.environ.get("WM_USERNAME")
|
||||
|
||||
# return value is converted to JSON
|
||||
return {"splitted": name.split(), "user": user, "state": new_state}`
|
||||
|
||||
export let NATIVETS_INIT_CODE = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
|
||||
//import * as wmill from './windmill.ts'
|
||||
|
||||
export async function main(example_input: number = 3) {
|
||||
@@ -25,7 +85,7 @@ export async function main(example_input: number = 3) {
|
||||
}
|
||||
`
|
||||
|
||||
export const BUNNATIVE_INIT_CODE = `//native
|
||||
export let BUNNATIVE_INIT_CODE = `//native
|
||||
//you can add proxy support using //proxy http(s)://host:port
|
||||
|
||||
// native scripts are bun scripts that are executed on native workers and can be parallelized
|
||||
@@ -42,7 +102,7 @@ export async function main(example_input: number = 3) {
|
||||
}
|
||||
`
|
||||
|
||||
export const NATIVETS_INIT_CODE_CLEAR = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
|
||||
export let NATIVETS_INIT_CODE_CLEAR = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
|
||||
//import * as wmill from './windmill.ts'
|
||||
|
||||
export async function main() {
|
||||
@@ -53,7 +113,7 @@ export async function main() {
|
||||
}
|
||||
`
|
||||
|
||||
export const DENO_INIT_CODE = `// Ctrl/CMD+. to cache dependencies on imports hover.
|
||||
export let DENO_INIT_CODE = `// Ctrl/CMD+. to cache dependencies on imports hover.
|
||||
|
||||
// Deno uses "npm:" prefix to import from npm (https://deno.land/manual@v1.36.3/node/npm_specifiers)
|
||||
// import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
||||
@@ -74,7 +134,7 @@ export async function main(
|
||||
}
|
||||
`
|
||||
|
||||
export const BUN_INIT_CODE = `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
|
||||
export let BUN_INIT_CODE = `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
|
||||
// https://www.windmill.dev/docs/getting_started/scripts_quickstart/typescript#modes
|
||||
|
||||
// import { toWords } from "number-to-words@1"
|
||||
@@ -105,7 +165,7 @@ export async function main(
|
||||
}
|
||||
`
|
||||
|
||||
export const GO_INIT_CODE = `package inner
|
||||
export let GO_INIT_CODE = `package inner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -129,7 +189,7 @@ func main(x string, nested struct {
|
||||
}
|
||||
`
|
||||
|
||||
export const GO_FAILURE_MODULE_CODE = `package inner
|
||||
export let GO_FAILURE_MODULE_CODE = `package inner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -146,21 +206,21 @@ func main(message string, name string) (interface{}, error) {
|
||||
}
|
||||
`
|
||||
|
||||
export const DENO_INIT_CODE_CLEAR = `// import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
||||
export let DENO_INIT_CODE_CLEAR = `// import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
||||
|
||||
export async function main(x: string) {
|
||||
return x
|
||||
}
|
||||
`
|
||||
|
||||
export const BUN_INIT_CODE_CLEAR = `// import * as wmill from "windmill-client"
|
||||
export let BUN_INIT_CODE_CLEAR = `// import * as wmill from "windmill-client"
|
||||
|
||||
export async function main(x: string) {
|
||||
return x
|
||||
}
|
||||
`
|
||||
|
||||
export const DENO_FAILURE_MODULE_CODE = `
|
||||
export let DENO_FAILURE_MODULE_CODE = `
|
||||
export async function main(message: string, name: string, step_id: string) {
|
||||
const flow_id = Deno.env.get("WM_ROOT_FLOW_JOB_ID")
|
||||
console.log("message", message)
|
||||
@@ -170,7 +230,7 @@ export async function main(message: string, name: string, step_id: string) {
|
||||
}
|
||||
`
|
||||
|
||||
export const BUN_FAILURE_MODULE_CODE = `
|
||||
export let BUN_FAILURE_MODULE_CODE = `
|
||||
export async function main(message: string, name: string, step_id: string) {
|
||||
const flow_id = process.env.WM_ROOT_FLOW_JOB_ID
|
||||
console.log("message", message)
|
||||
@@ -180,7 +240,7 @@ export async function main(message: string, name: string, step_id: string) {
|
||||
}
|
||||
`
|
||||
|
||||
export const POSTGRES_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
export let POSTGRES_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
-- $1 name1 = default arg
|
||||
-- $2 name2
|
||||
-- $3 name3
|
||||
@@ -189,7 +249,7 @@ INSERT INTO demo VALUES (\$1::TEXT, \$2::INT, \$3::TEXT[]) RETURNING *;
|
||||
UPDATE demo SET col2 = \$4::INT WHERE col2 = \$2::INT;
|
||||
`
|
||||
|
||||
export const MYSQL_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
export let MYSQL_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
-- :name1 (text) = default arg
|
||||
-- :name2 (int)
|
||||
-- :name3 (int)
|
||||
@@ -197,7 +257,7 @@ INSERT INTO demo VALUES (:name1, :name2);
|
||||
UPDATE demo SET col2 = :name3 WHERE col2 = :name2;
|
||||
`
|
||||
|
||||
export const BIGQUERY_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
export let BIGQUERY_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
-- @name1 (string) = default arg
|
||||
-- @name2 (integer)
|
||||
-- @name3 (string[])
|
||||
@@ -206,7 +266,7 @@ INSERT INTO \`demodb.demo\` VALUES (@name1, @name2, @name3);
|
||||
UPDATE \`demodb.demo\` SET col2 = @name4 WHERE col2 = @name2;
|
||||
`
|
||||
|
||||
export const SNOWFLAKE_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
export let SNOWFLAKE_INIT_CODE = `-- to pin the database use '-- database f/your/path'
|
||||
-- ? name1 (varchar) = default arg
|
||||
-- ? name2 (int)
|
||||
INSERT INTO demo VALUES (?, ?);
|
||||
@@ -215,7 +275,7 @@ INSERT INTO demo VALUES (?, ?);
|
||||
UPDATE demo SET col2 = ? WHERE col2 = ?;
|
||||
`
|
||||
|
||||
export const MSSQL_INIT_CODE = `-- return_last_result
|
||||
export let MSSQL_INIT_CODE = `-- return_last_result
|
||||
-- to pin the database use '-- database f/your/path'
|
||||
-- @p1 name1 (varchar) = default arg
|
||||
-- @p2 name2 (int)
|
||||
@@ -224,7 +284,7 @@ INSERT INTO demo VALUES (@p1, @p2);
|
||||
UPDATE demo SET col2 = @p3 WHERE col2 = @p2;
|
||||
`
|
||||
|
||||
export const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [String]) {
|
||||
export let GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [String]) {
|
||||
demo(name1: $name1, name2: $name2, name3: $name3) {
|
||||
name1,
|
||||
name2,
|
||||
@@ -233,7 +293,7 @@ export const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [St
|
||||
}
|
||||
`
|
||||
|
||||
export const PHP_INIT_CODE = `<?php
|
||||
export let PHP_INIT_CODE = `<?php
|
||||
|
||||
// remove the first // of the following lines to specify packages to install using composer
|
||||
// // require:
|
||||
@@ -253,7 +313,7 @@ function main(
|
||||
}
|
||||
`
|
||||
|
||||
export const RUST_INIT_CODE = `//! Add dependencies in the following partial Cargo.toml manifest
|
||||
export let RUST_INIT_CODE = `//! Add dependencies in the following partial Cargo.toml manifest
|
||||
//!
|
||||
//! \`\`\`cargo
|
||||
//! [dependencies]
|
||||
@@ -288,7 +348,7 @@ fn main(who_to_greet: String, numbers: Vec<i8>) -> anyhow::Result<Ret> {
|
||||
}
|
||||
`
|
||||
|
||||
export const FETCH_INIT_CODE = `export async function main(
|
||||
export let FETCH_INIT_CODE = `export async function main(
|
||||
url: string | undefined,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' = 'GET',
|
||||
body: Object = {},
|
||||
@@ -319,7 +379,7 @@ export const FETCH_INIT_CODE = `export async function main(
|
||||
})
|
||||
}`
|
||||
|
||||
export const BASH_INIT_CODE = `# shellcheck shell=bash
|
||||
export let BASH_INIT_CODE = `# shellcheck shell=bash
|
||||
# arguments of the form X="$I" are parsed as parameters X of type string
|
||||
msg="$1"
|
||||
dflt="\${2:-default value}"
|
||||
@@ -329,7 +389,7 @@ dflt="\${2:-default value}"
|
||||
echo "Hello $msg"
|
||||
`
|
||||
|
||||
export const DENO_INIT_CODE_TRIGGER = `import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
||||
export let DENO_INIT_CODE_TRIGGER = `import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
||||
|
||||
export async function main() {
|
||||
|
||||
@@ -350,7 +410,7 @@ export async function main() {
|
||||
}
|
||||
`
|
||||
|
||||
export const BUN_INIT_CODE_TRIGGER = `import * as wmill from "windmill-client"
|
||||
export let BUN_INIT_CODE_TRIGGER = `import * as wmill from "windmill-client"
|
||||
|
||||
export async function main() {
|
||||
|
||||
@@ -371,7 +431,7 @@ export async function main() {
|
||||
}
|
||||
`
|
||||
|
||||
export const GO_INIT_CODE_TRIGGER = `package inner
|
||||
export let GO_INIT_CODE_TRIGGER = `package inner
|
||||
|
||||
import (
|
||||
wmill "github.com/windmill-labs/windmill-go-client"
|
||||
@@ -395,7 +455,7 @@ func main() (interface{}, error) {
|
||||
}
|
||||
`
|
||||
|
||||
export const DENO_INIT_CODE_APPROVAL = `import * as wmill from "npm:windmill-client@^1.158.2"
|
||||
export let DENO_INIT_CODE_APPROVAL = `import * as wmill from "npm:windmill-client@^1.158.2"
|
||||
|
||||
export async function main(approver?: string) {
|
||||
const urls = await wmill.getResumeUrls(approver)
|
||||
@@ -423,7 +483,7 @@ export async function main(approver?: string) {
|
||||
// add a form in Advanced - Suspend
|
||||
// all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
|
||||
|
||||
export const BUN_INIT_CODE_APPROVAL = `import * as wmill from "windmill-client@^1.158.2"
|
||||
export let BUN_INIT_CODE_APPROVAL = `import * as wmill from "windmill-client@^1.158.2"
|
||||
|
||||
export async function main(approver?: string) {
|
||||
const urls = await wmill.getResumeUrls(approver)
|
||||
@@ -451,7 +511,7 @@ export async function main(approver?: string) {
|
||||
// add a form in Advanced - Suspend
|
||||
// all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
|
||||
|
||||
export const BUN_PREPROCESSOR_MODULE_CODE = `
|
||||
export let BUN_PREPROCESSOR_MODULE_CODE = `
|
||||
export async function preprocessor(
|
||||
wm_trigger: {
|
||||
kind: 'http' | 'email' | 'webhook',
|
||||
@@ -472,7 +532,7 @@ export async function preprocessor(
|
||||
}
|
||||
`
|
||||
|
||||
export const DENO_PREPROCESSOR_MODULE_CODE = `
|
||||
export let DENO_PREPROCESSOR_MODULE_CODE = `
|
||||
export async function preprocessor(
|
||||
wm_trigger: {
|
||||
kind: 'http' | 'email' | 'wehbook',
|
||||
@@ -493,7 +553,7 @@ export async function preprocessor(
|
||||
}
|
||||
`
|
||||
|
||||
export const PYTHON_INIT_CODE_APPROVAL = `import wmill
|
||||
export let PYTHON_INIT_CODE_APPROVAL = `import wmill
|
||||
|
||||
def main():
|
||||
urls = wmill.get_resume_urls()
|
||||
@@ -520,7 +580,7 @@ def main():
|
||||
# add a form in Advanced - Suspend
|
||||
# all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
|
||||
|
||||
export const PYTHON_PREPROCESSOR_MODULE_CODE = `from typing import TypedDict, Literal
|
||||
export let PYTHON_PREPROCESSOR_MODULE_CODE = `from typing import TypedDict, Literal
|
||||
|
||||
class Http(TypedDict):
|
||||
route: str # The route path, e.g. "/users/:id"
|
||||
@@ -543,7 +603,7 @@ def preprocessor(
|
||||
}
|
||||
`
|
||||
|
||||
export const DOCKER_INIT_CODE = `# shellcheck shell=bash
|
||||
export let DOCKER_INIT_CODE = `# shellcheck shell=bash
|
||||
# Bash script that calls docker as a client to the host daemon
|
||||
# See documentation: https://www.windmill.dev/docs/advanced/docker
|
||||
msg="\${1:-world}"
|
||||
@@ -556,7 +616,7 @@ docker pull $IMAGE
|
||||
docker run --rm $IMAGE $COMMAND
|
||||
`
|
||||
|
||||
export const POWERSHELL_INIT_CODE = `param($Msg, $Dflt = "default value", [int]$Nb = 3)
|
||||
export let POWERSHELL_INIT_CODE = `param($Msg, $Dflt = "default value", [int]$Nb = 3)
|
||||
|
||||
# Import-Module MyModule
|
||||
|
||||
@@ -567,7 +627,7 @@ export const POWERSHELL_INIT_CODE = `param($Msg, $Dflt = "default value", [int]$
|
||||
# the last line of the stdout is the return value
|
||||
Write-Output "Hello $Msg"`
|
||||
|
||||
export const ANSIBLE_PLAYBOOK_INIT_CODE = `---
|
||||
export let ANSIBLE_PLAYBOOK_INIT_CODE = `---
|
||||
inventory:
|
||||
- resource_type: ansible_inventory
|
||||
# You can pin an inventory to this script by hardcoding the resource path:
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
Badge,
|
||||
Loader2,
|
||||
GitFork,
|
||||
Play,
|
||||
History,
|
||||
Columns,
|
||||
Pen,
|
||||
@@ -182,6 +183,19 @@
|
||||
label: `View runs`,
|
||||
buttonProps: {
|
||||
href: `${base}/runs/${flow.path}`,
|
||||
size: 'xs',
|
||||
color: 'light',
|
||||
startIcon: Play
|
||||
}
|
||||
})
|
||||
|
||||
buttons.push({
|
||||
label: `History`,
|
||||
buttonProps: {
|
||||
onClick: () => {
|
||||
flowHistory?.open()
|
||||
},
|
||||
|
||||
size: 'xs',
|
||||
color: 'light',
|
||||
startIcon: History
|
||||
@@ -442,7 +456,7 @@
|
||||
{/if}
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center">
|
||||
<span class="text-sm text-tertiary">
|
||||
Edited <TimeAgo withDate date={flow.edited_at ?? ''} /> by {flow.edited_by}
|
||||
Edited <TimeAgo date={flow.edited_at ?? ''} /> by {flow.edited_by}
|
||||
</span>
|
||||
|
||||
{#if flow.archived}
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
type WorkflowStatus,
|
||||
type NewScript,
|
||||
ConcurrencyGroupsService,
|
||||
|
||||
MetricsService
|
||||
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
canWrite,
|
||||
@@ -97,7 +95,7 @@
|
||||
let job: Job | undefined
|
||||
let jobUpdateLastFetch: Date | undefined
|
||||
|
||||
let scriptProgress: number | undefined = undefined;
|
||||
let scriptProgress: number | undefined = undefined
|
||||
let currentJobIsLongRunning: boolean = false
|
||||
|
||||
let viewTab: 'result' | 'logs' | 'code' | 'stats' = 'result'
|
||||
@@ -203,20 +201,20 @@
|
||||
async function onJobLoaded() {
|
||||
// We want to set up scriptProgress once job is loaded
|
||||
// We need this to show progress bar if job has progress and is finished
|
||||
if (job && job.type == "CompletedJob"){
|
||||
if (job && job.type == 'CompletedJob') {
|
||||
// If error occured and job is completed
|
||||
// than we fetch progress from server to display on what progress did it fail
|
||||
// Could be displayed after run or as a historical page
|
||||
// If opening page without running job (e.g. reloading page after run) progress will be displayed instantly
|
||||
MetricsService.getJobProgress({
|
||||
workspace: job.workspace_id ?? "NO_WORKSPACE",
|
||||
id: job.id,
|
||||
}).then(progress => {
|
||||
// Returned progress is not always 100%, could be 65%, 33%, anything
|
||||
// Its ok if its a failure and we want to keep that value
|
||||
// But we want progress to be 100% if job has been succeeded
|
||||
scriptProgress = progress;
|
||||
});
|
||||
workspace: job.workspace_id ?? 'NO_WORKSPACE',
|
||||
id: job.id
|
||||
}).then((progress) => {
|
||||
// Returned progress is not always 100%, could be 65%, 33%, anything
|
||||
// Its ok if its a failure and we want to keep that value
|
||||
// But we want progress to be 100% if job has been succeeded
|
||||
scriptProgress = progress
|
||||
})
|
||||
}
|
||||
|
||||
if (job === undefined || job.job_kind !== 'script' || job.script_hash === undefined) {
|
||||
@@ -229,7 +227,6 @@
|
||||
if (script.restart_unless_cancelled ?? false) {
|
||||
persistentScriptDefinition = script
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$: {
|
||||
@@ -349,7 +346,7 @@
|
||||
|
||||
<TestJobLoader
|
||||
lazyLogs
|
||||
bind:scriptProgress
|
||||
bind:scriptProgress
|
||||
on:done={() => job?.['result'] != undefined && (viewTab = 'result')}
|
||||
bind:this={testJobLoader}
|
||||
bind:getLogs
|
||||
@@ -360,7 +357,7 @@
|
||||
bind:notfound
|
||||
/>
|
||||
|
||||
<Portal>
|
||||
<Portal name="persistent-run">
|
||||
<PersistentScriptDrawer bind:this={persistentScriptDrawer} />
|
||||
</Portal>
|
||||
|
||||
@@ -613,11 +610,7 @@
|
||||
<Button
|
||||
on:click|once={() => {
|
||||
$initialArgsStore = job?.args
|
||||
goto(
|
||||
`${stem}/edit/${
|
||||
job?.script_path
|
||||
}${isScript ? `` : `?nodraft=true`}`
|
||||
)
|
||||
goto(`${stem}/edit/${job?.script_path}${isScript ? `` : `?nodraft=true`}`)
|
||||
}}
|
||||
color="blue"
|
||||
size="sm"
|
||||
@@ -753,7 +746,7 @@
|
||||
<Skeleton loading={!job} layout={[[9.5]]} />
|
||||
{#if job}
|
||||
<FlowMetadata {job} {scheduleEditor} />
|
||||
{#if currentJobIsLongRunning && showExplicitProgressTip && !scriptProgress && 'running' in job}
|
||||
{#if currentJobIsLongRunning && showExplicitProgressTip && !scriptProgress && 'running' in job}
|
||||
<Alert
|
||||
class="mt-4 p-1 flex flex-row relative text-center"
|
||||
size="xs"
|
||||
@@ -801,7 +794,7 @@
|
||||
/>
|
||||
{/if}
|
||||
{#if scriptProgress}
|
||||
<JobProgressBar {job} {scriptProgress} class="py-4" hideStepTitle={true}/>
|
||||
<JobProgressBar {job} {scriptProgress} class="py-4" hideStepTitle={true} />
|
||||
{/if}
|
||||
<!-- Logs and outputs-->
|
||||
<div class="mr-2 sm:mr-0 mt-12">
|
||||
|
||||
@@ -660,8 +660,7 @@
|
||||
{/if}
|
||||
<div class="flex flex-row gap-x-2 flex-wrap items-center">
|
||||
<span class="text-sm text-tertiary">
|
||||
Edited <TimeAgo withDate date={script.created_at || ''} /> by {script.created_by ||
|
||||
'unknown'}
|
||||
Edited <TimeAgo date={script.created_at || ''} /> by {script.created_by || 'unknown'}
|
||||
</span>
|
||||
<Badge small color="gray">
|
||||
{truncateHash(script?.hash ?? '')}
|
||||
|
||||
@@ -629,7 +629,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal>
|
||||
<Portal name="workspace-settings">
|
||||
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={false} fromWorkspaceSettings={true} />
|
||||
</Portal>
|
||||
|
||||
@@ -1053,9 +1053,12 @@
|
||||
{:else if tab == 'windmill_lfs'}
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class=" text-primary text-lg font-semibold">Workspace object storage (S3/Azure Blob)</div>
|
||||
<div class=" text-primary text-lg font-semibold"
|
||||
>Workspace object storage (S3/Azure Blob)</div
|
||||
>
|
||||
<div class="text-tertiary text-xs">
|
||||
Connect your Windmill workspace to your S3 bucket or your Azure Blob storage to enable users to read and write from S3 without having to have access to the credentials.
|
||||
Connect your Windmill workspace to your S3 bucket or your Azure Blob storage to enable
|
||||
users to read and write from S3 without having to have access to the credentials.
|
||||
<a
|
||||
href="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#workspace-object-storage"
|
||||
target="_blank"
|
||||
@@ -1074,8 +1077,11 @@
|
||||
<Alert type="info" title="Logs storage is set at the instance level">
|
||||
This setting is only for storage of large files allowing to upload files directly to
|
||||
object storage using S3Object and use the wmill sdk to read and write large files backed
|
||||
by an object storage. Large-scale log management and distributed dependency caching is under <a href="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#instance-object-storage" class="text-blue-500">Instance object storage</a>, set by the superadmins in the
|
||||
instance settings UI.
|
||||
by an object storage. Large-scale log management and distributed dependency caching is
|
||||
under <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/object_storage_in_windmill#instance-object-storage"
|
||||
class="text-blue-500">Instance object storage</a
|
||||
>, set by the superadmins in the instance settings UI.
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if s3ResourceSettings}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'App Connection' }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user