mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
fix(frontend): App Editor v0 (#886)
This commit is contained in:
+23
-23
@@ -428,29 +428,6 @@
|
||||
},
|
||||
"query": "SELECT dependency_job FROM flow WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"190cb52762c82b4c7f318decf73cfb4377825c93b5777904b82f8940bb18be3e": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO app\n (workspace_id, path, summary, policy)\n VALUES ($1, $2, $3, $4) RETURNING id"
|
||||
},
|
||||
"1ad8677694aca94ee0e6da287d7cc028dcf673583a0e3e4fedd0e5d6766c5860": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2683,6 +2660,29 @@
|
||||
},
|
||||
"query": "DELETE FROM token WHERE token = $1 RETURNING email"
|
||||
},
|
||||
"9def921a0b697e4cbcb6ad88196ff6f66358f3b819aaf4e279596ccfc433fa92": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO app\n (workspace_id, path, summary, policy, versions)\n VALUES ($1, $2, $3, $4, '{}') RETURNING id"
|
||||
},
|
||||
"a227548b6604c56bfc15eb780bd8ee72a89dc6701a50f5048e928bd87baa7b9a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
@@ -2486,6 +2486,8 @@ paths:
|
||||
required:
|
||||
- content
|
||||
- language
|
||||
force_viewer_static_fields:
|
||||
type: object
|
||||
required:
|
||||
- args
|
||||
|
||||
@@ -4758,6 +4760,18 @@ components:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
required:
|
||||
- id
|
||||
- workspace_id
|
||||
- path
|
||||
- summary
|
||||
- versions
|
||||
- created_by
|
||||
- created_at
|
||||
- value
|
||||
- policy
|
||||
- execution_mode
|
||||
- extra_perms
|
||||
|
||||
SlackToken:
|
||||
type: object
|
||||
|
||||
@@ -80,7 +80,7 @@ pub struct AppWithLastVersion {
|
||||
|
||||
pub type StaticFields = Map<String, Value>;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ExecutionMode {
|
||||
Anonymous,
|
||||
@@ -88,7 +88,7 @@ pub enum ExecutionMode {
|
||||
Viewer,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Policy {
|
||||
pub on_behalf_of: Option<String>,
|
||||
//paths:
|
||||
@@ -208,8 +208,8 @@ async fn create_app(
|
||||
|
||||
let id = sqlx::query_scalar!(
|
||||
"INSERT INTO app
|
||||
(workspace_id, path, summary, policy)
|
||||
VALUES ($1, $2, $3, $4) RETURNING id",
|
||||
(workspace_id, path, summary, policy, versions)
|
||||
VALUES ($1, $2, $3, $4, '{}') RETURNING id",
|
||||
w_id,
|
||||
app.path,
|
||||
app.summary,
|
||||
@@ -249,7 +249,7 @@ async fn create_app(
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok((StatusCode::CREATED, format!("app {} created", app.path)))
|
||||
Ok((StatusCode::CREATED, app.path))
|
||||
}
|
||||
|
||||
async fn delete_app(
|
||||
@@ -371,13 +371,15 @@ async fn update_app(
|
||||
Ok(format!("app {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ExecuteApp {
|
||||
pub args: Map<String, serde_json::Value>,
|
||||
// - script: script/<path>
|
||||
// - flow: flow/<path>
|
||||
pub path: Option<String>,
|
||||
pub raw_code: Option<RawCode>,
|
||||
// if set, the app is executed as viewer with the given static fields
|
||||
pub force_viewer_static_fields: Option<StaticFields>,
|
||||
}
|
||||
|
||||
fn digest(code: &str) -> String {
|
||||
@@ -422,6 +424,24 @@ async fn execute_component(
|
||||
|
||||
let policy = serde_json::from_value::<Policy>(policy).map_err(to_anyhow)?;
|
||||
|
||||
let policy = if let Some(static_fields) = payload.clone().force_viewer_static_fields {
|
||||
let mut hm = HashMap::new();
|
||||
if let Some(path) = payload.path.clone() {
|
||||
hm.insert(path, static_fields);
|
||||
} else {
|
||||
hm.insert(
|
||||
format!(
|
||||
"rawcode/{}",
|
||||
digest(payload.raw_code.clone().unwrap().content.as_str())
|
||||
),
|
||||
static_fields,
|
||||
);
|
||||
}
|
||||
Policy { execution_mode: ExecutionMode::Viewer, triggerables: hm, on_behalf_of: None }
|
||||
} else {
|
||||
policy
|
||||
};
|
||||
|
||||
let (username, permissioned_as) = match policy.execution_mode {
|
||||
ExecutionMode::Anonymous => {
|
||||
let username = opt_authed
|
||||
@@ -464,14 +484,14 @@ async fn execute_component(
|
||||
};
|
||||
|
||||
let (job_payload, args) = match &payload {
|
||||
ExecuteApp { args, raw_code: Some(raw_code), path: None } => {
|
||||
ExecuteApp { args, raw_code: Some(raw_code), path: None, .. } => {
|
||||
let content = &raw_code.content;
|
||||
let payload = JobPayload::Code(raw_code.clone());
|
||||
let path = digest(content);
|
||||
let args = build_args(policy, path, args)?;
|
||||
(payload, args)
|
||||
}
|
||||
ExecuteApp { args, raw_code: None, path: Some(path) } => {
|
||||
ExecuteApp { args, raw_code: None, path: Some(path), .. } => {
|
||||
let payload = if path.starts_with("script/") {
|
||||
script_path_to_payload(path.strip_prefix("script/").unwrap(), &mut tx, &w_id)
|
||||
.await?
|
||||
|
||||
@@ -1309,6 +1309,7 @@ run();
|
||||
.spawn()?
|
||||
} else {
|
||||
let mut args = Vec::new();
|
||||
let script_path = format!("{job_dir}/main.ts");
|
||||
args.push("run");
|
||||
if lockfile.is_some() {
|
||||
args.push("--lock=/tmp/lock.json");
|
||||
@@ -1316,7 +1317,7 @@ run();
|
||||
args.push("--unstable");
|
||||
args.push("--v8-flags=--max-heap-size=2048");
|
||||
args.push("-A");
|
||||
args.push("/tmp/main.ts");
|
||||
args.push(&script_path);
|
||||
Command::new(deno_path)
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
|
||||
Generated
+52
-37
@@ -51,6 +51,7 @@
|
||||
"svelte": "^3.52.0",
|
||||
"svelte-awesome": "^3.0.0",
|
||||
"svelte-check": "^2.9.2",
|
||||
"svelte-dnd-action": "^0.9.21",
|
||||
"svelte-heros": "^2.3.5",
|
||||
"svelte-highlight": "^6.2.1",
|
||||
"svelte-markdown": "^0.2.3",
|
||||
@@ -780,9 +781,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@zerodevx/svelte-toast": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.8.1.tgz",
|
||||
"integrity": "sha512-XofCfaHj79uNiKc5leWUxO6LP/Xcawvi3B6Kb1gPCaBf/X5//HdZ/OIfG3z//QuKyFRUmEkDkoMzmbYWTUW2jw==",
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.8.2.tgz",
|
||||
"integrity": "sha512-EDtZ/Hw37T/UWCQ5drhMss0J9vItYUSDivQ3+mET5My6No7YNiNQklj2bkE61UAzut2TjHJfOJNBZsj78ODFtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -2261,23 +2262,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.14.53",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz",
|
||||
"integrity": "sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.14.53",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz",
|
||||
@@ -2482,6 +2466,23 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild/node_modules/esbuild-linux-64": {
|
||||
"version": "0.14.53",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz",
|
||||
"integrity": "sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -5881,6 +5882,12 @@
|
||||
"svelte": "^3.24.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-dnd-action": {
|
||||
"version": "0.9.21",
|
||||
"resolved": "https://registry.npmjs.org/svelte-dnd-action/-/svelte-dnd-action-0.9.21.tgz",
|
||||
"integrity": "sha512-uSiaGiSY5KCTx5OtColSk/1qFFUAzGvnoQYDE5xfOmcyvf3lAI1IjPv0p0LRKrTudS9rGZYWPx44qsD09ER6QQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/svelte-heros": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-heros/-/svelte-heros-2.3.5.tgz",
|
||||
@@ -6154,9 +6161,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.2.tgz",
|
||||
"integrity": "sha512-c2GtSdqg+harR4QeoTmex0Ngfg8IIHNeLQH5yr2B9uZbZR1Xt1rYbjWOWTcj3YLTZhrmZnPowoQDbSRFyZHQ5Q==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.3.tgz",
|
||||
"integrity": "sha512-Xt9D4PK4zuuQCEB8bwK9JUCKmTgUwyac/6b0/42Vqhgl6YJkep+Wf5wq+5uXYfmrupdAD0YY2NY1hyZp1HjRrg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"arg": "^5.0.2",
|
||||
@@ -7315,9 +7322,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"@zerodevx/svelte-toast": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.8.1.tgz",
|
||||
"integrity": "sha512-XofCfaHj79uNiKc5leWUxO6LP/Xcawvi3B6Kb1gPCaBf/X5//HdZ/OIfG3z//QuKyFRUmEkDkoMzmbYWTUW2jw==",
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.8.2.tgz",
|
||||
"integrity": "sha512-EDtZ/Hw37T/UWCQ5drhMss0J9vItYUSDivQ3+mET5My6No7YNiNQklj2bkE61UAzut2TjHJfOJNBZsj78ODFtw==",
|
||||
"dev": true
|
||||
},
|
||||
"accepts": {
|
||||
@@ -8299,6 +8306,16 @@
|
||||
"esbuild-windows-32": "0.14.53",
|
||||
"esbuild-windows-64": "0.14.53",
|
||||
"esbuild-windows-arm64": "0.14.53"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild-linux-64": {
|
||||
"version": "0.14.53",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz",
|
||||
"integrity": "sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"esbuild-android-64": {
|
||||
@@ -8357,14 +8374,6 @@
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"esbuild-linux-64": {
|
||||
"version": "0.14.53",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz",
|
||||
"integrity": "sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"esbuild-linux-arm": {
|
||||
"version": "0.14.53",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz",
|
||||
@@ -10936,6 +10945,12 @@
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"svelte-dnd-action": {
|
||||
"version": "0.9.21",
|
||||
"resolved": "https://registry.npmjs.org/svelte-dnd-action/-/svelte-dnd-action-0.9.21.tgz",
|
||||
"integrity": "sha512-uSiaGiSY5KCTx5OtColSk/1qFFUAzGvnoQYDE5xfOmcyvf3lAI1IjPv0p0LRKrTudS9rGZYWPx44qsD09ER6QQ==",
|
||||
"dev": true
|
||||
},
|
||||
"svelte-heros": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-heros/-/svelte-heros-2.3.5.tgz",
|
||||
@@ -11122,9 +11137,9 @@
|
||||
}
|
||||
},
|
||||
"tailwindcss": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.2.tgz",
|
||||
"integrity": "sha512-c2GtSdqg+harR4QeoTmex0Ngfg8IIHNeLQH5yr2B9uZbZR1Xt1rYbjWOWTcj3YLTZhrmZnPowoQDbSRFyZHQ5Q==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.3.tgz",
|
||||
"integrity": "sha512-Xt9D4PK4zuuQCEB8bwK9JUCKmTgUwyac/6b0/42Vqhgl6YJkep+Wf5wq+5uXYfmrupdAD0YY2NY1hyZp1HjRrg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arg": "^5.0.2",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"svelte": "^3.52.0",
|
||||
"svelte-awesome": "^3.0.0",
|
||||
"svelte-check": "^2.9.2",
|
||||
"svelte-dnd-action": "^0.9.21",
|
||||
"svelte-heros": "^2.3.5",
|
||||
"svelte-highlight": "^6.2.1",
|
||||
"svelte-markdown": "^0.2.3",
|
||||
|
||||
Vendored
+9
@@ -1 +1,10 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
declare type Item = import('svelte-dnd-action').Item
|
||||
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>
|
||||
declare namespace svelte.JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
|
||||
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import Required from './Required.svelte'
|
||||
import Popover from './Popover.svelte'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule'
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
|
||||
export let meta: Meta = {
|
||||
ownerKind: 'user',
|
||||
owner: '',
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
export let inputTransform = false
|
||||
export let schema: Schema
|
||||
export let args: Record<string, InputTransform | any> = {}
|
||||
export let disabledArgs: string[] = []
|
||||
|
||||
export let editableSchema = false
|
||||
export let isValid: boolean = true
|
||||
export let extraLib: string = 'missing extraLib'
|
||||
@@ -64,6 +66,7 @@
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
properties={schema.properties[argName].properties}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
disabled={disabledArgs.includes(argName)}
|
||||
{editableSchema}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Job, JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
import type { Preview } from '$lib/gen/models/Preview'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let isLoading = false
|
||||
export let job: Job | undefined = undefined
|
||||
export let workspaceOverride: string | undefined = undefined
|
||||
export let notfound = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: workspace = workspaceOverride ?? $workspaceStore
|
||||
let intervalId: NodeJS.Timer
|
||||
|
||||
let syncIteration: number = 0
|
||||
let ITERATIONS_BEFORE_SLOW_REFRESH = 100
|
||||
|
||||
export async function runPreview(
|
||||
path: string | undefined,
|
||||
code: string,
|
||||
lang: 'deno' | 'go' | 'python3' | 'bash',
|
||||
args: Record<string, any>
|
||||
): Promise<void> {
|
||||
export async function abstractRun(fn: () => Promise<string>) {
|
||||
try {
|
||||
intervalId && clearInterval(intervalId)
|
||||
if (isLoading && job) {
|
||||
@@ -37,8 +30,49 @@
|
||||
}
|
||||
isLoading = true
|
||||
|
||||
const testId = await JobService.runScriptPreview({
|
||||
workspace: workspace!,
|
||||
const testId = await fn()
|
||||
await watchJob(testId)
|
||||
} catch (err) {
|
||||
isLoading = false
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function runScriptByPath(
|
||||
path: string | undefined,
|
||||
args: Record<string, any>
|
||||
): Promise<void> {
|
||||
abstractRun(() =>
|
||||
JobService.runScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: path ?? '',
|
||||
requestBody: args
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function runFlowByPath(
|
||||
path: string | undefined,
|
||||
args: Record<string, any>
|
||||
): Promise<void> {
|
||||
abstractRun(() =>
|
||||
JobService.runFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: path ?? '',
|
||||
requestBody: args
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function runPreview(
|
||||
path: string | undefined,
|
||||
code: string,
|
||||
lang: 'deno' | 'go' | 'python3' | 'bash',
|
||||
args: Record<string, any>
|
||||
): Promise<void> {
|
||||
abstractRun(() =>
|
||||
JobService.runScriptPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
content: code,
|
||||
@@ -46,11 +80,7 @@
|
||||
language: lang as Preview.language
|
||||
}
|
||||
})
|
||||
await watchJob(testId)
|
||||
} catch (err) {
|
||||
isLoading = false
|
||||
throw err
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export async function cancelJob() {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import { Bar } from 'svelte-chartjs'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
BarElement
|
||||
} from 'chart.js'
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
BarElement
|
||||
)
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
export const schema: Schema = emptySchema()
|
||||
export const staticOutputs: string[] = []
|
||||
|
||||
const data = {
|
||||
labels: ['Red', 'Blue', 'Yellow'],
|
||||
datasets: [
|
||||
{
|
||||
label: '% of Votes',
|
||||
data: [12, 19, 3],
|
||||
backgroundColor: [
|
||||
'rgba(255, 134,159,0.4)',
|
||||
'rgba(98, 182, 239,0.4)',
|
||||
'rgba(255, 218, 128,0.4)'
|
||||
],
|
||||
borderWidth: 2,
|
||||
borderColor: ['rgba(255, 134, 159, 1)', 'rgba(98, 182, 239, 1)', 'rgba(255, 218, 128, 1)']
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $worldStore}
|
||||
<Bar {data} options={{ responsive: true }} />
|
||||
{/if}
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, ComponentInputsSpec } from '../types'
|
||||
|
||||
export let componentInputs: ComponentInputsSpec
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
$: inputResult =
|
||||
componentInputs.result.id && componentInputs.result.name
|
||||
? $worldStore?.connect<any>(componentInputs.result, (x) => {
|
||||
update()
|
||||
})
|
||||
: undefined
|
||||
|
||||
let result: any
|
||||
|
||||
function update() {
|
||||
result = inputResult?.peak()
|
||||
}
|
||||
|
||||
export const staticOutputs: string[] = []
|
||||
</script>
|
||||
|
||||
{#if $worldStore}
|
||||
<DisplayResult {result} />
|
||||
{/if}
|
||||
@@ -0,0 +1,130 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { Button } from '$lib/components/common'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
|
||||
import { AppService, type CompletedJob } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { faArrowsRotate, faFile } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import type { Output } from '../rx'
|
||||
import type { AppEditorContext, InputsSpec } from '../types'
|
||||
import { buildArgs, loadSchema, schemaToInputsSpec } from '../utils'
|
||||
|
||||
// Component props
|
||||
export let id: string
|
||||
export let inputs: InputsSpec
|
||||
export let path: string | undefined = undefined
|
||||
export let runType: 'script' | 'flow' | undefined = undefined
|
||||
|
||||
export const staticOutputs = ['loading', 'result']
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
let pagePath = $page.params.path
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<any>
|
||||
loading: Output<boolean>
|
||||
}
|
||||
|
||||
// Local state
|
||||
let args: Record<string, any> = {}
|
||||
let schema: Schema | undefined = undefined
|
||||
let schemaClone: Schema | undefined = undefined
|
||||
|
||||
let isValid = true
|
||||
let testIsLoading = false
|
||||
let testJob: CompletedJob | undefined = undefined
|
||||
let testJobLoader: TestJobLoader | undefined = undefined
|
||||
|
||||
$: if ($workspaceStore && path && runType) {
|
||||
loadSchemaFromTriggerable($workspaceStore, path, runType)
|
||||
}
|
||||
|
||||
$: if (inputs && schema !== undefined) {
|
||||
if (Object.keys(schema.properties).length !== Object.keys(inputs).length) {
|
||||
inputs = schemaToInputsSpec(schema)
|
||||
}
|
||||
|
||||
reloadSchemaAndArgs()
|
||||
}
|
||||
|
||||
// Load once
|
||||
async function loadSchemaFromTriggerable(
|
||||
workspace: string,
|
||||
path: string,
|
||||
runType: 'script' | 'flow'
|
||||
) {
|
||||
schema = await loadSchema(workspace, path, runType)
|
||||
args = buildArgs(inputs, schema)
|
||||
}
|
||||
|
||||
async function reloadSchemaAndArgs() {
|
||||
schemaClone = JSON.parse(JSON.stringify(schema))
|
||||
|
||||
if (schemaClone !== undefined) {
|
||||
args = buildArgs(inputs, schemaClone)
|
||||
|
||||
Object.keys(schemaClone.properties).forEach((propKey) => {
|
||||
if (!Object.keys(args).includes(propKey)) {
|
||||
delete schemaClone!.properties[propKey]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$: disabledArgs = Object.keys(inputs).reduce((a: string[], c: string) => {
|
||||
if (inputs[c].type === 'static') {
|
||||
a = [...a, c]
|
||||
}
|
||||
return a
|
||||
}, [])
|
||||
|
||||
async function executeComponent() {
|
||||
await testJobLoader?.abstractRun(() =>
|
||||
AppService.executeComponent({
|
||||
workspace: $workspaceStore!,
|
||||
path: pagePath,
|
||||
requestBody: {
|
||||
path: `${runType}/${path}`,
|
||||
args,
|
||||
force_viewer_static_fields: {}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
outputs?.loading.set(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
on:done={() => {
|
||||
if (testJob) {
|
||||
outputs?.result.set(testJob?.result)
|
||||
outputs?.loading.set(false)
|
||||
}
|
||||
}}
|
||||
bind:isLoading={testIsLoading}
|
||||
bind:job={testJob}
|
||||
bind:this={testJobLoader}
|
||||
/>
|
||||
|
||||
{#if schemaClone !== undefined}
|
||||
<SchemaForm schema={schemaClone} bind:args bind:isValid {disabledArgs} />
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="border"
|
||||
on:click={() => executeComponent()}
|
||||
startIcon={{ icon: faFile }}
|
||||
disabled={!isValid}
|
||||
>
|
||||
<div>
|
||||
Submit
|
||||
{#if testIsLoading}
|
||||
<Icon data={faArrowsRotate} class="animate-spin ml-2" scale={0.8} />
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import type {} from '$lib/common'
|
||||
import { classNames } from '$lib/utils'
|
||||
|
||||
export let id: string
|
||||
export let title: string
|
||||
export let description: string | undefined = undefined
|
||||
|
||||
export let headers: string[]
|
||||
export let data: Array<Record<string, any>>
|
||||
|
||||
export const staticOutputs: string[] = []
|
||||
|
||||
let query: string = ''
|
||||
let page: number = 1
|
||||
</script>
|
||||
|
||||
<div class="p-8 w-full">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-xl font-semibold text-gray-900">{title}</h1>
|
||||
{#if description}
|
||||
<p class="mt-2 text-sm text-gray-700">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4 flex flex-col">
|
||||
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
{#each headers as header}
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{header}
|
||||
</th>
|
||||
{/each}
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each data as x}
|
||||
<tr>
|
||||
{#each headers as header}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{x[header]}
|
||||
</td>
|
||||
{/each}
|
||||
<td
|
||||
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
|
||||
>
|
||||
<a href="#" class="text-indigo-600 hover:text-indigo-900">
|
||||
Edit
|
||||
<span class="sr-only">, Lindsay Walton </span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<ul class="inline-flex -space-x-px">
|
||||
<li>
|
||||
<button
|
||||
on:click={() => (page -= 1)}
|
||||
class="text-sm py-2 px-4 text-gray-500 bg-white rounded-l-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{#each Array(5) as x, i}
|
||||
<li>
|
||||
<button
|
||||
on:click={() => (page = i)}
|
||||
class={classNames(
|
||||
'text-sm py-2 px-4 text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700',
|
||||
page === i ? 'bg-blue-100 font-bold' : 'bg-white'
|
||||
)}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
<li>
|
||||
<button
|
||||
on:click={() => (page += 1)}
|
||||
class="text-sm py-2 px-4 text-gray-500 bg-white rounded-r-lg border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts">
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import { onMount, setContext } from 'svelte'
|
||||
|
||||
import { Pane } from 'svelte-splitpanes'
|
||||
import { writable } from 'svelte/store'
|
||||
import { buildWorld, type World } from '../rx'
|
||||
import type { App, AppEditorContext, AppSelection, ConnectingInput, EditorMode } from '../types'
|
||||
import AppEditorHeader from './AppEditorHeader.svelte'
|
||||
import SectionsEditor from './SectionsEditor.svelte'
|
||||
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
|
||||
|
||||
import type { Schema } from '$lib/common'
|
||||
import SectionPanel from './settingsPanel/SectionPanel.svelte'
|
||||
import ComponentList from './componentsPanel/ComponentList.svelte'
|
||||
|
||||
export let app: App
|
||||
|
||||
console.log(app)
|
||||
const appStore = writable<App>(app)
|
||||
const worldStore = writable<World | undefined>(undefined)
|
||||
const staticOutputs = writable<Record<string, string[]>>({})
|
||||
|
||||
const selection = writable<AppSelection | undefined>(undefined)
|
||||
const mode = writable<EditorMode>('dnd')
|
||||
const schemas = writable<Schema[]>([])
|
||||
|
||||
const connectingInput = writable<ConnectingInput>({
|
||||
opened: false,
|
||||
input: undefined
|
||||
})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
worldStore,
|
||||
staticOutputs,
|
||||
app: appStore,
|
||||
selection,
|
||||
mode,
|
||||
schemas,
|
||||
connectingInput
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
$worldStore = buildWorld($staticOutputs)
|
||||
})
|
||||
</script>
|
||||
|
||||
<AppEditorHeader title="Sample app" bind:mode={$mode} />
|
||||
<SplitPanesWrapper>
|
||||
<Pane minSize={20} maxSize={30} size={20}>
|
||||
<ComponentList />
|
||||
</Pane>
|
||||
<Pane>
|
||||
<SectionsEditor bind:sections={$appStore.sections} mode={$mode} />
|
||||
</Pane>
|
||||
<Pane minSize={20} maxSize={30} size={20}>
|
||||
{#if $selection?.sectionIndex !== undefined && $selection?.componentIndex !== undefined}
|
||||
<ComponentPanel
|
||||
bind:component={$appStore.sections[$selection?.sectionIndex].components[
|
||||
$selection?.componentIndex
|
||||
]}
|
||||
on:remove={() => {
|
||||
if ($selection?.sectionIndex !== undefined && $selection?.componentIndex !== undefined) {
|
||||
$appStore.sections[$selection?.sectionIndex].components.splice(
|
||||
$selection?.componentIndex,
|
||||
1
|
||||
)
|
||||
$appStore = $appStore
|
||||
$selection = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if $selection?.sectionIndex !== undefined}
|
||||
<SectionPanel
|
||||
bind:section={$appStore.sections[$selection.sectionIndex]}
|
||||
on:remove={() => {
|
||||
if ($selection?.sectionIndex !== undefined) {
|
||||
$appStore.sections.splice($selection?.sectionIndex, 1)
|
||||
$appStore = $appStore
|
||||
$selection = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Pane>
|
||||
</SplitPanesWrapper>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
|
||||
import { AppService, Policy } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { faArrowsLeftRight, faExternalLink, faHand } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, EditorMode } from '../types'
|
||||
|
||||
export let title: string
|
||||
export let mode: EditorMode
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
async function save() {
|
||||
await AppService.updateApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path,
|
||||
requestBody: {
|
||||
value: $app!,
|
||||
summary: 'App summary',
|
||||
policy: {
|
||||
triggerables: {},
|
||||
execution_mode: Policy.execution_mode.PUBLISHER,
|
||||
on_behalf_of: `u/${$userStore?.username}`
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log('App saved')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="border-b h-12 flex flex-row justify-between py-2 px-4 items-center">
|
||||
<span class="text-sm">{title}</span>
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={mode}>
|
||||
<ToggleButton position="left" value="dnd" startIcon={{ icon: faHand }} size="sm">
|
||||
Component editor
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value="width"
|
||||
startIcon={{ icon: faArrowsLeftRight }}
|
||||
size="sm"
|
||||
>
|
||||
Width editor
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button color="dark" size="sm" variant="border" startIcon={{ icon: faExternalLink }}>
|
||||
Publish
|
||||
</Button>
|
||||
<Button on:click={save} size="sm">Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import BarChartComponent from '../components/BarChartComponent.svelte'
|
||||
import DisplayComponent from '../components/DisplayComponent.svelte'
|
||||
import RunFormComponent from '../components/RunFormComponent.svelte'
|
||||
import TableComponent from '../components/TableComponent.svelte'
|
||||
import type { AppComponent, AppEditorContext } from '../types'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
|
||||
const { staticOutputs, app, connectingInput } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function connectInput(output: string) {
|
||||
if ($connectingInput) {
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
input: {
|
||||
id: component.id,
|
||||
name: output,
|
||||
type: 'output',
|
||||
defaultValue: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if component}
|
||||
<div class="h-full flex flex-col w-full">
|
||||
<span
|
||||
class={classNames(
|
||||
' text-white px-2 text-xs py-1 font-bold rounded-t-sm w-fit ',
|
||||
selected ? 'bg-indigo-500' : 'bg-gray-500'
|
||||
)}
|
||||
>
|
||||
{component.type}
|
||||
</span>
|
||||
<div
|
||||
class={classNames(
|
||||
'p-2 border overflow-auto cursor-pointer hover:bg-blue-100 h-full bg-white relative',
|
||||
selected ? 'border-indigo-400' : 'border-gray-400'
|
||||
)}
|
||||
>
|
||||
{#if $connectingInput.opened && $staticOutputs[component.id]}
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 right-0 bg-opacity-80 w-full h-full bg-gray-500 flex justify-center items-center flex-col gap-2"
|
||||
>
|
||||
{#each $staticOutputs[component.id] as output}
|
||||
<Button color="dark" on:click={() => connectInput(output)}>
|
||||
{output}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if component.type === 'runformcomponent'}
|
||||
<RunFormComponent
|
||||
{...component}
|
||||
bind:inputs={component.inputs}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{:else if component.type === 'displaycomponent'}
|
||||
<DisplayComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{:else if component.type === 'barchartcomponent'}
|
||||
<BarChartComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{:else if component.type === 'tablecomponent'}
|
||||
<TableComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,232 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppComponent, AppEditorContext } from '../types'
|
||||
import ComponentEditor from './ComponentEditor.svelte'
|
||||
import { dndzone } from 'svelte-dnd-action'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { getNextId } from '$lib/components/flows/flowStateUtils'
|
||||
|
||||
export let components: AppComponent[]
|
||||
export let sectionIndex: number
|
||||
export let columns: number
|
||||
|
||||
const flipDurationMs = 200
|
||||
const { selection, connectingInput } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function handleDndConsider(event: CustomEvent<DndEvent<AppComponent>>) {
|
||||
components = event.detail.items
|
||||
}
|
||||
|
||||
function handleDndFinalize(event: CustomEvent<DndEvent<AppComponent>>) {
|
||||
const totalWidth = components
|
||||
.map((c) => c.width)
|
||||
.filter(Boolean)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
|
||||
components = event.detail.items.map((item) => {
|
||||
if (item.width === undefined) {
|
||||
item.width = 100 - totalWidth
|
||||
|
||||
const id = getNextId(
|
||||
components.map((c) => c.id).filter((id) => id !== event.detail.info.id)
|
||||
)
|
||||
|
||||
item.id = id
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// HACK
|
||||
onMount(() => {
|
||||
// get all div elements with the id "component"
|
||||
const divs = document.querySelectorAll<HTMLDivElement>('#component')
|
||||
// loop through the divs
|
||||
divs.forEach((div, index) => {
|
||||
// set component width
|
||||
div.style.width = `${Math.round(components[index].width)}%`
|
||||
})
|
||||
})
|
||||
|
||||
const numberToTailwindWidthMap = {
|
||||
1: 'w-[1%]',
|
||||
2: 'w-[2%]',
|
||||
3: 'w-[3%]',
|
||||
4: 'w-[4%]',
|
||||
5: 'w-[5%]',
|
||||
6: 'w-[6%]',
|
||||
7: 'w-[7%]',
|
||||
8: 'w-[8%]',
|
||||
9: 'w-[9%]',
|
||||
10: 'w-[10%]',
|
||||
11: 'w-[11%]',
|
||||
12: 'w-[12%]',
|
||||
13: 'w-[13%]',
|
||||
14: 'w-[14%]',
|
||||
15: 'w-[15%]',
|
||||
16: 'w-[16%]',
|
||||
17: 'w-[17%]',
|
||||
18: 'w-[18%]',
|
||||
19: 'w-[19%]',
|
||||
20: 'w-[20%]',
|
||||
21: 'w-[21%]',
|
||||
22: 'w-[22%]',
|
||||
23: 'w-[23%]',
|
||||
24: 'w-[24%]',
|
||||
25: 'w-[25%]',
|
||||
26: 'w-[26%]',
|
||||
27: 'w-[27%]',
|
||||
28: 'w-[28%]',
|
||||
29: 'w-[29%]',
|
||||
30: 'w-[30%]',
|
||||
31: 'w-[31%]',
|
||||
32: 'w-[32%]',
|
||||
33: 'w-[33%]',
|
||||
34: 'w-[34%]',
|
||||
35: 'w-[35%]',
|
||||
36: 'w-[36%]',
|
||||
37: 'w-[37%]',
|
||||
38: 'w-[38%]',
|
||||
39: 'w-[39%]',
|
||||
40: 'w-[40%]',
|
||||
41: 'w-[41%]',
|
||||
42: 'w-[42%]',
|
||||
43: 'w-[43%]',
|
||||
44: 'w-[44%]',
|
||||
45: 'w-[45%]',
|
||||
46: 'w-[46%]',
|
||||
47: 'w-[47%]',
|
||||
48: 'w-[48%]',
|
||||
49: 'w-[49%]',
|
||||
50: 'w-[50%]',
|
||||
51: 'w-[51%]',
|
||||
52: 'w-[52%]',
|
||||
53: 'w-[53%]',
|
||||
54: 'w-[54%]',
|
||||
55: 'w-[55%]',
|
||||
56: 'w-[56%]',
|
||||
57: 'w-[57%]',
|
||||
58: 'w-[58%]',
|
||||
59: 'w-[59%]',
|
||||
60: 'w-[60%]',
|
||||
61: 'w-[61%]',
|
||||
62: 'w-[62%]',
|
||||
63: 'w-[63%]',
|
||||
64: 'w-[64%]',
|
||||
65: 'w-[65%]',
|
||||
66: 'w-[66%]',
|
||||
67: 'w-[67%]',
|
||||
68: 'w-[68%]',
|
||||
69: 'w-[69%]',
|
||||
70: 'w-[70%]',
|
||||
71: 'w-[71%]',
|
||||
72: 'w-[72%]',
|
||||
73: 'w-[73%]',
|
||||
74: 'w-[74%]',
|
||||
75: 'w-[75%]',
|
||||
76: 'w-[76%]',
|
||||
77: 'w-[77%]',
|
||||
78: 'w-[78%]',
|
||||
79: 'w-[79%]',
|
||||
80: 'w-[80%]',
|
||||
81: 'w-[81%]',
|
||||
82: 'w-[82%]',
|
||||
83: 'w-[83%]',
|
||||
84: 'w-[84%]',
|
||||
85: 'w-[85%]',
|
||||
86: 'w-[86%]',
|
||||
87: 'w-[87%]',
|
||||
88: 'w-[88%]',
|
||||
89: 'w-[89%]',
|
||||
90: 'w-[90%]',
|
||||
91: 'w-[91%]',
|
||||
92: 'w-[92%]',
|
||||
93: 'w-[93%]',
|
||||
94: 'w-[94%]',
|
||||
95: 'w-[95%]',
|
||||
96: 'w-[96%]',
|
||||
97: 'w-[97%]',
|
||||
98: 'w-[98%]',
|
||||
99: 'w-[99%]',
|
||||
100: 'w-[100%]'
|
||||
}
|
||||
|
||||
$: sum = components.reduce((acc, component) => acc + component.width, 0)
|
||||
$: canDrop = components.length > 0 && components.length < columns
|
||||
</script>
|
||||
|
||||
<div class="h-80 rounded-b-sm flex-row p-4 flex border-2 border-gray bg-white cursor-pointer ">
|
||||
<div class="dotted-background h-full flex flex-row gap-1 w-full">
|
||||
<div
|
||||
class={classNames(
|
||||
'flex gap-1',
|
||||
canDrop ? numberToTailwindWidthMap[Math.round(sum)] : 'w-full'
|
||||
)}
|
||||
use:dndzone={{
|
||||
items: components,
|
||||
flipDurationMs,
|
||||
type: 'component',
|
||||
dropTargetStyle: {
|
||||
outline: 'dashed blue',
|
||||
outlineOffset: '2px'
|
||||
},
|
||||
dragDisabled: components.length === 0,
|
||||
dropFromOthersDisabled: components.length === columns
|
||||
}}
|
||||
on:consider={handleDndConsider}
|
||||
on:finalize={handleDndFinalize}
|
||||
>
|
||||
{#if components.length > 0}
|
||||
{#each components as component, componentIndex (component.id)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={numberToTailwindWidthMap[Math.round((100 * component.width) / sum)]}
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
on:click|stopPropagation={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selection = { componentIndex, sectionIndex }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ComponentEditor
|
||||
bind:component
|
||||
selected={componentIndex === $selection?.componentIndex &&
|
||||
sectionIndex === $selection?.sectionIndex}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex w-full gap-1">
|
||||
{#each Array(columns - components.length) as x}
|
||||
<div
|
||||
class="border flex justify-center flex-col items-center w-full bg-green-200 bg-opacity-50"
|
||||
>
|
||||
<div>Empty component</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if canDrop}
|
||||
<div class={classNames('h-full flex gap-2', numberToTailwindWidthMap[Math.round(100 - sum)])}>
|
||||
{#each Array(columns - components.length) as _}
|
||||
<div
|
||||
class="border flex justify-center flex-col items-center h-full w-full bg-green-200 bg-opacity-50"
|
||||
>
|
||||
<div>Empty component</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dotted-background {
|
||||
background-image: radial-gradient(circle at 1px 1px, #ccc 1px, transparent 0);
|
||||
background-size: 40px 40px;
|
||||
background-position: 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,264 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppSection, EditorMode } from '../types'
|
||||
import { dndzone } from 'svelte-dnd-action'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { getNextId } from '$lib/components/flows/flowStateUtils'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import ComponentsEditor from './ComponentsEditor.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import ComponentEditor from './ComponentEditor.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import RunFormComponent from '../components/RunFormComponent.svelte'
|
||||
import DisplayComponent from '../components/DisplayComponent.svelte'
|
||||
|
||||
export let sections: AppSection[]
|
||||
|
||||
console.log(sections)
|
||||
export let mode: EditorMode = 'width'
|
||||
|
||||
const flipDurationMs = 200
|
||||
const { selection, staticOutputs, app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function handleSort(e) {
|
||||
sections = e.detail.items
|
||||
}
|
||||
|
||||
function addEmptySection() {
|
||||
sections = [
|
||||
...sections,
|
||||
{
|
||||
components: [],
|
||||
columns: 3,
|
||||
id: getNextId(sections.map((s) => s.id)),
|
||||
title: 'New section',
|
||||
description: 'section'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const numberToTailwindWidthMap = {
|
||||
1: 'w-[1%]',
|
||||
2: 'w-[2%]',
|
||||
3: 'w-[3%]',
|
||||
4: 'w-[4%]',
|
||||
5: 'w-[5%]',
|
||||
6: 'w-[6%]',
|
||||
7: 'w-[7%]',
|
||||
8: 'w-[8%]',
|
||||
9: 'w-[9%]',
|
||||
10: 'w-[10%]',
|
||||
11: 'w-[11%]',
|
||||
12: 'w-[12%]',
|
||||
13: 'w-[13%]',
|
||||
14: 'w-[14%]',
|
||||
15: 'w-[15%]',
|
||||
16: 'w-[16%]',
|
||||
17: 'w-[17%]',
|
||||
18: 'w-[18%]',
|
||||
19: 'w-[19%]',
|
||||
20: 'w-[20%]',
|
||||
21: 'w-[21%]',
|
||||
22: 'w-[22%]',
|
||||
23: 'w-[23%]',
|
||||
24: 'w-[24%]',
|
||||
25: 'w-[25%]',
|
||||
26: 'w-[26%]',
|
||||
27: 'w-[27%]',
|
||||
28: 'w-[28%]',
|
||||
29: 'w-[29%]',
|
||||
30: 'w-[30%]',
|
||||
31: 'w-[31%]',
|
||||
32: 'w-[32%]',
|
||||
33: 'w-[33%]',
|
||||
34: 'w-[34%]',
|
||||
35: 'w-[35%]',
|
||||
36: 'w-[36%]',
|
||||
37: 'w-[37%]',
|
||||
38: 'w-[38%]',
|
||||
39: 'w-[39%]',
|
||||
40: 'w-[40%]',
|
||||
41: 'w-[41%]',
|
||||
42: 'w-[42%]',
|
||||
43: 'w-[43%]',
|
||||
44: 'w-[44%]',
|
||||
45: 'w-[45%]',
|
||||
46: 'w-[46%]',
|
||||
47: 'w-[47%]',
|
||||
48: 'w-[48%]',
|
||||
49: 'w-[49%]',
|
||||
50: 'w-[50%]',
|
||||
51: 'w-[51%]',
|
||||
52: 'w-[52%]',
|
||||
53: 'w-[53%]',
|
||||
54: 'w-[54%]',
|
||||
55: 'w-[55%]',
|
||||
56: 'w-[56%]',
|
||||
57: 'w-[57%]',
|
||||
58: 'w-[58%]',
|
||||
59: 'w-[59%]',
|
||||
60: 'w-[60%]',
|
||||
61: 'w-[61%]',
|
||||
62: 'w-[62%]',
|
||||
63: 'w-[63%]',
|
||||
64: 'w-[64%]',
|
||||
65: 'w-[65%]',
|
||||
66: 'w-[66%]',
|
||||
67: 'w-[67%]',
|
||||
68: 'w-[68%]',
|
||||
69: 'w-[69%]',
|
||||
70: 'w-[70%]',
|
||||
71: 'w-[71%]',
|
||||
72: 'w-[72%]',
|
||||
73: 'w-[73%]',
|
||||
74: 'w-[74%]',
|
||||
75: 'w-[75%]',
|
||||
76: 'w-[76%]',
|
||||
77: 'w-[77%]',
|
||||
78: 'w-[78%]',
|
||||
79: 'w-[79%]',
|
||||
80: 'w-[80%]',
|
||||
81: 'w-[81%]',
|
||||
82: 'w-[82%]',
|
||||
83: 'w-[83%]',
|
||||
84: 'w-[84%]',
|
||||
85: 'w-[85%]',
|
||||
86: 'w-[86%]',
|
||||
87: 'w-[87%]',
|
||||
88: 'w-[88%]',
|
||||
89: 'w-[89%]',
|
||||
90: 'w-[90%]',
|
||||
91: 'w-[91%]',
|
||||
92: 'w-[92%]',
|
||||
93: 'w-[93%]',
|
||||
94: 'w-[94%]',
|
||||
95: 'w-[95%]',
|
||||
96: 'w-[96%]',
|
||||
97: 'w-[97%]',
|
||||
98: 'w-[98%]',
|
||||
99: 'w-[99%]',
|
||||
100: 'w-[100%]'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dotted-background h-full w-full p-8">
|
||||
<div
|
||||
class="flex-col flex justify-start gap-8 pt-8 mb-8"
|
||||
use:dndzone={{
|
||||
items: sections,
|
||||
flipDurationMs,
|
||||
type: 'section',
|
||||
dragDisabled: mode === 'width',
|
||||
dropTargetStyle: {
|
||||
outline: 'dashed blue',
|
||||
outlineOffset: '8px'
|
||||
}
|
||||
}}
|
||||
on:consider={handleSort}
|
||||
on:finalize={handleSort}
|
||||
>
|
||||
{#each sections as section, sectionIndex (section.id)}
|
||||
{@const selected = $selection?.sectionIndex === sectionIndex}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<section
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
on:click={() => {
|
||||
$selection = { sectionIndex, componentIndex: undefined }
|
||||
}}
|
||||
>
|
||||
{#if mode !== 'preview'}
|
||||
<span
|
||||
class={classNames(
|
||||
'text-white px-2 text-sm py-1 font-bold',
|
||||
selected ? 'bg-indigo-500' : 'bg-gray-500'
|
||||
)}
|
||||
>
|
||||
Section {sectionIndex + 1}
|
||||
<Badge>{section.id}</Badge>
|
||||
</span>
|
||||
{/if}
|
||||
{#if mode === 'dnd'}
|
||||
<ComponentsEditor
|
||||
bind:components={section.components}
|
||||
columns={section.columns}
|
||||
{sectionIndex}
|
||||
/>
|
||||
{:else if mode === 'preview'}
|
||||
<div class="w-full flex bg-white">
|
||||
{#each section.components as component}
|
||||
<div class={classNames(numberToTailwindWidthMap[Math.round(component.width)])}>
|
||||
{#if component.type === 'runformcomponent'}
|
||||
<RunFormComponent
|
||||
{...component}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{:else if component.type === 'displaycomponent'}
|
||||
<DisplayComponent
|
||||
{...component}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if mode === 'width'}
|
||||
<div
|
||||
class="h-80 w-full rounded-b-sm flex-row gap-4 p-4 flex border-2 border-gray-200 bg-white cursor-pointer "
|
||||
>
|
||||
<Splitpanes>
|
||||
{#each section.components as component}
|
||||
<Pane bind:size={component.width} minSize={20}>
|
||||
<ComponentEditor bind:component selected={false} />
|
||||
</Pane>
|
||||
{/each}
|
||||
|
||||
{#if section.components.length < section.columns}
|
||||
<Pane
|
||||
size={100 - section.components.reduce((accu, curr) => accu + curr.width, 0)}
|
||||
minSize={20}
|
||||
class="gap-2 w-full flex flex-row"
|
||||
>
|
||||
{#each Array(section.columns - section.components.length) as _}
|
||||
<div
|
||||
class="border flex justify-center flex-col items-center w-full h-full bg-green-200 bg-opacity-50"
|
||||
>
|
||||
<div>Empty</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Pane>
|
||||
{/if}
|
||||
</Splitpanes>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
{#if mode !== 'preview'}
|
||||
<section>
|
||||
<span class="bg-blue-500 text-white px-2 text-sm py-1 font-bold rounded-t-sm">
|
||||
Empty section
|
||||
</span>
|
||||
<div class="h-96 border-2 border-blue-200 border-dashed bg-white flex">
|
||||
<Button
|
||||
btnClasses="m-auto"
|
||||
color="dark"
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => addEmptySection()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dotted-background {
|
||||
background-image: radial-gradient(circle at 1px 1px, #ccc 1px, transparent 0);
|
||||
background-size: 40px 40px;
|
||||
background-position: 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
import { flip } from 'svelte/animate'
|
||||
import { dndzone, TRIGGERS, SHADOW_ITEM_MARKER_PROPERTY_NAME } from 'svelte-dnd-action'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faWpforms } from '@fortawesome/free-brands-svg-icons'
|
||||
import { getNextId } from '$lib/components/flows/flowStateUtils'
|
||||
|
||||
const defaultProps = {
|
||||
horizontalAlignement: 'center',
|
||||
verticalAlignement: 'center',
|
||||
title: 'My title',
|
||||
description: 'My description',
|
||||
configSchema: undefined,
|
||||
inputs: {},
|
||||
componentInputs: {}
|
||||
}
|
||||
|
||||
let items = [
|
||||
{
|
||||
...defaultProps,
|
||||
// Used by the dnd library, should be replaced by unique id
|
||||
id: 'displaycomponent',
|
||||
type: 'displaycomponent',
|
||||
componentInputs: {
|
||||
result: {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
type: 'output',
|
||||
defaultValue: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
...defaultProps,
|
||||
// Used by the dnd library, should be replaced by unique id
|
||||
id: 'runformcomponent',
|
||||
type: 'runformcomponent'
|
||||
}
|
||||
]
|
||||
|
||||
const displayData = {
|
||||
displaycomponent: {
|
||||
name: 'Display component',
|
||||
icon: faDisplay
|
||||
},
|
||||
runformcomponent: {
|
||||
name: 'Run form',
|
||||
icon: faWpforms
|
||||
}
|
||||
}
|
||||
|
||||
const flipDurationMs = 300
|
||||
let shouldIgnoreDndEvents = false
|
||||
|
||||
function handleDndConsider(e) {
|
||||
const { trigger, id } = e.detail.info
|
||||
if (trigger === TRIGGERS.DRAG_STARTED) {
|
||||
const idx = items.findIndex((item) => item.id === id)
|
||||
|
||||
e.detail.items = e.detail.items.filter((item) => !item[SHADOW_ITEM_MARKER_PROPERTY_NAME])
|
||||
|
||||
e.detail.items.splice(idx, 0, {
|
||||
...items[idx],
|
||||
id: getNextId(e.detail.items.map((item) => item.id)),
|
||||
// @ts-ignore
|
||||
type: items[idx].type,
|
||||
width: 100
|
||||
})
|
||||
items = e.detail.items
|
||||
|
||||
shouldIgnoreDndEvents = true
|
||||
} else if (!shouldIgnoreDndEvents) {
|
||||
items = e.detail.items
|
||||
} else {
|
||||
items = [...items]
|
||||
}
|
||||
}
|
||||
|
||||
function handleDndFinalize(e) {
|
||||
if (!shouldIgnoreDndEvents) {
|
||||
items = e.detail.items
|
||||
} else {
|
||||
items = [...items]
|
||||
shouldIgnoreDndEvents = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
use:dndzone={{ items, flipDurationMs, type: 'component' }}
|
||||
on:consider={handleDndConsider}
|
||||
on:finalize={handleDndFinalize}
|
||||
class="grid grid-cols-2 gap-2 p-2"
|
||||
>
|
||||
{#each items as item (item.id)}
|
||||
<div
|
||||
class="border shadow-sm h-24 p-2 flex flex-col gap-2 items-center justify-center bg-white rounded-md"
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
>
|
||||
<Icon data={displayData[item.type].icon} scale={1.6} />
|
||||
<div class="text-xs">{displayData[item.type].name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { ComponentInputsSpec } from '../../types'
|
||||
import DynamicInputEditor from './DynamicInputEditor.svelte'
|
||||
|
||||
export let componentInputs: ComponentInputsSpec
|
||||
|
||||
let openedProp = Object.keys(componentInputs)[0]
|
||||
</script>
|
||||
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
{#each Object.keys(componentInputs) as inputSpecKey}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs font-bold border rounded-md py-1 px-2 cursor-pointer hover:bg-gray-800 hover:text-white transition-all',
|
||||
openedProp !== inputSpecKey ? 'bg-gray-200 ' : 'bg-gray-600 text-gray-300'
|
||||
)}
|
||||
on:click={() => {
|
||||
openedProp = inputSpecKey
|
||||
}}
|
||||
>
|
||||
{inputSpecKey}
|
||||
</div>
|
||||
{#if inputSpecKey === openedProp}
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<DynamicInputEditor bind:input={componentInputs[inputSpecKey]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { ToggleButton, ToggleButtonGroup } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import PickScript from '$lib/components/flows/pickers/PickScript.svelte'
|
||||
import {
|
||||
faAlignCenter,
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
faTrashAlt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import type { AppComponent, AppEditorContext } from '../../types'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import ComponentInputsSpecsEditor from './ComponentInputsSpecsEditor.svelte'
|
||||
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
|
||||
import PickFlow from './PickFlow.svelte'
|
||||
|
||||
export let component: AppComponent | undefined
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if component}
|
||||
{#key component.id}
|
||||
<div class="flex flex-col w-full divide-y">
|
||||
<span class="text-sm border-y w-full py-1 px-2 bg-gray-800 text-white">Component editor</span>
|
||||
|
||||
<PanelSection title="Inputs">
|
||||
{#if component.type === 'runformcomponent'}
|
||||
<InputsSpecsEditor bind:inputSpecs={component.inputs} />
|
||||
{/if}
|
||||
|
||||
{#if component.type === 'runformcomponent' && component.path === undefined}
|
||||
<span class="text-sm">Select a script or a flow to continue</span>
|
||||
<PickScript
|
||||
kind="script"
|
||||
on:pick={({ detail }) => {
|
||||
if (component && component.type === 'runformcomponent') {
|
||||
component.path = detail.path
|
||||
component.runType = 'script'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PickFlow
|
||||
on:pick={({ detail }) => {
|
||||
if (component && component.type === 'runformcomponent') {
|
||||
component.path = detail.path
|
||||
component.runType = 'flow'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if component.type === 'displaycomponent' && component.componentInputs}
|
||||
<ComponentInputsSpecsEditor bind:componentInputs={component.componentInputs} />
|
||||
{/if}
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Content">
|
||||
<div class="w-full text-xs font-bold">Title</div>
|
||||
<input type="text" class="w-full" bind:value={component.title} />
|
||||
<div class="w-full text-xs font-bold">Description</div>
|
||||
<textarea type="text" class="w-full" rows="2" bind:value={component.description} />
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Alignement">
|
||||
<div class="w-full text-xs font-bold">Horizontal alignement</div>
|
||||
|
||||
<ToggleButtonGroup bind:selected={component.horizontalAlignement}>
|
||||
<ToggleButton position="left" value="left" size="xs">
|
||||
<Icon data={faAlignLeft} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="center" value="center" size="xs">
|
||||
<Icon data={faAlignCenter} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="right" size="xs">
|
||||
<Icon data={faAlignRight} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<div class="w-full text-xs font-bold">Vertical alignement</div>
|
||||
|
||||
<ToggleButtonGroup bind:selected={component.verticalAlignement}>
|
||||
<ToggleButton position="left" value="left" size="xs">
|
||||
<Icon data={faAlignLeft} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="center" value="center" size="xs">
|
||||
<Icon data={faAlignCenter} />
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="right" size="xs">
|
||||
<Icon data={faAlignRight} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<div class="w-full text-xs font-bold">Width (%)</div>
|
||||
<input value={Math.round(component.width)} type="number" class="w-full" disabled />
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Danger zone">
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faTrashAlt }}
|
||||
on:click={() => dispatch('remove')}
|
||||
>
|
||||
Delete component
|
||||
</Button>
|
||||
</PanelSection>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import type { AppEditorContext, DynamicInput } from '../../types'
|
||||
import { Badge, Button } from '$lib/components/common'
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
export let input: DynamicInput
|
||||
|
||||
const { connectingInput, selection } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function applyConnection() {
|
||||
if (!$connectingInput.opened && $connectingInput.input !== undefined) {
|
||||
input = $connectingInput.input
|
||||
|
||||
$selection = $selection
|
||||
|
||||
$connectingInput = {
|
||||
opened: false,
|
||||
input: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: $connectingInput && applyConnection()
|
||||
</script>
|
||||
|
||||
{#if input.id && input.name}
|
||||
<div class="flex justify-between w-full">
|
||||
<span class="text-xs font-bold">Status</span>
|
||||
<Badge color="green">Connected</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between w-full">
|
||||
<span class="text-xs font-bold">Component</span>
|
||||
<Badge color="indigo">{input.id}</Badge>
|
||||
</div>
|
||||
<div class="flex justify-between w-full">
|
||||
<span class="text-xs font-bold">Field name</span>
|
||||
<Badge color="indigo">{input.name}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: faLink }}
|
||||
color="red"
|
||||
on:click={() => {
|
||||
input.id = undefined
|
||||
input.name = undefined
|
||||
}}
|
||||
>
|
||||
Clear connection
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="flex justify-between w-full">
|
||||
<span class="text-xs font-bold">Status</span>
|
||||
<Badge color="dark-yellow">Not connected</Badge>
|
||||
</div>
|
||||
<Button
|
||||
size="xs"
|
||||
startIcon={{ icon: faLink }}
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
$connectingInput = {
|
||||
opened: true,
|
||||
input: undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connect this input to an ouput
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { AppInputTransform } from '../../types'
|
||||
import DynamicInputEditor from './DynamicInputEditor.svelte'
|
||||
import StaticInputEditor from './StaticInputEditor.svelte'
|
||||
|
||||
export let appInputTransform: AppInputTransform
|
||||
</script>
|
||||
|
||||
{#if appInputTransform.type === 'static'}
|
||||
<StaticInputEditor bind:input={appInputTransform} />
|
||||
{:else if appInputTransform.type === 'output'}
|
||||
<DynamicInputEditor bind:input={appInputTransform} />
|
||||
{:else if appInputTransform.type === 'user'}
|
||||
<span>Not implemented</span>
|
||||
{/if}
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { ToggleButton, ToggleButtonGroup } from '$lib/components/common'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { faBolt, faLink, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import type { InputsSpec } from '../../types'
|
||||
import InputsSpecEditor from './InputsSpecEditor.svelte'
|
||||
|
||||
export let inputSpecs: InputsSpec
|
||||
|
||||
let openedProp = Object.keys(inputSpecs)[0]
|
||||
</script>
|
||||
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
{#each Object.keys(inputSpecs) as inputSpecKey}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full text-xs font-bold border rounded-md py-1 px-2 cursor-pointer hover:bg-gray-800 hover:text-white transition-all',
|
||||
openedProp !== inputSpecKey ? 'bg-gray-200 ' : 'bg-gray-600 text-gray-300'
|
||||
)}
|
||||
on:click={() => {
|
||||
openedProp = inputSpecKey
|
||||
}}
|
||||
>
|
||||
{inputSpecKey}
|
||||
</div>
|
||||
{#if inputSpecKey === openedProp}
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<ToggleButtonGroup bind:selected={inputSpecs[inputSpecKey].type}>
|
||||
<ToggleButton position="left" value="static" startIcon={{ icon: faBolt }} size="xs">
|
||||
Static
|
||||
</ToggleButton>
|
||||
<ToggleButton position="center" value="output" startIcon={{ icon: faLink }} size="xs">
|
||||
Dynamic
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="user" startIcon={{ icon: faUser }} size="xs">
|
||||
User
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<InputsSpecEditor bind:appInputTransform={inputSpecs[inputSpecKey]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
||||
import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FlowService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
|
||||
|
||||
type Item = { summary: String; path: String; version?: String }
|
||||
|
||||
let itemPicker: ItemPicker
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function loadItems(): Promise<Item[]> {
|
||||
return await FlowService.listFlows({ workspace: $workspaceStore! })
|
||||
}
|
||||
</script>
|
||||
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path, summary) => {
|
||||
dispatch('pick', { path, summary })
|
||||
}}
|
||||
itemName="Flow"
|
||||
extraField="summary"
|
||||
{loadItems}
|
||||
/>
|
||||
|
||||
<FlowScriptPicker
|
||||
label={`Pick flow from workspace`}
|
||||
icon={faUserGroup}
|
||||
iconColor="text-blue-500"
|
||||
on:click={() => itemPicker.openDrawer()}
|
||||
/>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { Alert, ToggleButton, ToggleButtonGroup } from '$lib/components/common'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { AppSection } from '../../types'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
|
||||
export let section: AppSection | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let deleteConfirmedCallback: (() => void) | undefined = undefined
|
||||
$: open = Boolean(deleteConfirmedCallback)
|
||||
|
||||
function deleteSection(event: CustomEvent<PointerEvent>) {
|
||||
if (
|
||||
section &&
|
||||
Array.isArray(section.components) &&
|
||||
section?.components.length > 0 &&
|
||||
!event.detail.shiftKey
|
||||
) {
|
||||
deleteConfirmedCallback = () => {
|
||||
dispatch('remove')
|
||||
deleteConfirmedCallback = undefined
|
||||
}
|
||||
} else {
|
||||
dispatch('remove')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if section}
|
||||
<div class="flex flex-col w-full divide-y">
|
||||
<span class="text-sm border-y w-full py-1 px-2 bg-gray-800 text-white">Section editor</span>
|
||||
<PanelSection title="Columns">
|
||||
<ToggleButtonGroup bind:selected={section.columns}>
|
||||
<ToggleButton position="left" value={1} disabled={section.components.length >= 2} size="xs">
|
||||
1
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
position="center"
|
||||
value={2}
|
||||
disabled={section.components.length === 3}
|
||||
size="xs"
|
||||
>
|
||||
2
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value={3} size="xs">3</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Danger zone">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
color="red"
|
||||
startIcon={{ icon: faTrashAlt }}
|
||||
on:click={deleteSection}
|
||||
>
|
||||
Delete section
|
||||
</Button>
|
||||
</PanelSection>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ConfirmationModal
|
||||
{open}
|
||||
title="Remove section"
|
||||
confirmationText="Remove"
|
||||
on:canceled={() => {
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
if (deleteConfirmedCallback) {
|
||||
deleteConfirmedCallback()
|
||||
}
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>Are you sure you want to remove this section?</span>
|
||||
<Alert type="info" title="Bypass confirmation">
|
||||
<div>
|
||||
You can press
|
||||
<Badge color="dark-gray">SHIFT</Badge>
|
||||
while removing a resource to bypass confirmation.
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { EditorConfig, StaticInput } from '../../types'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
export let input: StaticInput
|
||||
</script>
|
||||
|
||||
<Toggle bind:checked={input.visible} options={{ right: 'Visible' }} />
|
||||
<input bind:value={input.value} />
|
||||
@@ -0,0 +1,2 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
export let title: string
|
||||
</script>
|
||||
|
||||
<div class="p-4 flex flex-col gap-2 items-start">
|
||||
<div class="text-sm font-bold">{title}</div>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { AppInputTransform, DynamicInput, InputsSpec, StaticInput } from './types'
|
||||
|
||||
export interface Subscriber<T> {
|
||||
next(v: T)
|
||||
}
|
||||
|
||||
export interface Observable<T> {
|
||||
subscribe(x: Subscriber<T>)
|
||||
}
|
||||
export interface Output<T> extends Observable<T> {
|
||||
set(x: T, force?: boolean): void
|
||||
}
|
||||
|
||||
export interface Input<T> extends Subscriber<T> {
|
||||
peak(): T | undefined
|
||||
}
|
||||
|
||||
export type World = {
|
||||
outputsById: Record<string, Record<string, Output<any>>>
|
||||
connect: <T>(inputSpec: AppInputTransform, next: (x: T) => void) => Input<T>
|
||||
}
|
||||
|
||||
export function buildWorld(components: Record<string, string[]>) {
|
||||
const newWorld = buildObservableWorld()
|
||||
const outputsById: Record<string, Record<string, Output<any>>> = {}
|
||||
|
||||
for (const [k, outputs] of Object.entries(components)) {
|
||||
outputsById[k] = {}
|
||||
for (const o of outputs) {
|
||||
outputsById[k][o] = newWorld.newOutput(k, o)
|
||||
}
|
||||
}
|
||||
return { outputsById, connect: newWorld.connect }
|
||||
}
|
||||
|
||||
export function buildObservableWorld() {
|
||||
const observables: Record<string, Output<any>> = {}
|
||||
|
||||
function connect<T>(inputSpec: AppInputTransform, next: (x: T) => void): Input<T> {
|
||||
if (inputSpec.type === 'static') {
|
||||
return {
|
||||
peak: () => inputSpec.value,
|
||||
next: () => {}
|
||||
}
|
||||
} else if (inputSpec.type === 'output') {
|
||||
const input = cachedInput(next)
|
||||
let obs = observables[`${inputSpec.id}.${inputSpec.name}`]
|
||||
|
||||
if (!obs) {
|
||||
throw Error('Observable at ' + inputSpec.id + '.' + inputSpec.name + ' not found')
|
||||
}
|
||||
obs.subscribe(input)
|
||||
return input
|
||||
} else if (inputSpec.type === 'user') {
|
||||
return {
|
||||
peak: () => inputSpec.value,
|
||||
next: () => {}
|
||||
}
|
||||
} else {
|
||||
throw Error('Unknown input type ' + inputSpec)
|
||||
}
|
||||
}
|
||||
|
||||
function newOutput<T>(id: string, name: string): Output<T> {
|
||||
const output = settableOutput<T>()
|
||||
observables[`${id}.${name}`] = output
|
||||
return output
|
||||
}
|
||||
|
||||
return {
|
||||
connect,
|
||||
newOutput
|
||||
}
|
||||
}
|
||||
export function cachedInput<T>(nextParan: (x: T) => void): Input<T> {
|
||||
let value: T | undefined = undefined
|
||||
function peak(): T | undefined {
|
||||
return value
|
||||
}
|
||||
|
||||
function next(x: T): void {
|
||||
value = x
|
||||
nextParan(x)
|
||||
}
|
||||
|
||||
return {
|
||||
peak,
|
||||
next
|
||||
}
|
||||
}
|
||||
|
||||
export function settableOutput<T>(): Output<T> {
|
||||
let value: T | undefined = undefined
|
||||
const subscribers: Subscriber<T>[] = []
|
||||
|
||||
function subscribe(x: Subscriber<T>) {
|
||||
if (!subscribers.includes(x)) {
|
||||
subscribers.push(x)
|
||||
}
|
||||
}
|
||||
|
||||
function set(x: T, force: boolean = false) {
|
||||
if (value != x || force) {
|
||||
value = x
|
||||
subscribers.forEach((x) => x.next(value!))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { Schema, SchemaProperty } from '$lib/common'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { World } from './rx'
|
||||
|
||||
export type UserInput = {
|
||||
type: 'user'
|
||||
schemaProperty: SchemaProperty
|
||||
// Default value override
|
||||
defaultValue: any
|
||||
value: any
|
||||
}
|
||||
|
||||
export type DynamicInput = {
|
||||
type: 'output'
|
||||
id: FieldID | undefined
|
||||
name: string | undefined
|
||||
// Before any connection occures
|
||||
defaultValue: any
|
||||
}
|
||||
|
||||
export type StaticInput = {
|
||||
type: 'static'
|
||||
value: any
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
export type AppInputTransform = DynamicInput | StaticInput | UserInput
|
||||
|
||||
// Inner inputs, (search, filter, page, inputs of a script or flow)
|
||||
export type InputsSpec = Record<FieldID, AppInputTransform>
|
||||
export type ComponentInputsSpec = Record<FieldID, DynamicInput>
|
||||
|
||||
export type TextInputComponent = {
|
||||
type: 'textinputcomponent'
|
||||
}
|
||||
|
||||
export type RunFormComponent = {
|
||||
type: 'runformcomponent'
|
||||
path?: string
|
||||
runType?: 'script' | 'flow'
|
||||
}
|
||||
|
||||
export type BarChartComponent = {
|
||||
type: 'barchartcomponent'
|
||||
inputs: {}
|
||||
}
|
||||
|
||||
export type TableComponent = {
|
||||
type: 'tablecomponent'
|
||||
inputs: {}
|
||||
path: string
|
||||
runType: 'script' | 'flow'
|
||||
title: string
|
||||
description: string | undefined
|
||||
headers: string[]
|
||||
data: Array<Record<string, any>>
|
||||
}
|
||||
|
||||
export type DisplayComponent = {
|
||||
type: 'displaycomponent'
|
||||
}
|
||||
|
||||
export type AppComponent =
|
||||
| (
|
||||
| RunFormComponent
|
||||
| DisplayComponent
|
||||
| TextInputComponent
|
||||
| BarChartComponent
|
||||
| TableComponent
|
||||
) & {
|
||||
id: ComponentID
|
||||
title: string
|
||||
description: string
|
||||
width: number
|
||||
horizontalAlignement?: 'left' | 'center' | 'right'
|
||||
verticalAlignement?: 'top' | 'center' | 'bottom'
|
||||
configSchema: Schema | undefined
|
||||
inputs: InputsSpec
|
||||
componentInputs: ComponentInputsSpec
|
||||
}
|
||||
|
||||
type SectionID = string
|
||||
|
||||
export type AppSection = {
|
||||
title: string
|
||||
description: string
|
||||
components: AppComponent[]
|
||||
id: SectionID
|
||||
columns: 1 | 2 | 3
|
||||
}
|
||||
|
||||
export type App = {
|
||||
sections: AppSection[]
|
||||
title: string
|
||||
}
|
||||
|
||||
export type AppSelection = { sectionIndex: number; componentIndex: number | undefined }
|
||||
|
||||
export type ConnectingInput = {
|
||||
opened: boolean
|
||||
input?: DynamicInput
|
||||
}
|
||||
|
||||
export type AppEditorContext = {
|
||||
worldStore: Writable<World | undefined>
|
||||
staticOutputs: Writable<Record<string, string[]>>
|
||||
app: Writable<App>
|
||||
selection: Writable<AppSelection | undefined>
|
||||
mode: Writable<EditorMode>
|
||||
schemas: Writable<Schema[]>
|
||||
connectingInput: Writable<ConnectingInput>
|
||||
}
|
||||
|
||||
export type EditorMode = 'width' | 'dnd' | 'preview'
|
||||
|
||||
type FieldID = string
|
||||
|
||||
type ComponentID = string
|
||||
|
||||
export type EditorConfig = {
|
||||
staticInputDisabled: boolean
|
||||
outputInputDisabled: boolean
|
||||
userInputEnabled: boolean
|
||||
visibiltyEnabled: boolean
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { InputsSpec } from './types'
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
|
||||
type Args = Record<string, any>
|
||||
|
||||
export function buildArgs(
|
||||
inputSpecs: InputsSpec,
|
||||
schema: Schema,
|
||||
includeHidden: boolean = false
|
||||
): Args {
|
||||
const obj = Object.keys(schema.properties).reduce((acc, key) => {
|
||||
let input = inputSpecs[key]
|
||||
|
||||
if (!input) {
|
||||
input = {
|
||||
type: 'static',
|
||||
value: '',
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
|
||||
if (input.type === 'static' && (input.visible || includeHidden)) {
|
||||
acc[key] = input.value
|
||||
}
|
||||
|
||||
if (input.type === 'output') {
|
||||
acc[key] = input.defaultValue
|
||||
}
|
||||
|
||||
if (input.type === 'user') {
|
||||
acc[key] = schema.properties[key].default
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
export async function loadSchema(
|
||||
workspace: string,
|
||||
path: string,
|
||||
runType: 'script' | 'flow'
|
||||
): Promise<Schema> {
|
||||
if (runType === 'script') {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace,
|
||||
path
|
||||
})
|
||||
|
||||
return script.schema
|
||||
} else {
|
||||
const flow = await FlowService.getFlowByPath({
|
||||
workspace,
|
||||
path
|
||||
})
|
||||
|
||||
return flow.schema
|
||||
}
|
||||
}
|
||||
|
||||
export function schemaToInputsSpec(schema: Schema): InputsSpec {
|
||||
return Object.keys(schema.properties).reduce((accu, key) => {
|
||||
const property = schema.properties[key]
|
||||
accu[key] = {
|
||||
type: 'static',
|
||||
defaultValue: property.default,
|
||||
value: undefined,
|
||||
visible: true
|
||||
}
|
||||
return accu
|
||||
}, {})
|
||||
}
|
||||
@@ -95,7 +95,7 @@
|
||||
<svelte:element
|
||||
this={href ? 'a' : 'button'}
|
||||
bind:this={element}
|
||||
on:click={onClick}
|
||||
on:click|stopPropagation={onClick}
|
||||
on:focus
|
||||
on:blur
|
||||
{...buttonProps}
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { faRing } from '@fortawesome/free-solid-svg-icons'
|
||||
import { getContext } from 'svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import type { ToggleButtonContext } from './ToggleButtonGroup.svelte'
|
||||
|
||||
export let value: string
|
||||
export let value: any
|
||||
export let position: 'left' | 'center' | 'right'
|
||||
|
||||
const { select, selected } = getContext<ToggleButtonContext>('ToggleButtonGroup')
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
{...$$props}
|
||||
on:click={() => select(value)}
|
||||
class={classNames(
|
||||
'py-1 px-2 text-sm font-medium border-gray-200 ',
|
||||
position === 'left' ? 'rounded-l-lg border' : '',
|
||||
position === 'center' ? 'border-t border-b' : '',
|
||||
position === 'right' ? 'rounded-r-md border border-l-0' : '',
|
||||
$selected.includes(value) ? 'text-white bg-gray-900 ' : 'text-gray-900 bg-white '
|
||||
btnClasses={classNames(
|
||||
'border-gray-200 focus:ring-0 w-full',
|
||||
position === 'left' ? 'rounded-none rounded-l-lg border' : '',
|
||||
position === 'center' ? 'rounded-none border-t border-b' : '',
|
||||
position === 'right' ? 'rounded-none rounded-r-md !border border-l-0' : ''
|
||||
)}
|
||||
color={$selected === value ? 'dark' : 'light'}
|
||||
variant="contained"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script context="module" lang="ts">
|
||||
export type ToggleButtonContext = {
|
||||
selected: Writable<string[]>
|
||||
select: (value: string) => void
|
||||
selected: Writable<any>
|
||||
select: (value: any) => void
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,31 +9,19 @@
|
||||
import { setContext } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
|
||||
export let exclusive: boolean = true
|
||||
export let selected: string[] = []
|
||||
export let selected: any
|
||||
|
||||
const selectedContent = writable(selected)
|
||||
|
||||
setContext<ToggleButtonContext>('ToggleButtonGroup', {
|
||||
selected: selectedContent,
|
||||
select: (value: string) => {
|
||||
if (exclusive) {
|
||||
selectedContent.set([value])
|
||||
} else {
|
||||
selectedContent.update((selected) => {
|
||||
const index = selected.findIndex((val: string) => val === value)
|
||||
if (index !== -1) {
|
||||
selected.splice(index, 1)
|
||||
return selected
|
||||
} else {
|
||||
return [value, ...selected]
|
||||
}
|
||||
})
|
||||
}
|
||||
select: (value: any) => {
|
||||
selectedContent.set(value)
|
||||
selected = value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="inline-flex rounded-md shadow-sm" role="group">
|
||||
<div class="flex w-full rounded-md shadow-sm" role="group">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Editor from '$lib/components/Editor.svelte'
|
||||
import EditorBar, { EDITOR_BAR_WIDTH_THRESHOLD } from '$lib/components/EditorBar.svelte'
|
||||
import EditorBar from '$lib/components/EditorBar.svelte'
|
||||
import ModulePreview from '$lib/components/ModulePreview.svelte'
|
||||
import { createScriptFromInlineScript, fork } from '$lib/components/flows/flowStateUtils'
|
||||
import { flowStore } from '$lib/components/flows/flowStore'
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
NEVER_TESTED_THIS_FAR,
|
||||
numberToChars
|
||||
} from './utils'
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { Mutex } from 'async-mutex'
|
||||
|
||||
export async function loadFlowModuleState(flowModule: FlowModule): Promise<FlowModuleState> {
|
||||
try {
|
||||
@@ -30,21 +30,32 @@ export async function loadFlowModuleState(flowModule: FlowModule): Promise<FlowM
|
||||
}
|
||||
}
|
||||
|
||||
export const idMutex = new Mutex();
|
||||
export const idMutex = new Mutex()
|
||||
|
||||
export function getNextId(currentKeys: string[]): string {
|
||||
const max = currentKeys.reduce((acc, key) => {
|
||||
if (key === 'failure' || key.includes('branch') || key.includes('loop')) {
|
||||
return acc
|
||||
} else {
|
||||
const num = charsToNumber(key)
|
||||
return Math.max(acc, num + 1)
|
||||
}
|
||||
}, 0)
|
||||
return numberToChars(max)
|
||||
}
|
||||
|
||||
// Computes the next available id
|
||||
export function nextId(): string {
|
||||
const flowState = get(flowStateStore)
|
||||
|
||||
const max = Object.keys(flowState)
|
||||
.reduce((acc, key) => {
|
||||
if (key === 'failure' || key.includes('branch') || key.includes('loop')) {
|
||||
return acc
|
||||
} else {
|
||||
const num = charsToNumber(key)
|
||||
return Math.max(acc, num + 1)
|
||||
}
|
||||
}, 0)
|
||||
const max = Object.keys(flowState).reduce((acc, key) => {
|
||||
if (key === 'failure' || key.includes('branch') || key.includes('loop')) {
|
||||
return acc
|
||||
} else {
|
||||
const num = charsToNumber(key)
|
||||
return Math.max(acc, num + 1)
|
||||
}
|
||||
}, 0)
|
||||
return numberToChars(max)
|
||||
}
|
||||
|
||||
@@ -124,7 +135,9 @@ export async function createBranchAll(id: string): Promise<[FlowModule, FlowModu
|
||||
return [branchesFlowModules, flowModuleState]
|
||||
}
|
||||
|
||||
export async function fork(flowModule: FlowModule): Promise<[FlowModule & { value: RawScript }, FlowModuleState]> {
|
||||
export async function fork(
|
||||
flowModule: FlowModule
|
||||
): Promise<[FlowModule & { value: RawScript }, FlowModuleState]> {
|
||||
if (flowModule.value.type !== 'script') {
|
||||
throw new Error('Can only fork a script module')
|
||||
}
|
||||
@@ -136,7 +149,10 @@ export async function fork(flowModule: FlowModule): Promise<[FlowModule & { valu
|
||||
return [forkedFlowModule, flowModuleState]
|
||||
}
|
||||
|
||||
async function createInlineScriptModuleFromPath(path: string, id: string): Promise<FlowModule & { value: RawScript }> {
|
||||
async function createInlineScriptModuleFromPath(
|
||||
path: string,
|
||||
id: string
|
||||
): Promise<FlowModule & { value: RawScript }> {
|
||||
const { content, language } = await getScriptByPath(path)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import type { IconDefinition } from '@fortawesome/free-brands-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
|
||||
export let disabled: boolean = false
|
||||
export let icon: IconDefinition
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { check } from 'svelte-awesome/icons'
|
||||
import { Badge, Button } from '../common'
|
||||
import { Badge } from '../common'
|
||||
|
||||
const SMALL_ICON_SCALE = 0.7
|
||||
|
||||
|
||||
@@ -44,7 +44,10 @@ export function displayDaysAgo(dateString: string): string {
|
||||
if (dAgo == 0) {
|
||||
return `yday at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
} else if (dAgo > 7) {
|
||||
return `${dAgo + 1} days ago at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
return `${dAgo + 1} days ago at ${date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`
|
||||
} else {
|
||||
return displayDate(dateString)
|
||||
}
|
||||
@@ -56,8 +59,9 @@ export function displayDate(dateString: string | undefined): string {
|
||||
if (date.toString() === 'Invalid Date') {
|
||||
return ''
|
||||
} else {
|
||||
return `${date.getFullYear()}/${date.getMonth() + 1
|
||||
}/${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
return `${date.getFullYear()}/${
|
||||
date.getMonth() + 1
|
||||
}/${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,3 +561,13 @@ export async function copyToClipboard(value: string, sendToast = true): Promise<
|
||||
sendUserToast(success ? 'Copied to clipboard!' : "Couldn't copy to clipboard", !success)
|
||||
return success
|
||||
}
|
||||
|
||||
export function pluralize(quantity: number, word: string, customPlural?: string) {
|
||||
if (quantity <= 1) {
|
||||
return `${quantity} ${word}`
|
||||
} else if (customPlural) {
|
||||
return `${quantity} ${customPlural}}`
|
||||
} else {
|
||||
return `${quantity} ${word}s`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
goto('/user/login')
|
||||
}
|
||||
|
||||
beforeNavigate((newNavigationState) => {
|
||||
beforeNavigate(() => {
|
||||
menuOpen = false
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import type { App } from '$lib/components/apps/types'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { DrawerContent, Skeleton } from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import { AppService, ListableApp, Policy } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { faEdit, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
let path: string = ''
|
||||
let initialPath = ''
|
||||
let pathError = ''
|
||||
|
||||
let apps: ListableApp[] | undefined = undefined
|
||||
|
||||
async function createApp() {
|
||||
const appJson: App = {
|
||||
sections: [],
|
||||
title: 'New app'
|
||||
}
|
||||
|
||||
const policy = {
|
||||
triggerables: {},
|
||||
execution_mode: Policy.execution_mode.PUBLISHER,
|
||||
on_behalf_of: `u/${$userStore?.username}`
|
||||
}
|
||||
try {
|
||||
const appId = await AppService.createApp({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
value: appJson,
|
||||
path,
|
||||
summary: 'App summary',
|
||||
policy
|
||||
}
|
||||
})
|
||||
|
||||
goto(`/apps/edit/${appId}`)
|
||||
} catch (e) {
|
||||
sendUserToast('Error creating app', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApps(): Promise<void> {
|
||||
apps = await AppService.listApps({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
let drawerOpen = false
|
||||
|
||||
function closeDrawer() {
|
||||
drawerOpen = false
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore && $userStore) {
|
||||
loadApps()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:open={drawerOpen} size="800px">
|
||||
<DrawerContent title="Add an app" on:close={() => closeDrawer()}>
|
||||
<Path bind:error={pathError} bind:path {initialPath} namePlaceholder="my_app" kind="app">
|
||||
<div slot="ownerToolkit">
|
||||
App permissions depend on their path. Select the group <span class="font-mono">all</span>
|
||||
to share it, and <span class="font-mono">user</span> to keep it private.
|
||||
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
|
||||
</div>
|
||||
</Path>
|
||||
|
||||
<Button on:click={() => createApp()}>Create app</Button>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader title="Apps">
|
||||
<Button size="sm" startIcon={{ icon: faPlus }} on:click={() => (drawerOpen = true)}>
|
||||
New app
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<div class="p-4 border ">
|
||||
{#if !apps}
|
||||
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-3 mt-2">
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[8.5]]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if apps?.length == 0}
|
||||
<p class="text-xs text-gray-600 italic mt-2">No scripts yet</p>
|
||||
{:else if apps}
|
||||
<div class="grid md:grid-cols-2 gap-4 sm:grid-cols-1 xl:grid-cols-3 mt-2">
|
||||
{#each apps as { summary, path, extra_perms }}
|
||||
<a
|
||||
class="border p-4 rounded-sm shadow-sm space-y-2 hover:border-blue-600 text-gray-800 flex flex-col justify-between"
|
||||
href="/apps/get/{path}"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<a href="/apps/get/{path}" class="px-6">
|
||||
<div class="font-semibold text-gray-700">
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
</div>
|
||||
<p class="text-gray-700 text-xs">
|
||||
{path}
|
||||
</p>
|
||||
</a>
|
||||
<div class="flex flex-wrap items-center gap-2 mt-1 px-6">
|
||||
<SharedBadge canWrite={true} extraPerms={extra_perms} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse items-end w-full gap-2 pr-2 mt-2">
|
||||
<div>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/apps/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import AppEditor from '$lib/components/apps/editor/AppEditor.svelte'
|
||||
import { AppService, AppWithLastVersion } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
let app: AppWithLastVersion | undefined = undefined
|
||||
let path = $page.params.path
|
||||
|
||||
async function loadApp(): Promise<void> {
|
||||
app = await AppService.getAppByPath({
|
||||
path,
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
console.log(app)
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
loadApp()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if app}
|
||||
<div class="h-screen">
|
||||
<AppEditor app={app.value} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,35 @@
|
||||
<script context="module">
|
||||
export function load({ params }) {
|
||||
return {
|
||||
stuff: { title: `Script ${params.hash}` }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Skeleton } from '$lib/components/common'
|
||||
import { AppService, AppWithLastVersion } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
let app: AppWithLastVersion | undefined = undefined
|
||||
|
||||
async function loadApp() {
|
||||
app = await AppService.getAppByPath({ workspace: $workspaceStore!, path: $page.params.path })
|
||||
}
|
||||
$: if ($workspaceStore) {
|
||||
loadApp()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Skeleton loading={app == undefined} layout={[10]} />
|
||||
|
||||
<CenteredPage>
|
||||
<a href="/apps">Back to apps</a>
|
||||
|
||||
{#if app}
|
||||
<a href="/apps/edit/{$page.params.path}">Edit</a>
|
||||
<div>{JSON.stringify(app, null, 4)} </div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
+133
-93
@@ -1,15 +1,10 @@
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
const plugin = require('tailwindcss/plugin')
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}'
|
||||
],
|
||||
safelist: [
|
||||
'hljs',
|
||||
'splitpanes__pane',
|
||||
'splitpanes__splitter'
|
||||
],
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
safelist: ['hljs', 'splitpanes__pane', 'splitpanes__splitter'],
|
||||
safelist: ['hljs', 'splitpanes__pane', 'splitpanes__splitter'],
|
||||
theme: {
|
||||
colors: {
|
||||
current: 'currentcolor',
|
||||
@@ -25,7 +20,7 @@ const config = {
|
||||
600: '#4b5563',
|
||||
700: '#374151',
|
||||
800: '#1f2937',
|
||||
900: '#111827',
|
||||
900: '#111827'
|
||||
},
|
||||
red: {
|
||||
50: '#fef2f2',
|
||||
@@ -37,7 +32,7 @@ const config = {
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
900: '#7f1d1d'
|
||||
},
|
||||
orange: {
|
||||
100: '#ffedd5',
|
||||
@@ -46,8 +41,7 @@ const config = {
|
||||
500: '#f97316',
|
||||
600: '#ea580c',
|
||||
700: '#c2410c',
|
||||
800: '#c2410c',
|
||||
|
||||
800: '#c2410c'
|
||||
},
|
||||
yellow: {
|
||||
50: '#fefce8',
|
||||
@@ -59,7 +53,7 @@ const config = {
|
||||
600: '#ca8a04',
|
||||
700: '#a16207',
|
||||
800: '#854d0e',
|
||||
900: '#713f12',
|
||||
900: '#713f12'
|
||||
},
|
||||
green: {
|
||||
50: '#f0fdf4',
|
||||
@@ -71,7 +65,7 @@ const config = {
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
900: '#14532d'
|
||||
},
|
||||
blue: {
|
||||
50: '#eff6ff',
|
||||
@@ -83,7 +77,7 @@ const config = {
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
900: '#1e3a8a'
|
||||
},
|
||||
indigo: {
|
||||
100: '#e0e7ff',
|
||||
@@ -91,13 +85,22 @@ const config = {
|
||||
300: '#a5b4fc',
|
||||
500: '#6366f1',
|
||||
800: '#3730a3',
|
||||
900: '#312e81',
|
||||
900: '#312e81'
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
// add double quotes if there is space in font name
|
||||
main: ['Inter', 'sans-serif'],
|
||||
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace']
|
||||
mono: [
|
||||
'ui-monospace',
|
||||
'SFMono-Regular',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace'
|
||||
]
|
||||
},
|
||||
extend: {
|
||||
maxHeight: {
|
||||
@@ -111,7 +114,6 @@ const config = {
|
||||
'1/2': '50%',
|
||||
'2/3': '66%',
|
||||
'3/4': '75%'
|
||||
|
||||
},
|
||||
minHeight: {
|
||||
'1/2': '50vh'
|
||||
@@ -120,16 +122,16 @@ const config = {
|
||||
'2/3': '66vh'
|
||||
},
|
||||
transitionProperty: {
|
||||
'height': 'height'
|
||||
height: 'height'
|
||||
},
|
||||
fontSize: {
|
||||
'2xs': '0.7rem'
|
||||
},
|
||||
screens: {
|
||||
'fhd': '1900px',
|
||||
'qhd': '2500px',
|
||||
'4k': '3800px',
|
||||
},
|
||||
fhd: '1900px',
|
||||
qhd: '2500px',
|
||||
'4k': '3800px'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -138,105 +140,137 @@ const config = {
|
||||
require('@tailwindcss/typography'),
|
||||
plugin(({ addBase, addComponents, addUtilities, theme }) => {
|
||||
addBase({
|
||||
'html': {
|
||||
html: {
|
||||
fontFamily: theme('fontFamily.main'),
|
||||
fontSize: theme('fontSize.base'),
|
||||
fontWeight: theme('fontWeight.normal'),
|
||||
color: theme('colors.gray.900'),
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: theme('fontSize.lg'),
|
||||
},
|
||||
fontSize: theme('fontSize.lg')
|
||||
}
|
||||
},
|
||||
'h1': {
|
||||
h1: {
|
||||
fontSize: '24px',
|
||||
fontWeight: theme('fontWeight.extrabold'),
|
||||
lineHeight: '1.05',
|
||||
color: theme('colors.gray.800'),
|
||||
[`@media (min-width: ${theme('screens.lg')})`]: {
|
||||
fontSize: '26px',
|
||||
fontSize: '26px'
|
||||
},
|
||||
[`@media (min-width: ${theme('screens.fhd')})`]: {
|
||||
fontSize: '29px',
|
||||
fontSize: '29px'
|
||||
},
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '34px',
|
||||
},
|
||||
fontSize: '34px'
|
||||
}
|
||||
},
|
||||
'h2': {
|
||||
h2: {
|
||||
fontSize: '20px',
|
||||
fontWeight: theme('fontWeight.extrabold'),
|
||||
lineHeight: '1.1',
|
||||
color: theme('colors.gray.700'),
|
||||
[`@media (min-width: ${theme('screens.fhd')})`]: {
|
||||
fontSize: '22px',
|
||||
fontSize: '22px'
|
||||
},
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '25px',
|
||||
},
|
||||
fontSize: '25px'
|
||||
}
|
||||
},
|
||||
'h3': {
|
||||
h3: {
|
||||
fontSize: '18px',
|
||||
fontWeight: theme('fontWeight.bold'),
|
||||
lineHeight: '1.2',
|
||||
color: theme('colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.fhd')})`]: {
|
||||
fontSize: '20px',
|
||||
fontSize: '20px'
|
||||
},
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '22px',
|
||||
},
|
||||
fontSize: '22px'
|
||||
}
|
||||
},
|
||||
'h4': {
|
||||
h4: {
|
||||
fontSize: '18px',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
lineHeight: '1.3',
|
||||
color: theme('colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '20px',
|
||||
},
|
||||
fontSize: '20px'
|
||||
}
|
||||
},
|
||||
'h5': {
|
||||
h5: {
|
||||
fontSize: '16px',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
lineHeight: '1.5',
|
||||
color: theme('colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '18px',
|
||||
},
|
||||
fontSize: '18px'
|
||||
}
|
||||
},
|
||||
'h6': {
|
||||
h6: {
|
||||
fontSize: '16px',
|
||||
fontWeight: theme('fontWeight.medium'),
|
||||
lineHeight: '1.5',
|
||||
color: theme('colors.gray.600'),
|
||||
[`@media (min-width: ${theme('screens.qhd')})`]: {
|
||||
fontSize: '18px',
|
||||
},
|
||||
},
|
||||
'button': {
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
},
|
||||
'a': {
|
||||
color: theme('colors.blue.500')
|
||||
},
|
||||
'input,input[type="text"],input[type="email"],input[type="url"],input[type="password"],input[type="number"],input[type="date"],input[type="datetime-local"],input[type="month"],input[type="search"],input[type="tel"],input[type="time"],input[type="week"],textarea,textarea[type="text"],select': {
|
||||
display: 'block',
|
||||
fontSize: theme('fontSize.sm'),
|
||||
width: '100%',
|
||||
padding: `${theme('spacing.1')} ${theme('spacing.2')}`,
|
||||
border: `1px solid ${theme('colors.gray.300')}`,
|
||||
borderRadius: theme('borderRadius.md'),
|
||||
boxShadow: theme('boxShadow.sm'),
|
||||
'&:focus': {
|
||||
'--tw-ring-color': theme('colors.indigo.100'),
|
||||
'--tw-ring-offset-shadow': 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)',
|
||||
'--tw-ring-shadow': 'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
|
||||
boxShadow: 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)'
|
||||
},
|
||||
'&:disabled,[disabled]': {
|
||||
backgroundColor: theme('colors.gray.100') + ' !important'
|
||||
fontSize: '18px'
|
||||
}
|
||||
},
|
||||
button: {
|
||||
fontWeight: theme('fontWeight.semibold')
|
||||
},
|
||||
a: {
|
||||
color: theme('colors.blue.500')
|
||||
},
|
||||
'input,input[type="text"],input[type="email"],input[type="url"],input[type="password"],input[type="number"],input[type="date"],input[type="datetime-local"],input[type="month"],input[type="search"],input[type="tel"],input[type="time"],input[type="week"],textarea,textarea[type="text"],select':
|
||||
{
|
||||
display: 'block',
|
||||
fontSize: theme('fontSize.sm'),
|
||||
width: '100%',
|
||||
padding: `${theme('spacing.1')} ${theme('spacing.2')}`,
|
||||
border: `1px solid ${theme('colors.gray.300')}`,
|
||||
borderRadius: theme('borderRadius.md'),
|
||||
boxShadow: theme('boxShadow.sm'),
|
||||
'&:focus': {
|
||||
'--tw-ring-color': theme('colors.indigo.100'),
|
||||
'--tw-ring-offset-shadow':
|
||||
'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)',
|
||||
'--tw-ring-shadow':
|
||||
'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
|
||||
boxShadow:
|
||||
'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)'
|
||||
},
|
||||
'&:disabled,[disabled]': {
|
||||
backgroundColor: theme('colors.gray.100') + ' !important'
|
||||
}
|
||||
},
|
||||
button: {
|
||||
fontWeight: theme('fontWeight.semibold')
|
||||
},
|
||||
a: {
|
||||
color: theme('colors.blue.500')
|
||||
},
|
||||
'input,input[type="text"],input[type="email"],input[type="url"],input[type="password"],input[type="number"],input[type="date"],input[type="datetime-local"],input[type="month"],input[type="search"],input[type="tel"],input[type="time"],input[type="week"],textarea,select':
|
||||
{
|
||||
display: 'block',
|
||||
fontSize: theme('fontSize.sm'),
|
||||
width: '100%',
|
||||
padding: `${theme('spacing.1')} ${theme('spacing.2')}`,
|
||||
border: `1px solid ${theme('colors.gray.300')}`,
|
||||
borderRadius: theme('borderRadius.md'),
|
||||
boxShadow: theme('boxShadow.sm'),
|
||||
'&:focus': {
|
||||
'--tw-ring-color': theme('colors.indigo.100'),
|
||||
'--tw-ring-offset-shadow':
|
||||
'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)',
|
||||
'--tw-ring-shadow':
|
||||
'var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
|
||||
boxShadow:
|
||||
'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)'
|
||||
},
|
||||
'&:disabled,[disabled]': {
|
||||
backgroundColor: theme('colors.gray.100') + ' !important'
|
||||
}
|
||||
},
|
||||
'button:disabled,button[disabled=true],a:disabled,a[disabled=true]': {
|
||||
pointerEvents: 'none',
|
||||
cursor: 'default',
|
||||
@@ -248,8 +282,8 @@ const config = {
|
||||
fontSize: theme('fontSize.sm') + ' !important',
|
||||
lineHeight: theme('lineHeight.4') + ' !important',
|
||||
whiteSpace: 'pre-wrap'
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
addComponents({
|
||||
'.table-custom': {
|
||||
'& th': {
|
||||
@@ -261,24 +295,24 @@ const config = {
|
||||
textAlign: 'left',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
color: theme('colors.gray.900'),
|
||||
textTransform: 'capitalize',
|
||||
textTransform: 'capitalize'
|
||||
},
|
||||
'& td': {
|
||||
paddingRight: theme('spacing.2'),
|
||||
paddingTop: theme('spacing.2'),
|
||||
paddingBottom: theme('spacing.2'),
|
||||
fontSize: theme('fontSize.sm'),
|
||||
color: theme('colors.gray.700'),
|
||||
color: theme('colors.gray.700')
|
||||
},
|
||||
'& tbody > :not([hidden]) ~ :not([hidden])': {
|
||||
borderTop: `1px solid ${theme('colors.gray.200')}`
|
||||
},
|
||||
}
|
||||
},
|
||||
'.commit-hash': {
|
||||
fontSize: theme('fontSize.2xs'),
|
||||
color: theme('colors.gray.500'),
|
||||
backgroundColor: theme('colors.gray.200'),
|
||||
fontFamily: theme('fontFamily.mono'),
|
||||
fontFamily: theme('fontFamily.mono')
|
||||
},
|
||||
'.input-error': {
|
||||
borderColor: `${theme('colors.red.500')} !important`
|
||||
@@ -292,23 +326,25 @@ const config = {
|
||||
'.animate-skeleton': {
|
||||
animation: theme('animation.pulse'),
|
||||
backgroundColor: theme('colors.blue.100'),
|
||||
borderRadius: theme('borderRadius.DEFAULT'),
|
||||
borderRadius: theme('borderRadius.DEFAULT')
|
||||
},
|
||||
'.text-blue-gradient': {
|
||||
color: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
backgroundImage: `linear-gradient(to right, ${theme('colors.blue.600')}, ${theme('colors.blue.500')})`
|
||||
backgroundImage: `linear-gradient(to right, ${theme('colors.blue.600')}, ${theme(
|
||||
'colors.blue.500'
|
||||
)})`
|
||||
},
|
||||
'.splitpanes__pane': {
|
||||
backgroundColor: theme('colors.white') + ' !important',
|
||||
overflow: 'auto !important',
|
||||
overflow: 'auto !important'
|
||||
},
|
||||
'.splitpanes__splitter': {
|
||||
backgroundColor: theme('colors.gray.300') + ' !important',
|
||||
margin: '0 !important',
|
||||
border: 'none !important',
|
||||
'&::before': {
|
||||
backgroundColor: '#00000060 !important',
|
||||
backgroundColor: '#00000060 !important'
|
||||
},
|
||||
'&::after': {
|
||||
backgroundColor: '#3f83f850 !important',
|
||||
@@ -321,7 +357,7 @@ const config = {
|
||||
'--splitter-hover-adjustment': '-2px'
|
||||
},
|
||||
'&:hover::after': {
|
||||
opacity: '1',
|
||||
opacity: '1'
|
||||
}
|
||||
},
|
||||
'.splitpanes--vertical>.splitpanes__splitter': {
|
||||
@@ -329,13 +365,13 @@ const config = {
|
||||
'&::before': {
|
||||
left: '1px !important',
|
||||
width: '1px !important',
|
||||
marginLeft: '0 !important',
|
||||
marginLeft: '0 !important'
|
||||
},
|
||||
'&::after': {
|
||||
top: '0 !important',
|
||||
height: '100% !important',
|
||||
left: 'var(--splitter-hover-adjustment) !important',
|
||||
width: 'var(--splitter-hover-size) !important',
|
||||
width: 'var(--splitter-hover-size) !important'
|
||||
}
|
||||
},
|
||||
'.splitpanes--horizontal>.splitpanes__splitter': {
|
||||
@@ -343,16 +379,16 @@ const config = {
|
||||
'&::before': {
|
||||
top: '1px !important',
|
||||
height: '1px !important',
|
||||
marginTop: '0 !important',
|
||||
marginTop: '0 !important'
|
||||
},
|
||||
'&::after': {
|
||||
top: 'var(--splitter-hover-adjustment) !important',
|
||||
height: 'var(--splitter-hover-size) !important',
|
||||
left: '0 !important',
|
||||
width: '100% !important',
|
||||
width: '100% !important'
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
addUtilities({
|
||||
'.separator': {
|
||||
backgroundColor: '#ddd !important'
|
||||
@@ -360,15 +396,15 @@ const config = {
|
||||
'.center-center': {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'.ellipsize': {
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
/** Set the '-webkit-line-clamp' property to the desired number of lines.
|
||||
*
|
||||
*
|
||||
* Eg.: `class="ellipsize-multi-line [-webkit-line-clamp:3]"`
|
||||
*/
|
||||
'.ellipsize-multi-line': {
|
||||
@@ -386,13 +422,17 @@ const config = {
|
||||
'scrollbar-width': 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
width: '0px',
|
||||
},
|
||||
width: '0px'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
],
|
||||
darkMode: 'class',
|
||||
purge: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
'./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
||||
Reference in New Issue
Block a user