mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 08:01:38 +00:00
Merge remote-tracking branch 'origin/main' into glm/replace_menuV2
This commit is contained in:
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [1.460.1](https://github.com/windmill-labs/windmill/compare/v1.460.0...v1.460.1) (2025-02-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pin opentelemetry to 0.27.1 ([e92a909](https://github.com/windmill-labs/windmill/commit/e92a90907f41568e4e04c932e1fbef64ab4c48a9))
|
||||
|
||||
## [1.460.0](https://github.com/windmill-labs/windmill/compare/v1.459.0...v1.460.0) (2025-02-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add postgres trigger captures ([#5165](https://github.com/windmill-labs/windmill/issues/5165)) ([57cfa40](https://github.com/windmill-labs/windmill/commit/57cfa4045bf9aa7c2ef625cf3b24067567466aff))
|
||||
* improve large apps performances ([#5265](https://github.com/windmill-labs/windmill/issues/5265)) ([aae3683](https://github.com/windmill-labs/windmill/commit/aae3683fe90adc0eea055238f7776b96140706bd))
|
||||
* lazy mode ([7c4b8a7](https://github.com/windmill-labs/windmill/commit/7c4b8a7e1dca870b51b60f33a352d344ef34218f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove cache dir mount and mount only the cache executable (Rust, C#) ([#5270](https://github.com/windmill-labs/windmill/issues/5270)) ([6357ed3](https://github.com/windmill-labs/windmill/commit/6357ed3d5e1188bb92ccaf4710e526ab2ec7e874))
|
||||
|
||||
## [1.459.0](https://github.com/windmill-labs/windmill/compare/v1.458.4...v1.459.0) (2025-02-10)
|
||||
|
||||
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n path,\n is_flow,\n workspace_id,\n owner,\n email,\n trigger_config as \"trigger_config!: _\"\n FROM\n capture_config\n WHERE\n trigger_kind = 'postgres' AND\n last_client_ping > NOW() - INTERVAL '10 seconds' AND\n trigger_config IS NOT NULL AND\n (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "owner",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "trigger_config!: _",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "011c7638eeeda710deb86a216a9e10df9c3e9458e85bcdde466b01011a1f2ac2"
|
||||
}
|
||||
+2
-1
@@ -18,7 +18,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE \n capture_config \n SET \n last_server_ping = NULL \n WHERE \n workspace_id = $1 AND \n path = $2 AND \n is_flow = $3 AND \n trigger_kind = 'postgres' AND \n server_id IS NULL\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "12e868b63a7c622c76713db5a5577a927efca4ae49a15c2b999e2410f2a312ff"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE postgres_trigger \n SET \n server_id = $1, \n last_server_ping = now(),\n error = 'Connecting...'\n WHERE \n enabled IS TRUE \n AND workspace_id = $2 \n AND path = $3 \n AND (last_server_ping IS NULL \n OR last_server_ping < now() - INTERVAL '15 seconds'\n ) \n RETURNING true\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1488e1b5007752e1ebae4235ad04c398fe6398745e16fd119008b8ea67662416"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n capture_config \n SET \n last_server_ping = now(), \n error = $1 \n WHERE \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'postgres' AND \n server_id = $5 AND \n last_client_ping > NOW() - INTERVAL '10 seconds' \n RETURNING 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1974bd65bbf40024773aad4dee1c50b12e110e76bb58e6de25bec094e758a71c"
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, path, url, script_path, is_flow, edited_by, email, edited_at, server_id, last_server_ping, extra_perms, error, enabled, filters as \"filters: _\", initial_messages as \"initial_messages: _\", url_runnable_args as \"url_runnable_args: _\", can_return_message FROM websocket_trigger\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "filters: _",
|
||||
"type_info": "JsonbArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "initial_messages: _",
|
||||
"type_info": "JsonbArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "url_runnable_args: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "can_return_message",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1aa8ead10f8d994f6685d266fcbd409b0fff43111d9600e64b2348401ed8929d"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n capture_config \n SET \n error = $1, \n server_id = NULL, \n last_server_ping = NULL \n WHERE \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'postgres'\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29f096ec62c4abb1435a5667e2b30e9c1724e419cdc23ef1b300e84c02a20427"
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM postgres_trigger\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "postgres_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "replication_slot_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "publication_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4931d4752357078ae3ae01f37742639dba0dde680ab934ba78abcb5fdda8117a"
|
||||
}
|
||||
+4
-2
@@ -26,7 +26,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -58,7 +59,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n postgres_trigger \n SET\n last_server_ping = NULL \n WHERE \n workspace_id = $1 \n AND path = $2 \n AND server_id IS NULL",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "61bed1bc6d3e6a3c1d640eeacc290a85d8b63ee36c39dfbf4348d120f6e561ae"
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM nats_trigger\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "nats_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "subjects",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "stream_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "consumer_name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "use_jetstream",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "61e6070b8a1e3a138818c327d6dbe7efbe27f9e2c8e02258cf7aa06e1779fddb"
|
||||
}
|
||||
+2
-1
@@ -18,7 +18,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT slot_name FROM pg_replication_slots where slot_name = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "slot_name",
|
||||
"type_info": "Name"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Name"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "6f56acb985aa7141ea1891d7ad58a32c35d1b02fe7070c92a2e62c1a5339c396"
|
||||
}
|
||||
+2
-1
@@ -29,7 +29,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n capture_config \n SET \n server_id = $1,\n last_server_ping = now(), \n error = 'Connecting...' \n WHERE \n last_client_ping > NOW() - INTERVAL '10 seconds' AND \n workspace_id = $2 AND \n path = $3 AND \n is_flow = $4 AND \n trigger_kind = 'postgres' AND \n (last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds') \n RETURNING true\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "74d928f4c3f0de191f414471b9a4fbe9c20f9685b06ad5bbded424948b2dc88c"
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM kafka_trigger\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "kafka_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "topics",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "group_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "server_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "last_server_ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "enabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "8b784784fe63d91cc5ebe27022f803caf85d5916960308cff512047d4f0dcba4"
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n postgres_trigger\n SET \n last_server_ping = now(),\n error = $1\n WHERE\n workspace_id = $2\n AND path = $3\n AND server_id = $4 \n AND enabled IS TRUE\n RETURNING 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9116102c6ccad5b0d752d5d690c233dfe48062aef23072b4f4ae4ab5ca269082"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT pubname FROM pg_publication WHERE pubname = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "pubname",
|
||||
"type_info": "Name"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Name"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "baa1dddc616419bf4b923715f0a863bc0ff69c98db0f0c8f55e4ac89fdde7a60"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n postgres_trigger \n SET \n enabled = FALSE, \n error = $1, \n server_id = NULL, \n last_server_ping = NULL \n WHERE \n workspace_id = $2 AND \n path = $3\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bfc534d87d701d7ac78cc97d0054d829165ba3f22fba75c3161e4cddb72264ee"
|
||||
}
|
||||
+2
-1
@@ -21,7 +21,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,7 +18,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members \n FROM usr u\n JOIN usr_to_group u2g ON u2g.usr = u.username AND u2g.workspace_id = u.workspace_id\n RIGHT JOIN group_ g_ ON g_.workspace_id = u.workspace_id AND g_.name = u2g.group_\n WHERE g_.workspace_id = $1 AND g_.name != 'all'\n GROUP BY g_.workspace_id, name, summary, extra_perms",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "summary",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "members",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "cc6e21bf16d7b92764aa2b261cee94f6daf6a1eedd8a68742a2f510f0452cc32"
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "route_path_key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "requires_auth",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d8aa1a5813fedbb22128112fcdf9d16769811cd5d5ad3b66362cc5bb1750ae6b"
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n -- slack_team_id, \n -- slack_name, \n -- slack_command_script, \n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\", \n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\", \n webhook, \n deploy_to, \n error_handler, \n ai_resource, \n ai_models,\n code_completion_model,\n error_handler_extra_args, \n error_handler_muted_on_cancel, \n large_file_storage, \n git_sync,\n default_app,\n default_scripts,\n workspace.name,\n mute_critical_alerts,\n color,\n operator_settings\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "auto_invite_enabled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "auto_invite_as!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "auto_invite_mode!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "webhook",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "deploy_to",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "error_handler",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "ai_resource",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "ai_models",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "code_completion_model",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "error_handler_extra_args",
|
||||
"type_info": "Json"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "error_handler_muted_on_cancel",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "large_file_storage",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "git_sync",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "default_app",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "default_scripts",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "mute_critical_alerts",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "operator_settings",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "dc165e2d3e6cfc52d92b48500b5ca7dd94b46263c58163071c0ded1c54535727"
|
||||
}
|
||||
+2
-1
@@ -26,7 +26,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
"nats",
|
||||
"postgres"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT * FROM usr\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "operator",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "disabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e5fb3531f8bc7ef1f7484524f8c3bc9c48f71a44827ba0d01ac5588dc31082a2"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n schemaname AS schema_name,\n tablename AS table_name,\n CASE\n WHEN array_length(attnames, 1) = (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = pg_publication_tables.schemaname AND table_name = pg_publication_tables.tablename)\n THEN NULL\n ELSE attnames\n END AS columns,\n rowfilter AS where_clause\n FROM\n pg_publication_tables\n WHERE\n pubname = $1;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "schema_name",
|
||||
"type_info": "Name"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "table_name",
|
||||
"type_info": "Name"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "columns",
|
||||
"type_info": "NameArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "where_clause",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Name"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "fd5754fe3c6346ae28818a9d60d144a40f8884f47e5bbdd2824e939dafd8f154"
|
||||
}
|
||||
Generated
+66
-65
@@ -1025,7 +1025,7 @@ dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide 0.8.3",
|
||||
"miniz_oxide 0.8.4",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
@@ -1474,9 +1474,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.11+1.0.8"
|
||||
version = "0.1.12+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
|
||||
checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1668,9 +1668,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.28"
|
||||
version = "4.5.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
|
||||
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -1678,9 +1678,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.27"
|
||||
version = "4.5.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
|
||||
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -2776,7 +2776,7 @@ checksum = "688175eed35e7b3053ec114227894ef24786855405d8844058a48bffa997d85a"
|
||||
dependencies = [
|
||||
"deno_core",
|
||||
"deno_native_certs",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-tokio-stream",
|
||||
"rustls-webpki 0.102.8",
|
||||
@@ -3345,7 +3345,7 @@ checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"libz-sys",
|
||||
"miniz_oxide 0.8.3",
|
||||
"miniz_oxide 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4302,7 +4302,7 @@ dependencies = [
|
||||
"http 1.2.0",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -5077,7 +5077,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"gethostname",
|
||||
"mail-builder",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"rustls-pki-types",
|
||||
"smtp-proto",
|
||||
"tokio",
|
||||
@@ -5265,9 +5265,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
|
||||
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
@@ -5808,9 +5808,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.4.1+3.4.0"
|
||||
version = "300.4.2+3.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c"
|
||||
checksum = "168ce4e058f975fe43e89d9ccf78ca668601887ae736090aacc23ae353c298e2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -6425,7 +6425,7 @@ version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
dependencies = [
|
||||
"toml_edit 0.22.23",
|
||||
"toml_edit 0.22.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6576,9 +6576,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
|
||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
@@ -6586,12 +6586,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
|
||||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
@@ -6599,9 +6599,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
|
||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||
dependencies = [
|
||||
"prost",
|
||||
]
|
||||
@@ -6708,7 +6708,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"socket2",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
@@ -6726,7 +6726,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"ring 0.17.8",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.11",
|
||||
@@ -7129,7 +7129,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
@@ -7395,9 +7395,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.22"
|
||||
version = "0.23.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
|
||||
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -7479,7 +7479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"socket2",
|
||||
"tokio",
|
||||
]
|
||||
@@ -7786,12 +7786,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde-aux"
|
||||
version = "4.5.0"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95"
|
||||
checksum = "5290c39c5f6992b9dddbda28541d965dba46468294e6018a408fa297e6c602de"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -8375,7 +8376,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -9679,7 +9680,7 @@ version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
|
||||
dependencies = [
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -9791,7 +9792,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.23",
|
||||
"toml_edit 0.22.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9818,15 +9819,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.23"
|
||||
version = "0.22.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
|
||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||
dependencies = [
|
||||
"indexmap 2.7.1",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow 0.7.1",
|
||||
"winnow 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10453,7 +10454,7 @@ dependencies = [
|
||||
"log",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"rustls 0.23.22",
|
||||
"rustls 0.23.23",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -10858,7 +10859,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -10901,7 +10902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -10994,7 +10995,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -11012,7 +11013,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -11025,7 +11026,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -11039,7 +11040,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -11098,7 +11099,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -11112,7 +11113,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -11135,7 +11136,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -11147,7 +11148,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -11156,7 +11157,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11168,7 +11169,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -11180,7 +11181,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -11192,7 +11193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11204,7 +11205,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -11215,7 +11216,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -11226,7 +11227,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11246,7 +11247,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -11263,7 +11264,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11275,7 +11276,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11293,7 +11294,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.15",
|
||||
@@ -11315,7 +11316,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -11325,7 +11326,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11358,7 +11359,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -11368,7 +11369,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11652,9 +11653,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
|
||||
checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -30,7 +30,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.458.4"
|
||||
version = "1.460.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -315,10 +315,10 @@ async-stream = "^0"
|
||||
|
||||
opentelemetry = "0.27.0"
|
||||
tracing-opentelemetry = "0.28.0"
|
||||
opentelemetry_sdk = { version = "*", features = ["rt-tokio"] }
|
||||
opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.27.0", features = ["grpc-tonic", "tls"] }
|
||||
opentelemetry-appender-tracing = "0.27.0"
|
||||
opentelemetry-semantic-conventions = { version = "*", features = ["semconv_experimental"] }
|
||||
opentelemetry-semantic-conventions = { version = "0.27.0", features = ["semconv_experimental"] }
|
||||
|
||||
bollard = "0.18.1"
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE TRIGGER_KIND ADD VALUE IF NOT EXISTS 'postgres';
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add up migration script here
|
||||
GRANT ALL ON v2_as_queue TO windmill_admin;
|
||||
GRANT ALL ON v2_as_queue TO windmill_user;
|
||||
|
||||
GRANT ALL ON v2_as_completed_job TO windmill_admin;
|
||||
GRANT ALL ON v2_as_completed_job TO windmill_user;
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.458.4
|
||||
version: 1.460.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -8760,6 +8760,34 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/postgres_triggers/test:
|
||||
post:
|
||||
summary: test postgres connection
|
||||
operationId: testPostgresConnection
|
||||
tags:
|
||||
- postgres_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: test postgres connection
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
database:
|
||||
type: string
|
||||
required:
|
||||
- database
|
||||
responses:
|
||||
"200":
|
||||
description: successfuly connected to postgres
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/groups/list:
|
||||
get:
|
||||
summary: list instance groups
|
||||
@@ -13323,7 +13351,7 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- transaction_to_track
|
||||
|
||||
|
||||
TableToTrack:
|
||||
type: array
|
||||
items:
|
||||
@@ -14644,7 +14672,7 @@ components:
|
||||
|
||||
CaptureTriggerKind:
|
||||
type: string
|
||||
enum: [webhook, http, websocket, kafka, email, nats]
|
||||
enum: [webhook, http, websocket, kafka, email, nats, postgres]
|
||||
|
||||
Capture:
|
||||
type: object
|
||||
|
||||
@@ -38,6 +38,16 @@ use crate::http_triggers::{build_http_trigger_extra, HttpMethod};
|
||||
use crate::kafka_triggers_ee::KafkaTriggerConfigConnection;
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
use crate::nats_triggers_ee::NatsTriggerConfigConnection;
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
use crate::postgres_triggers::{
|
||||
create_logical_replication_slot_query, create_publication_query, drop_publication_query,
|
||||
generate_random_string, get_database_connection, PublicationData,
|
||||
};
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
use itertools::Itertools;
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
use pg_escape::quote_literal;
|
||||
|
||||
use crate::{
|
||||
args::WebhookArgs,
|
||||
db::{ApiAuthed, DB},
|
||||
@@ -78,7 +88,7 @@ pub fn workspaced_unauthed_service() -> Router {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize)]
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
|
||||
#[sqlx(type_name = "TRIGGER_KIND", rename_all = "lowercase")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TriggerKind {
|
||||
@@ -88,6 +98,7 @@ pub enum TriggerKind {
|
||||
Kafka,
|
||||
Email,
|
||||
Nats,
|
||||
Postgres,
|
||||
}
|
||||
|
||||
impl fmt::Display for TriggerKind {
|
||||
@@ -99,6 +110,7 @@ impl fmt::Display for TriggerKind {
|
||||
TriggerKind::Kafka => "kafka",
|
||||
TriggerKind::Email => "email",
|
||||
TriggerKind::Nats => "nats",
|
||||
TriggerKind::Postgres => "postgres",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
@@ -133,6 +145,16 @@ pub struct NatsTriggerConfig {
|
||||
pub use_jetstream: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PostgresTriggerConfig {
|
||||
pub postgres_resource_path: String,
|
||||
pub publication_name: Option<String>,
|
||||
pub replication_slot_name: Option<String>,
|
||||
pub publication: PublicationData,
|
||||
}
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct WebsocketTriggerConfig {
|
||||
pub url: String,
|
||||
@@ -145,6 +167,9 @@ pub struct WebsocketTriggerConfig {
|
||||
enum TriggerConfig {
|
||||
#[cfg(feature = "http_trigger")]
|
||||
Http(HttpTriggerConfig),
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
Postgres(PostgresTriggerConfig),
|
||||
#[cfg(feature = "websocket")]
|
||||
Websocket(WebsocketTriggerConfig),
|
||||
#[cfg(all(feature = "enterprise", feature = "kafka"))]
|
||||
Kafka(KafkaTriggerConfig),
|
||||
@@ -186,18 +211,88 @@ async fn get_configs(
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(configs))
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
async fn set_postgres_trigger_config(
|
||||
w_id: &str,
|
||||
authed: ApiAuthed,
|
||||
db: &DB,
|
||||
user_db: UserDB,
|
||||
mut capture_config: NewCaptureConfig,
|
||||
) -> Result<NewCaptureConfig> {
|
||||
let Some(TriggerConfig::Postgres(mut postgres_config)) = capture_config.trigger_config else {
|
||||
return Err(windmill_common::error::Error::BadRequest(
|
||||
"Invalid postgres config".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let mut connection = get_database_connection(
|
||||
authed,
|
||||
Some(user_db),
|
||||
&db,
|
||||
&postgres_config.postgres_resource_path,
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let publication_name = postgres_config
|
||||
.publication_name
|
||||
.get_or_insert(format!("windmill_capture_{}", generate_random_string()));
|
||||
let replication_slot_name = postgres_config
|
||||
.replication_slot_name
|
||||
.get_or_insert(publication_name.clone());
|
||||
|
||||
let query = drop_publication_query(&publication_name);
|
||||
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
let query = create_publication_query(
|
||||
&publication_name,
|
||||
postgres_config.publication.table_to_track.as_deref(),
|
||||
&postgres_config
|
||||
.publication
|
||||
.transaction_to_track
|
||||
.iter()
|
||||
.map(AsRef::as_ref)
|
||||
.collect_vec(),
|
||||
);
|
||||
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
let query = format!(
|
||||
"SELECT 1 from pg_replication_slots WHERE slot_name = {}",
|
||||
quote_literal(replication_slot_name)
|
||||
);
|
||||
|
||||
let row = sqlx::query(&query).fetch_optional(&mut connection).await?;
|
||||
|
||||
if row.is_none() {
|
||||
let query = create_logical_replication_slot_query(&replication_slot_name);
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
}
|
||||
capture_config.trigger_config = Some(TriggerConfig::Postgres(postgres_config));
|
||||
Ok(capture_config)
|
||||
}
|
||||
|
||||
async fn set_config(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
#[cfg(feature = "postgres_trigger")] Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(nc): Json<NewCaptureConfig>,
|
||||
) -> Result<()> {
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
let nc = if let TriggerKind::Postgres = nc.trigger_kind {
|
||||
set_postgres_trigger_config(&w_id, authed.clone(), &db, user_db.clone(), nc).await?
|
||||
}
|
||||
else {
|
||||
nc
|
||||
};
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
sqlx::query!(
|
||||
|
||||
@@ -75,7 +75,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/route_exists", post(exists_route))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, sqlx::Type)]
|
||||
#[derive(Serialize, Deserialize, sqlx::Type, Debug)]
|
||||
#[sqlx(type_name = "HTTP_METHOD", rename_all = "lowercase")]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum HttpMethod {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::{
|
||||
collections::{
|
||||
hash_map::Entry::{Occupied, Vacant},
|
||||
HashMap,
|
||||
},
|
||||
str::FromStr,
|
||||
use std::collections::{
|
||||
hash_map::Entry::{Occupied, Vacant},
|
||||
HashMap,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -14,29 +11,28 @@ use axum::{
|
||||
extract::{Path, Query},
|
||||
Extension, Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use http::StatusCode;
|
||||
use itertools::Itertools;
|
||||
use pg_escape::{quote_identifier, quote_literal};
|
||||
use quick_cache::sync::Cache;
|
||||
use rand::Rng;
|
||||
use rust_postgres::types::Type;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use sql_builder::{bind::Bind, SqlBuilder};
|
||||
use sqlx::{
|
||||
postgres::{types::Oid, PgConnectOptions, PgSslMode},
|
||||
Connection, FromRow, PgConnection, QueryBuilder,
|
||||
};
|
||||
use sqlx::{postgres::types::Oid, FromRow, PgConnection};
|
||||
use windmill_audit::{audit_ee::audit_log, ActionKind};
|
||||
use windmill_common::error::Error;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{self, JsonResult},
|
||||
error::{self, JsonResult, Result},
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
worker::CLOUD_HOSTED,
|
||||
};
|
||||
|
||||
use super::get_database_resource;
|
||||
use super::{
|
||||
create_logical_replication_slot_query, create_publication_query,
|
||||
drop_logical_replication_slot_query, drop_publication_query, generate_random_string,
|
||||
get_database_connection, ERROR_PUBLICATION_NAME_NOT_EXISTS, ERROR_REPLICATION_SLOT_NOT_EXISTS,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Debug)]
|
||||
@@ -88,7 +84,7 @@ impl Relations {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EditPostgresTrigger {
|
||||
replication_slot_name: String,
|
||||
publication_name: String,
|
||||
@@ -112,48 +108,39 @@ pub struct NewPostgresTrigger {
|
||||
publication: Option<PublicationData>,
|
||||
}
|
||||
|
||||
pub async fn get_database_connection(
|
||||
authed: ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
postgres_resource_path: &str,
|
||||
w_id: &str,
|
||||
) -> Result<PgConnection, windmill_common::error::Error> {
|
||||
let database = get_database_resource(authed, user_db, db, postgres_resource_path, w_id).await?;
|
||||
|
||||
Ok(get_raw_postgres_connection(&database).await?)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TestPostgres {
|
||||
pub postgres_resource_path: String,
|
||||
}
|
||||
|
||||
pub async fn get_raw_postgres_connection(db: &Database) -> Result<PgConnection, Error> {
|
||||
let options = {
|
||||
let sslmode = if !db.sslmode.is_empty() {
|
||||
PgSslMode::from_str(&db.sslmode)?
|
||||
} else {
|
||||
PgSslMode::Prefer
|
||||
};
|
||||
let options = PgConnectOptions::new()
|
||||
.host(&db.host)
|
||||
.database(&db.dbname)
|
||||
.port(db.port)
|
||||
.ssl_mode(sslmode)
|
||||
.username(&db.user);
|
||||
|
||||
let options = if !db.root_certificate_pem.is_empty() {
|
||||
options.ssl_root_cert_from_pem(db.root_certificate_pem.as_bytes().to_vec())
|
||||
} else {
|
||||
options
|
||||
};
|
||||
|
||||
if !db.password.is_empty() {
|
||||
options.password(&db.password)
|
||||
} else {
|
||||
options
|
||||
}
|
||||
};
|
||||
|
||||
PgConnection::connect_with(&options)
|
||||
pub async fn test_postgres_connection(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(workspace_id): Path<String>,
|
||||
Json(test_postgres): Json<TestPostgres>,
|
||||
) -> Result<()> {
|
||||
let connect_f = async {
|
||||
get_database_connection(
|
||||
authed,
|
||||
Some(user_db),
|
||||
&db,
|
||||
&test_postgres.postgres_resource_path,
|
||||
&workspace_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
.map_err(|err| {
|
||||
error::Error::BadConfig(format!("Error connecting to postgres: {}", err.to_string()))
|
||||
})
|
||||
};
|
||||
tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
error::Error::BadConfig(format!("Timeout connecting to postgres after 30 seconds"))
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -165,19 +152,20 @@ pub enum Language {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TemplateScript {
|
||||
postgres_resource_path: String,
|
||||
#[serde(deserialize_with = "check_if_not_duplication_relation")]
|
||||
#[serde(deserialize_with = "check_if_valid_relation")]
|
||||
relations: Option<Vec<Relations>>,
|
||||
language: Language,
|
||||
}
|
||||
|
||||
fn check_if_not_duplication_relation<'de, D>(
|
||||
fn check_if_valid_relation<'de, D>(
|
||||
relations: D,
|
||||
) -> std::result::Result<Option<Vec<Relations>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let relations: Option<Vec<Relations>> = Option::deserialize(relations)?;
|
||||
|
||||
let mut track_all_table_in_schema = false;
|
||||
let mut track_specific_columns_in_table = false;
|
||||
match relations {
|
||||
Some(relations) => {
|
||||
for relation in relations.iter() {
|
||||
@@ -187,12 +175,25 @@ where
|
||||
));
|
||||
}
|
||||
|
||||
if !track_all_table_in_schema && relation.table_to_track.is_empty() {
|
||||
track_all_table_in_schema = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
for table_to_track in relation.table_to_track.iter() {
|
||||
if table_to_track.table_name.trim().is_empty() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"Table name must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !track_specific_columns_in_table && !table_to_track.columns_name.is_empty() {
|
||||
track_specific_columns_in_table = true;
|
||||
}
|
||||
}
|
||||
|
||||
if track_all_table_in_schema && track_specific_columns_in_table {
|
||||
return Err(serde::de::Error::custom("Incompatible tracking options. Schema-level tracking and specific table tracking with column selection cannot be used together. Refer to the documentation for valid configurations."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,13 +250,105 @@ pub struct SetEnabled {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PostgresPublicationReplication {
|
||||
publication_name: String,
|
||||
replication_slot_name: String,
|
||||
}
|
||||
|
||||
impl PostgresPublicationReplication {
|
||||
pub fn new(
|
||||
publication_name: String,
|
||||
replication_slot_name: String,
|
||||
) -> PostgresPublicationReplication {
|
||||
PostgresPublicationReplication { publication_name, replication_slot_name }
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_if_publication_exist(
|
||||
connection: &mut PgConnection,
|
||||
publication_name: &str,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"SELECT pubname FROM pg_publication WHERE pubname = $1",
|
||||
publication_name
|
||||
)
|
||||
.fetch_one(connection)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
Error::BadRequest(ERROR_PUBLICATION_NAME_NOT_EXISTS.to_string())
|
||||
}
|
||||
err => Error::SqlErr { error: err, location: "pg_trigger".to_string() },
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_if_logical_replication_slot_exist(
|
||||
connection: &mut PgConnection,
|
||||
replication_slot_name: &str,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"SELECT slot_name FROM pg_replication_slots where slot_name = $1",
|
||||
&replication_slot_name
|
||||
)
|
||||
.fetch_one(connection)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
_ => Error::BadRequest(ERROR_REPLICATION_SLOT_NOT_EXISTS.to_string()),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_custom_slot_and_publication_inner(
|
||||
authed: ApiAuthed,
|
||||
user_db: UserDB,
|
||||
db: &DB,
|
||||
postgres_resource_path: &str,
|
||||
w_id: &str,
|
||||
publication: &PublicationData,
|
||||
) -> Result<PostgresPublicationReplication> {
|
||||
let publication_name = format!("windmill_trigger_{}", generate_random_string());
|
||||
let replication_slot_name = publication_name.clone();
|
||||
|
||||
let query = create_publication_query(
|
||||
&publication_name,
|
||||
publication.table_to_track.as_deref(),
|
||||
&publication
|
||||
.transaction_to_track
|
||||
.iter()
|
||||
.map(AsRef::as_ref)
|
||||
.collect_vec(),
|
||||
);
|
||||
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
&postgres_resource_path,
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
let query = create_logical_replication_slot_query(&replication_slot_name);
|
||||
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
Ok(PostgresPublicationReplication::new(
|
||||
publication_name,
|
||||
replication_slot_name,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn create_postgres_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(new_postgres_trigger): Json<NewPostgresTrigger>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
) -> Result<(StatusCode, String)> {
|
||||
if *CLOUD_HOSTED {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Postgres triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
|
||||
@@ -278,59 +371,32 @@ pub async fn create_postgres_trigger(
|
||||
"Publication data is missing".to_string(),
|
||||
));
|
||||
}
|
||||
let (pub_name, slot_name) = if publication_name.is_none() && replication_slot_name.is_none() {
|
||||
if publication.is_none() {
|
||||
return Err(Error::BadRequest("publication must be set".to_string()));
|
||||
}
|
||||
let PostgresPublicationReplication { publication_name, replication_slot_name } =
|
||||
create_custom_slot_and_publication_inner(
|
||||
authed.clone(),
|
||||
user_db.clone(),
|
||||
&db,
|
||||
&postgres_resource_path,
|
||||
&w_id,
|
||||
&publication.unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let create_slot = replication_slot_name.is_none();
|
||||
let create_publication = publication_name.is_none();
|
||||
|
||||
let name;
|
||||
let mut pub_name = publication_name.as_deref().unwrap_or_default();
|
||||
let mut slot_name = replication_slot_name.as_deref().unwrap_or_default();
|
||||
if create_publication || create_slot {
|
||||
let generate_random_string = move || {
|
||||
let timestamp = Utc::now().timestamp_millis().to_string();
|
||||
let mut rng = rand::rng();
|
||||
let charset = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
let random_part = (0..10)
|
||||
.map(|_| {
|
||||
charset
|
||||
.chars()
|
||||
.nth(rng.random_range(0..charset.len()))
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
format!("{}_{}", timestamp, random_part)
|
||||
};
|
||||
|
||||
name = format!("windmill_{}", generate_random_string());
|
||||
pub_name = &name;
|
||||
slot_name = &name;
|
||||
let publication = publication.unwrap();
|
||||
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
&postgres_resource_path,
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
new_publication(
|
||||
&mut connection,
|
||||
pub_name,
|
||||
publication.table_to_track.as_deref(),
|
||||
&publication
|
||||
.transaction_to_track
|
||||
.iter()
|
||||
.map(AsRef::as_ref)
|
||||
.collect_vec(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
new_slot(&mut connection, slot_name).await?;
|
||||
}
|
||||
(publication_name, replication_slot_name)
|
||||
} else {
|
||||
if publication_name.is_none() {
|
||||
return Err(Error::BadRequest("Missing publication name".to_string()));
|
||||
} else if replication_slot_name.is_none() {
|
||||
return Err(Error::BadRequest(
|
||||
"Missing replication slot name".to_string(),
|
||||
));
|
||||
}
|
||||
(replication_slot_name.unwrap(), publication_name.unwrap())
|
||||
};
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
@@ -450,10 +516,10 @@ pub async fn list_postgres_triggers(
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct PublicationData {
|
||||
#[serde(default, deserialize_with = "check_if_not_duplication_relation")]
|
||||
table_to_track: Option<Vec<Relations>>,
|
||||
#[serde(default, deserialize_with = "check_if_valid_relation")]
|
||||
pub table_to_track: Option<Vec<Relations>>,
|
||||
#[serde(deserialize_with = "check_if_valid_transaction_type")]
|
||||
transaction_to_track: Vec<String>,
|
||||
pub transaction_to_track: Vec<String>,
|
||||
}
|
||||
|
||||
fn check_if_valid_transaction_type<'de, D>(
|
||||
@@ -509,7 +575,7 @@ pub async fn list_slot_name(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, postgres_resource_path)): Path<(String, String)>,
|
||||
) -> error::Result<Json<Vec<SlotList>>> {
|
||||
) -> Result<Json<Vec<SlotList>>> {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
@@ -543,28 +609,13 @@ pub struct Slot {
|
||||
name: String,
|
||||
}
|
||||
|
||||
async fn new_slot(connection: &mut PgConnection, name: &str) -> error::Result<()> {
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
pg_create_logical_replication_slot({}, 'pgoutput');"#,
|
||||
quote_literal(&name)
|
||||
);
|
||||
|
||||
sqlx::query(&query).execute(connection).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_slot(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, postgres_resource_path)): Path<(String, String)>,
|
||||
Json(Slot { name }): Json<Slot>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
@@ -574,7 +625,9 @@ pub async fn create_slot(
|
||||
)
|
||||
.await?;
|
||||
|
||||
new_slot(&mut connection, &name).await?;
|
||||
let query = create_logical_replication_slot_query(&name);
|
||||
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
Ok(format!("Slot {} created!", name))
|
||||
}
|
||||
@@ -585,7 +638,7 @@ pub async fn drop_slot_name(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, postgres_resource_path)): Path<(String, String)>,
|
||||
Json(Slot { name }): Json<Slot>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
@@ -595,7 +648,7 @@ pub async fn drop_slot_name(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = format!("SELECT pg_drop_replication_slot({});", quote_literal(&name));
|
||||
let query = drop_logical_replication_slot_query(&name);
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
Ok(format!("Slot name {} deleted!", name))
|
||||
@@ -610,7 +663,7 @@ pub async fn list_database_publication(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, postgres_resource_path)): Path<(String, String)>,
|
||||
) -> error::Result<Json<Vec<String>>> {
|
||||
) -> Result<Json<Vec<String>>> {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
@@ -640,7 +693,7 @@ pub async fn get_publication_info(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, publication_name, postgres_resource_path)): Path<(String, String, String)>,
|
||||
) -> error::Result<Json<PublicationData>> {
|
||||
) -> Result<Json<PublicationData>> {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
@@ -651,13 +704,13 @@ pub async fn get_publication_info(
|
||||
.await?;
|
||||
|
||||
let publication_data =
|
||||
get_publication_scope_and_transaction(&publication_name, &mut connection).await;
|
||||
get_publication_scope_and_transaction(&mut connection, &publication_name).await;
|
||||
|
||||
let (all_table, transaction_to_track) = match publication_data {
|
||||
let (all_table, transaction_to_track) = match publication_data {
|
||||
Ok(pub_data) => pub_data,
|
||||
Err(Error::SqlErr { error: sqlx::Error::RowNotFound, .. }) => {
|
||||
return Err(Error::NotFound(
|
||||
"Publication was not found, please create a new publication".to_string(),
|
||||
ERROR_PUBLICATION_NAME_NOT_EXISTS.to_string(),
|
||||
))
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
@@ -674,82 +727,13 @@ pub async fn get_publication_info(
|
||||
)))
|
||||
}
|
||||
|
||||
async fn new_publication(
|
||||
connection: &mut PgConnection,
|
||||
publication_name: &str,
|
||||
table_to_track: Option<&[Relations]>,
|
||||
transaction_to_track: &[&str],
|
||||
) -> Result<(), Error> {
|
||||
let mut query = QueryBuilder::new("CREATE PUBLICATION ");
|
||||
|
||||
query.push(quote_identifier(publication_name));
|
||||
|
||||
match table_to_track {
|
||||
Some(database_component) if !database_component.is_empty() => {
|
||||
query.push(" FOR");
|
||||
for (i, schema) in database_component.iter().enumerate() {
|
||||
if schema.table_to_track.is_empty() {
|
||||
query.push(" TABLES IN SCHEMA ");
|
||||
query.push(quote_identifier(&schema.schema_name));
|
||||
} else {
|
||||
query.push(" TABLE ONLY ");
|
||||
for (j, table) in schema.table_to_track.iter().enumerate() {
|
||||
let table_name = quote_identifier(&table.table_name);
|
||||
let schema_name = quote_identifier(&schema.schema_name);
|
||||
let full_name = format!("{}.{}", &schema_name, &table_name);
|
||||
query.push(full_name);
|
||||
if !table.columns_name.is_empty() {
|
||||
query.push(" (");
|
||||
let columns = table
|
||||
.columns_name
|
||||
.iter()
|
||||
.map(|column| quote_identifier(column))
|
||||
.join(", ");
|
||||
query.push(&columns);
|
||||
query.push(")");
|
||||
}
|
||||
|
||||
if let Some(where_clause) = &table.where_clause {
|
||||
query.push(" WHERE (");
|
||||
query.push(where_clause);
|
||||
query.push(')');
|
||||
}
|
||||
|
||||
if j + 1 != schema.table_to_track.len() {
|
||||
query.push(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
if i < database_component.len() - 1 {
|
||||
query.push(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query.push(" FOR ALL TABLES ");
|
||||
}
|
||||
};
|
||||
|
||||
if !transaction_to_track.is_empty() {
|
||||
let transactions = || transaction_to_track.iter().join(", ");
|
||||
query.push(" WITH (publish = '");
|
||||
query.push(transactions());
|
||||
query.push("');");
|
||||
}
|
||||
|
||||
let query = query.build();
|
||||
query.execute(&mut *connection).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_publication(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, publication_name, postgres_resource_path)): Path<(String, String, String)>,
|
||||
Json(publication_data): Json<PublicationData>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let PublicationData { table_to_track, transaction_to_track } = publication_data;
|
||||
|
||||
let mut connection = get_database_connection(
|
||||
@@ -761,13 +745,13 @@ pub async fn create_publication(
|
||||
)
|
||||
.await?;
|
||||
|
||||
new_publication(
|
||||
&mut connection,
|
||||
let query = create_publication_query(
|
||||
&publication_name,
|
||||
table_to_track.as_deref(),
|
||||
&transaction_to_track.iter().map(AsRef::as_ref).collect_vec(),
|
||||
)
|
||||
.await?;
|
||||
);
|
||||
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
Ok(format!(
|
||||
"Publication {} successfully created!",
|
||||
@@ -775,24 +759,12 @@ pub async fn create_publication(
|
||||
))
|
||||
}
|
||||
|
||||
async fn drop_publication(
|
||||
publication_name: &str,
|
||||
connection: &mut PgConnection,
|
||||
) -> Result<(), Error> {
|
||||
let mut query = QueryBuilder::new("DROP PUBLICATION IF EXISTS ");
|
||||
let quoted_publication_name = quote_identifier(publication_name);
|
||||
query.push(quoted_publication_name);
|
||||
query.push(";");
|
||||
query.build().execute(&mut *connection).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_publication(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, publication_name, postgres_resource_path)): Path<(String, String, String)>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
@@ -802,7 +774,9 @@ pub async fn delete_publication(
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop_publication(&publication_name, &mut connection).await?;
|
||||
let query = drop_publication_query(&publication_name);
|
||||
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
|
||||
Ok(format!(
|
||||
"Publication {} successfully deleted!",
|
||||
@@ -810,65 +784,61 @@ pub async fn delete_publication(
|
||||
))
|
||||
}
|
||||
|
||||
async fn update_publication(
|
||||
connection: &mut PgConnection,
|
||||
pub fn get_update_publication_query(
|
||||
publication_name: &str,
|
||||
PublicationData { table_to_track, transaction_to_track }: PublicationData,
|
||||
) -> error::Result<String> {
|
||||
let (all_table, _) =
|
||||
get_publication_scope_and_transaction(&publication_name, connection).await?;
|
||||
|
||||
let mut query = QueryBuilder::new("");
|
||||
all_table: bool,
|
||||
) -> Vec<String> {
|
||||
let quoted_publication_name = quote_identifier(&publication_name);
|
||||
|
||||
let transaction_to_track_as_str = transaction_to_track.iter().join(",");
|
||||
|
||||
let mut queries = Vec::with_capacity(2);
|
||||
match table_to_track {
|
||||
Some(ref relations) if !relations.is_empty() => {
|
||||
if all_table {
|
||||
drop_publication(&publication_name, connection).await?;
|
||||
new_publication(
|
||||
connection,
|
||||
queries.push(drop_publication_query(&publication_name));
|
||||
queries.push(create_publication_query(
|
||||
&publication_name,
|
||||
table_to_track.as_deref(),
|
||||
&transaction_to_track.iter().map(AsRef::as_ref).collect_vec(),
|
||||
)
|
||||
.await?;
|
||||
));
|
||||
} else {
|
||||
query.push("ALTER PUBLICATION ");
|
||||
query.push("ed_publication_name);
|
||||
query.push(" SET");
|
||||
let mut query = String::from("");
|
||||
|
||||
query.push_str("ALTER PUBLICATION ");
|
||||
query.push_str("ed_publication_name);
|
||||
query.push_str(" SET");
|
||||
for (i, schema) in relations.iter().enumerate() {
|
||||
if schema.table_to_track.is_empty() {
|
||||
query.push(" TABLES IN SCHEMA ");
|
||||
query.push_str(" TABLES IN SCHEMA ");
|
||||
let quoted_schema = quote_identifier(&schema.schema_name);
|
||||
query.push("ed_schema);
|
||||
query.push_str("ed_schema);
|
||||
} else {
|
||||
query.push(" TABLE ONLY ");
|
||||
query.push_str(" TABLE ONLY ");
|
||||
for (j, table) in schema.table_to_track.iter().enumerate() {
|
||||
let table_name = quote_identifier(&table.table_name);
|
||||
let schema_name = quote_identifier(&schema.schema_name);
|
||||
let full_name = format!("{}.{}", &schema_name, &table_name);
|
||||
query.push(&full_name);
|
||||
query.push_str(&full_name);
|
||||
if !table.columns_name.is_empty() {
|
||||
query.push(" (");
|
||||
query.push_str(" (");
|
||||
let columns = table
|
||||
.columns_name
|
||||
.iter()
|
||||
.map(|column| quote_identifier(column))
|
||||
.join(", ");
|
||||
query.push(&columns);
|
||||
query.push(") ");
|
||||
query.push_str(&columns);
|
||||
query.push_str(") ");
|
||||
}
|
||||
|
||||
if let Some(where_clause) = &table.where_clause {
|
||||
query.push(" WHERE (");
|
||||
query.push(where_clause);
|
||||
query.push_str(" WHERE (");
|
||||
query.push_str(where_clause);
|
||||
query.push(')');
|
||||
}
|
||||
|
||||
if j + 1 != schema.table_to_track.len() {
|
||||
query.push(", ");
|
||||
query.push_str(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,36 +846,35 @@ async fn update_publication(
|
||||
query.push(',');
|
||||
}
|
||||
}
|
||||
query.push(";");
|
||||
query.build().execute(&mut *connection).await?;
|
||||
query.reset();
|
||||
query.push("ALTER PUBLICATION ");
|
||||
query.push("ed_publication_name);
|
||||
query.push(format!(
|
||||
query.push(';');
|
||||
|
||||
queries.push(query);
|
||||
|
||||
let mut query = String::new();
|
||||
|
||||
query.push_str("ALTER PUBLICATION ");
|
||||
query.push_str("ed_publication_name);
|
||||
query.push_str(&format!(
|
||||
" SET (publish = '{}');",
|
||||
transaction_to_track_as_str
|
||||
));
|
||||
queries.push(query);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
drop_publication(&publication_name, connection).await?;
|
||||
queries.push(drop_publication_query(&publication_name));
|
||||
let to_execute = format!(
|
||||
r#"
|
||||
CREATE
|
||||
PUBLICATION {} FOR ALL TABLES WITH (publish = '{}')
|
||||
PUBLICATION {} FOR ALL TABLES WITH (publish = '{}');
|
||||
"#,
|
||||
quoted_publication_name, transaction_to_track_as_str
|
||||
);
|
||||
query.push(&to_execute);
|
||||
queries.push(to_execute);
|
||||
}
|
||||
};
|
||||
|
||||
query.build().execute(&mut *connection).await?;
|
||||
|
||||
Ok(format!(
|
||||
"Publication {} successfully updated!",
|
||||
publication_name
|
||||
))
|
||||
queries
|
||||
}
|
||||
|
||||
pub async fn alter_publication(
|
||||
@@ -914,7 +883,7 @@ pub async fn alter_publication(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, publication_name, postgres_resource_path)): Path<(String, String, String)>,
|
||||
Json(publication_data): Json<PublicationData>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
@@ -923,15 +892,28 @@ pub async fn alter_publication(
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
let message = update_publication(&mut connection, &publication_name, publication_data).await?;
|
||||
|
||||
Ok(message)
|
||||
check_if_publication_exist(&mut connection, &publication_name).await?;
|
||||
|
||||
let (all_table, _) =
|
||||
get_publication_scope_and_transaction(&mut connection, &publication_name).await?;
|
||||
|
||||
let queries = get_update_publication_query(&publication_name, publication_data, all_table);
|
||||
|
||||
for query in queries {
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"Publication {} updated with success",
|
||||
publication_name
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_publication_scope_and_transaction(
|
||||
publication_name: &str,
|
||||
connection: &mut PgConnection,
|
||||
) -> Result<(bool, Vec<String>), Error> {
|
||||
publication_name: &str,
|
||||
) -> std::result::Result<(bool, Vec<String>), Error> {
|
||||
#[derive(Debug, Deserialize, FromRow)]
|
||||
struct PublicationTransaction {
|
||||
all_table: bool,
|
||||
@@ -976,7 +958,7 @@ async fn get_publication_scope_and_transaction(
|
||||
async fn get_tracked_relations(
|
||||
connection: &mut PgConnection,
|
||||
publication_name: &str,
|
||||
) -> error::Result<Vec<Relations>> {
|
||||
) -> Result<Vec<Relations>> {
|
||||
#[derive(Debug, Deserialize, FromRow)]
|
||||
struct PublicationData {
|
||||
schema_name: Option<String>,
|
||||
@@ -989,14 +971,18 @@ async fn get_tracked_relations(
|
||||
PublicationData,
|
||||
r#"
|
||||
SELECT
|
||||
schemaname AS schema_name,
|
||||
tablename AS table_name,
|
||||
attnames AS columns,
|
||||
rowfilter AS where_clause
|
||||
schemaname AS schema_name,
|
||||
tablename AS table_name,
|
||||
CASE
|
||||
WHEN array_length(attnames, 1) = (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = pg_publication_tables.schemaname AND table_name = pg_publication_tables.tablename)
|
||||
THEN NULL
|
||||
ELSE attnames
|
||||
END AS columns,
|
||||
rowfilter AS where_clause
|
||||
FROM
|
||||
pg_publication_tables
|
||||
WHERE
|
||||
pubname = $1
|
||||
pubname = $1;
|
||||
"#,
|
||||
publication_name
|
||||
)
|
||||
@@ -1011,7 +997,7 @@ async fn get_tracked_relations(
|
||||
let table_to_track = TableToTrack::new(
|
||||
publication.table_name.unwrap(),
|
||||
publication.where_clause,
|
||||
publication.columns.unwrap(),
|
||||
publication.columns.unwrap_or_default(),
|
||||
);
|
||||
match entry {
|
||||
Occupied(mut occuped) => {
|
||||
@@ -1075,8 +1061,9 @@ pub async fn update_postgres_trigger(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(postgres_trigger): Json<EditPostgresTrigger>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let workspace_path = path.to_path();
|
||||
|
||||
let EditPostgresTrigger {
|
||||
replication_slot_name,
|
||||
publication_name,
|
||||
@@ -1087,16 +1074,26 @@ pub async fn update_postgres_trigger(
|
||||
publication,
|
||||
} = postgres_trigger;
|
||||
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
&postgres_resource_path,
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
check_if_logical_replication_slot_exist(&mut connection, &replication_slot_name).await?;
|
||||
|
||||
if let Some(publication) = publication {
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
&postgres_resource_path,
|
||||
&w_id,
|
||||
)
|
||||
.await?;
|
||||
update_publication(&mut connection, &publication_name, publication).await?;
|
||||
check_if_publication_exist(&mut connection, &publication_name).await?;
|
||||
let (all_table, _) =
|
||||
get_publication_scope_and_transaction(&mut connection, &publication_name).await?;
|
||||
|
||||
let queries = get_update_publication_query(&publication_name, publication, all_table);
|
||||
for query in queries {
|
||||
sqlx::query(&query).execute(&mut connection).await?;
|
||||
}
|
||||
}
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
@@ -1153,7 +1150,7 @@ pub async fn delete_postgres_trigger(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
sqlx::query!(
|
||||
@@ -1213,7 +1210,7 @@ pub async fn set_enabled(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Json(payload): Json<SetEnabled>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let path = path.to_path();
|
||||
|
||||
@@ -1264,7 +1261,7 @@ pub async fn set_enabled(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_template_script(Path((_, id)): Path<(String, String)>) -> error::Result<String> {
|
||||
pub async fn get_template_script(Path((_, id)): Path<(String, String)>) -> Result<String> {
|
||||
let template = if let Some((_, template)) = TEMPLATE.remove(&id) {
|
||||
template
|
||||
} else {
|
||||
@@ -1279,7 +1276,7 @@ pub async fn create_template_script(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(template_script): Json<TemplateScript>,
|
||||
) -> error::Result<String> {
|
||||
) -> Result<String> {
|
||||
let TemplateScript { postgres_resource_path, relations, language } = template_script;
|
||||
if relations.is_none() {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -1460,4 +1457,4 @@ pub async fn is_database_in_logical_level(
|
||||
};
|
||||
|
||||
Ok(Json(is_logical))
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,17 @@ use crate::{
|
||||
resources::get_resource_value_interpolated_internal,
|
||||
users::fetch_api_authed,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use itertools::Itertools;
|
||||
use pg_escape::{quote_identifier, quote_literal};
|
||||
use rand::Rng;
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::{
|
||||
postgres::{PgConnectOptions, PgSslMode},
|
||||
Connection, PgConnection,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use axum::{
|
||||
routing::{delete, get, post},
|
||||
@@ -16,8 +25,10 @@ use handler::{
|
||||
create_template_script, delete_postgres_trigger, delete_publication, drop_slot_name,
|
||||
exists_postgres_trigger, get_postgres_trigger, get_publication_info, get_template_script,
|
||||
is_database_in_logical_level, list_database_publication, list_postgres_triggers,
|
||||
list_slot_name, set_enabled, update_postgres_trigger, Database,
|
||||
list_slot_name, set_enabled, test_postgres_connection, update_postgres_trigger, Database,
|
||||
Relations,
|
||||
};
|
||||
pub use handler::PostgresTrigger;
|
||||
use windmill_common::{db::UserDB, error::Error, utils::StripPath};
|
||||
use windmill_queue::PushArgsOwned;
|
||||
|
||||
@@ -30,9 +41,167 @@ mod relation;
|
||||
mod replication_message;
|
||||
mod trigger;
|
||||
|
||||
pub use handler::PostgresTrigger;
|
||||
pub use handler::PublicationData;
|
||||
pub use trigger::start_database;
|
||||
|
||||
const ERROR_REPLICATION_SLOT_NOT_EXISTS: &str = r#"The replication slot associated with this trigger no longer exists. Recreate a new replication slot or select an existing one in the advanced tab, or delete and recreate a new trigger"#;
|
||||
|
||||
const ERROR_PUBLICATION_NAME_NOT_EXISTS: &str = r#"The publication associated with this trigger no longer exists. Recreate a new publication or select an existing one in the advanced tab, or delete and recreate a new trigger"#;
|
||||
|
||||
pub async fn get_database_connection(
|
||||
authed: ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
db: &DB,
|
||||
postgres_resource_path: &str,
|
||||
w_id: &str,
|
||||
) -> std::result::Result<PgConnection, windmill_common::error::Error> {
|
||||
let database = get_database_resource(authed, user_db, db, postgres_resource_path, w_id).await?;
|
||||
|
||||
Ok(get_raw_postgres_connection(&database).await?)
|
||||
}
|
||||
|
||||
pub async fn get_raw_postgres_connection(
|
||||
db: &Database,
|
||||
) -> std::result::Result<PgConnection, Error> {
|
||||
let options = {
|
||||
let sslmode = if !db.sslmode.is_empty() {
|
||||
PgSslMode::from_str(&db.sslmode)?
|
||||
} else {
|
||||
PgSslMode::Prefer
|
||||
};
|
||||
let options = PgConnectOptions::new()
|
||||
.host(&db.host)
|
||||
.database(&db.dbname)
|
||||
.port(db.port)
|
||||
.ssl_mode(sslmode)
|
||||
.username(&db.user);
|
||||
|
||||
let options = if !db.root_certificate_pem.is_empty() {
|
||||
options.ssl_root_cert_from_pem(db.root_certificate_pem.as_bytes().to_vec())
|
||||
} else {
|
||||
options
|
||||
};
|
||||
|
||||
if !db.password.is_empty() {
|
||||
options.password(&db.password)
|
||||
} else {
|
||||
options
|
||||
}
|
||||
};
|
||||
|
||||
Ok(PgConnection::connect_with(&options).await?)
|
||||
}
|
||||
|
||||
pub fn create_logical_replication_slot_query(name: &str) -> String {
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
pg_create_logical_replication_slot({}, 'pgoutput');"#,
|
||||
quote_literal(&name)
|
||||
);
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
pub fn create_publication_query(
|
||||
publication_name: &str,
|
||||
table_to_track: Option<&[Relations]>,
|
||||
transaction_to_track: &[&str],
|
||||
) -> String {
|
||||
let mut query = String::from("CREATE PUBLICATION ");
|
||||
|
||||
query.push_str("e_identifier(publication_name));
|
||||
|
||||
match table_to_track {
|
||||
Some(database_component) if !database_component.is_empty() => {
|
||||
query.push_str(" FOR");
|
||||
for (i, schema) in database_component.iter().enumerate() {
|
||||
if schema.table_to_track.is_empty() {
|
||||
query.push_str(" TABLES IN SCHEMA ");
|
||||
query.push_str("e_identifier(&schema.schema_name));
|
||||
} else {
|
||||
query.push_str(" TABLE ONLY ");
|
||||
for (j, table) in schema.table_to_track.iter().enumerate() {
|
||||
let table_name = quote_identifier(&table.table_name);
|
||||
let schema_name = quote_identifier(&schema.schema_name);
|
||||
let full_name = format!("{}.{}", &schema_name, &table_name);
|
||||
query.push_str(&full_name);
|
||||
if !table.columns_name.is_empty() {
|
||||
query.push_str(" (");
|
||||
let columns = table
|
||||
.columns_name
|
||||
.iter()
|
||||
.map(|column| quote_identifier(column))
|
||||
.join(", ");
|
||||
query.push_str(&columns);
|
||||
query.push_str(")");
|
||||
}
|
||||
|
||||
if let Some(where_clause) = &table.where_clause {
|
||||
query.push_str(" WHERE (");
|
||||
query.push_str(where_clause);
|
||||
query.push(')');
|
||||
}
|
||||
|
||||
if j + 1 != schema.table_to_track.len() {
|
||||
query.push_str(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
if i < database_component.len() - 1 {
|
||||
query.push_str(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
query.push_str(" FOR ALL TABLES ");
|
||||
}
|
||||
};
|
||||
|
||||
if !transaction_to_track.is_empty() {
|
||||
let transactions = || transaction_to_track.iter().join(", ");
|
||||
query.push_str(" WITH (publish = '");
|
||||
query.push_str(&transactions());
|
||||
query.push_str("');");
|
||||
}
|
||||
|
||||
query
|
||||
}
|
||||
|
||||
pub fn drop_publication_query(publication_name: &str) -> String {
|
||||
let mut query = String::from("DROP PUBLICATION IF EXISTS ");
|
||||
let quoted_publication_name = quote_identifier(publication_name);
|
||||
query.push_str("ed_publication_name);
|
||||
query.push_str(";");
|
||||
query
|
||||
}
|
||||
|
||||
pub fn drop_logical_replication_slot_query(replication_slot_name: &str) -> String {
|
||||
format!(
|
||||
"SELECT pg_drop_replication_slot({});",
|
||||
quote_literal(&replication_slot_name)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_random_string() -> String {
|
||||
let timestamp = Utc::now().timestamp_millis().to_string();
|
||||
let mut rng = rand::rng();
|
||||
let charset = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
let random_part = (0..10)
|
||||
.map(|_| {
|
||||
charset
|
||||
.chars()
|
||||
.nth(rng.random_range(0..charset.len()))
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
format!("{}_{}", timestamp, random_part)
|
||||
}
|
||||
|
||||
pub async fn get_database_resource(
|
||||
authed: ApiAuthed,
|
||||
user_db: Option<UserDB>,
|
||||
@@ -87,6 +256,7 @@ fn slot_service() -> Router {
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
.route("/test", post(test_postgres_connection))
|
||||
.route("/create", post(create_postgres_trigger))
|
||||
.route("/list", get(list_postgres_triggers))
|
||||
.route("/get/*path", get(get_postgres_trigger))
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::{collections::HashMap, pin::Pin};
|
||||
|
||||
use crate::{
|
||||
db::DB,
|
||||
capture::{insert_capture_payload, PostgresTriggerConfig, TriggerKind},
|
||||
db::{ApiAuthed, DB},
|
||||
postgres_triggers::{
|
||||
get_database_resource,
|
||||
relation::RelationConverter,
|
||||
@@ -21,13 +22,20 @@ use pg_escape::{quote_identifier, quote_literal};
|
||||
use rand::seq::SliceRandom;
|
||||
use rust_postgres::{config::SslMode, Client, Config, CopyBothDuplex, SimpleQueryMessage};
|
||||
use rust_postgres_native_tls::MakeTlsConnector;
|
||||
use serde::Deserialize;
|
||||
use serde_json::value::RawValue;
|
||||
use sqlx::types::Json as SqlxJson;
|
||||
|
||||
use windmill_common::{
|
||||
db::UserDB, utils::report_critical_error, worker::to_raw_value, INSTANCE_NAME,
|
||||
db::UserDB, error, utils::report_critical_error, worker::to_raw_value, INSTANCE_NAME,
|
||||
};
|
||||
use windmill_queue::PushArgsOwned;
|
||||
|
||||
use super::{
|
||||
drop_logical_replication_slot_query, drop_publication_query, get_database_connection,
|
||||
handler::{Database, PostgresTrigger},
|
||||
replication_message::PrimaryKeepAliveBody,
|
||||
ERROR_PUBLICATION_NAME_NOT_EXISTS, ERROR_REPLICATION_SLOT_NOT_EXISTS,
|
||||
};
|
||||
|
||||
pub struct LogicalReplicationSettings {
|
||||
@@ -106,6 +114,7 @@ impl PostgresSimpleClient {
|
||||
let connector = MakeTlsConnector::new(TlsConnector::new()?);
|
||||
|
||||
let (client, connection) = config.connect(connector).await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
tracing::debug!("{:#?}", e);
|
||||
@@ -116,6 +125,13 @@ impl PostgresSimpleClient {
|
||||
Ok(PostgresSimpleClient(client))
|
||||
}
|
||||
|
||||
async fn execute_query(
|
||||
&self,
|
||||
query: &str,
|
||||
) -> Result<Vec<SimpleQueryMessage>, rust_postgres::Error> {
|
||||
self.0.simple_query(query).await
|
||||
}
|
||||
|
||||
async fn get_logical_replication_stream(
|
||||
&self,
|
||||
publication_name: &str,
|
||||
@@ -162,75 +178,9 @@ impl PostgresSimpleClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_ping(
|
||||
db: &DB,
|
||||
postgres_trigger: &PostgresTrigger,
|
||||
error: Option<&str>,
|
||||
) -> Option<()> {
|
||||
let updated = sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE
|
||||
postgres_trigger
|
||||
SET
|
||||
last_server_ping = now(),
|
||||
error = $1
|
||||
WHERE
|
||||
workspace_id = $2
|
||||
AND path = $3
|
||||
AND server_id = $4
|
||||
AND enabled IS TRUE
|
||||
RETURNING 1
|
||||
"#,
|
||||
error,
|
||||
&postgres_trigger.workspace_id,
|
||||
&postgres_trigger.path,
|
||||
*INSTANCE_NAME
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
|
||||
match updated {
|
||||
Ok(updated) => {
|
||||
if updated.flatten().is_none() {
|
||||
// allow faster restart of database trigger
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE
|
||||
postgres_trigger
|
||||
SET
|
||||
last_server_ping = NULL
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
AND path = $2
|
||||
AND server_id IS NULL"#,
|
||||
&postgres_trigger.workspace_id,
|
||||
&postgres_trigger.path,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
tracing::info!(
|
||||
"Postgres trigger {} changed, disabled, or deleted, stopping...",
|
||||
postgres_trigger.path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Error updating ping of postgres trigger {}: {:?}",
|
||||
postgres_trigger.path,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
async fn loop_ping(db: &DB, postgres_trigger: &PostgresTrigger, error: Option<&str>) {
|
||||
async fn loop_ping(db: &DB, pg: &PostgresConfig, error: Option<&str>) {
|
||||
loop {
|
||||
if update_ping(db, postgres_trigger, error).await.is_none() {
|
||||
if pg.update_ping(db, error).await.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -238,78 +188,395 @@ async fn loop_ping(db: &DB, postgres_trigger: &PostgresTrigger, error: Option<&s
|
||||
}
|
||||
}
|
||||
|
||||
async fn disable_with_error(postgres_trigger: &PostgresTrigger, db: &DB, error: String) -> () {
|
||||
match sqlx::query!(
|
||||
"UPDATE postgres_trigger SET enabled = FALSE, error = $1, server_id = NULL, last_server_ping = NULL WHERE workspace_id = $2 AND path = $3",
|
||||
error,
|
||||
postgres_trigger.workspace_id,
|
||||
postgres_trigger.path,
|
||||
)
|
||||
.execute(db).await {
|
||||
Ok(_) => {
|
||||
report_critical_error(format!("Disabling postgres trigger {} because of error: {}", postgres_trigger.path, error), db.clone(), Some(&postgres_trigger.workspace_id), None).await;
|
||||
},
|
||||
Err(disable_err) => {
|
||||
report_critical_error(
|
||||
format!("Could not disable postgres trigger {} with err {}, disabling because of error {}", postgres_trigger.path, disable_err, error),
|
||||
db.clone(),
|
||||
Some(&postgres_trigger.workspace_id),
|
||||
None,
|
||||
).await;
|
||||
enum PostgresConfig {
|
||||
Trigger(PostgresTrigger),
|
||||
Capture(CaptureConfigForPostgresTrigger),
|
||||
}
|
||||
|
||||
impl PostgresTrigger {
|
||||
async fn try_to_listen_to_database_transactions(
|
||||
self,
|
||||
db: DB,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> () {
|
||||
let postgres_trigger = sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE postgres_trigger
|
||||
SET
|
||||
server_id = $1,
|
||||
last_server_ping = now(),
|
||||
error = 'Connecting...'
|
||||
WHERE
|
||||
enabled IS TRUE
|
||||
AND workspace_id = $2
|
||||
AND path = $3
|
||||
AND (last_server_ping IS NULL
|
||||
OR last_server_ping < now() - INTERVAL '15 seconds'
|
||||
)
|
||||
RETURNING true
|
||||
"#,
|
||||
*INSTANCE_NAME,
|
||||
self.workspace_id,
|
||||
self.path,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await;
|
||||
match postgres_trigger {
|
||||
Ok(has_lock) => {
|
||||
if has_lock.flatten().unwrap_or(false) {
|
||||
tracing::info!("Spawning new task to listen_to_database_transaction");
|
||||
tokio::spawn(async move {
|
||||
listen_to_transactions(
|
||||
PostgresConfig::Trigger(self),
|
||||
db.clone(),
|
||||
killpill_rx,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
} else {
|
||||
tracing::info!("Postgres trigger {} already being listened to", self.path);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Error acquiring lock for postgres trigger {}: {:?}",
|
||||
self.path,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async fn update_ping(&self, db: &DB, error: Option<&str>) -> Option<()> {
|
||||
let updated = sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE
|
||||
postgres_trigger
|
||||
SET
|
||||
last_server_ping = now(),
|
||||
error = $1
|
||||
WHERE
|
||||
workspace_id = $2
|
||||
AND path = $3
|
||||
AND server_id = $4
|
||||
AND enabled IS TRUE
|
||||
RETURNING 1
|
||||
"#,
|
||||
error,
|
||||
&self.workspace_id,
|
||||
&self.path,
|
||||
*INSTANCE_NAME
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
|
||||
match updated {
|
||||
Ok(updated) => {
|
||||
if updated.flatten().is_none() {
|
||||
// allow faster restart of database trigger
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE
|
||||
postgres_trigger
|
||||
SET
|
||||
last_server_ping = NULL
|
||||
WHERE
|
||||
workspace_id = $1
|
||||
AND path = $2
|
||||
AND server_id IS NULL"#,
|
||||
&self.workspace_id,
|
||||
&self.path,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
tracing::info!(
|
||||
"Postgres trigger {} changed, disabled, or deleted, stopping...",
|
||||
self.path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Error updating ping of postgres trigger {}: {:?}",
|
||||
self.path,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
async fn disable_with_error(&self, db: &DB, error: String) -> () {
|
||||
match sqlx::query!(
|
||||
r#"
|
||||
UPDATE
|
||||
postgres_trigger
|
||||
SET
|
||||
enabled = FALSE,
|
||||
error = $1,
|
||||
server_id = NULL,
|
||||
last_server_ping = NULL
|
||||
WHERE
|
||||
workspace_id = $2 AND
|
||||
path = $3
|
||||
"#,
|
||||
error,
|
||||
self.workspace_id,
|
||||
self.path,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
report_critical_error(
|
||||
format!(
|
||||
"Disabling postgres trigger {} because of error: {}",
|
||||
self.path, error
|
||||
),
|
||||
db.clone(),
|
||||
Some(&self.workspace_id),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(disable_err) => {
|
||||
report_critical_error(
|
||||
format!("Could not disable postgres trigger {} with err {}, disabling because of error {}", self.path, disable_err, error),
|
||||
db.clone(),
|
||||
Some(&self.workspace_id),
|
||||
None,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_authed(&self, db: &DB) -> error::Result<ApiAuthed> {
|
||||
fetch_api_authed(
|
||||
self.edited_by.clone(),
|
||||
self.email.clone(),
|
||||
&self.workspace_id,
|
||||
db,
|
||||
Some(format!("pg-{}", self.path)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
db: &DB,
|
||||
args: Option<HashMap<String, Box<RawValue>>>,
|
||||
extra: Option<HashMap<String, Box<RawValue>>>,
|
||||
) -> () {
|
||||
if let Err(err) = run_job(args, extra, db, self).await {
|
||||
report_critical_error(
|
||||
format!(
|
||||
"Failed to trigger job from postgres {}: {:?}",
|
||||
self.path, err
|
||||
),
|
||||
db.clone(),
|
||||
Some(&self.workspace_id),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_transactions(
|
||||
postgres_trigger: &PostgresTrigger,
|
||||
db: DB,
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
let start_logical_replication_streaming = async {
|
||||
let authed = fetch_api_authed(
|
||||
postgres_trigger.edited_by.clone(),
|
||||
postgres_trigger.email.clone(),
|
||||
&postgres_trigger.workspace_id,
|
||||
&db,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
struct PgInfo<'a> {
|
||||
postgres_resource_path: &'a str,
|
||||
publication_name: &'a str,
|
||||
replication_slot_name: &'a str,
|
||||
workspace_id: &'a str,
|
||||
}
|
||||
|
||||
impl PostgresConfig {
|
||||
async fn update_ping(&self, db: &DB, error: Option<&str>) -> Option<()> {
|
||||
match self {
|
||||
PostgresConfig::Trigger(trigger) => trigger.update_ping(db, error).await,
|
||||
PostgresConfig::Capture(capture) => capture.update_ping(db, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn disable_with_error(&self, db: &DB, error: String) -> () {
|
||||
match self {
|
||||
PostgresConfig::Trigger(trigger) => trigger.disable_with_error(&db, error).await,
|
||||
PostgresConfig::Capture(capture) => capture.disable_with_error(db, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn retrieve_info(&self) -> PgInfo {
|
||||
let postgres_resource_path;
|
||||
let publication_name;
|
||||
let replication_slot_name;
|
||||
let workspace_id;
|
||||
|
||||
match self {
|
||||
PostgresConfig::Trigger(trigger) => {
|
||||
postgres_resource_path = &trigger.postgres_resource_path;
|
||||
publication_name = &trigger.publication_name;
|
||||
replication_slot_name = &trigger.replication_slot_name;
|
||||
workspace_id = &trigger.workspace_id;
|
||||
}
|
||||
PostgresConfig::Capture(capture) => {
|
||||
postgres_resource_path = &capture.trigger_config.postgres_resource_path;
|
||||
workspace_id = &capture.workspace_id;
|
||||
publication_name = capture.trigger_config.publication_name.as_ref().unwrap();
|
||||
replication_slot_name = capture
|
||||
.trigger_config
|
||||
.replication_slot_name
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
PgInfo { postgres_resource_path, replication_slot_name, workspace_id, publication_name }
|
||||
}
|
||||
|
||||
async fn start_logical_replication_streaming(
|
||||
&self,
|
||||
db: &DB,
|
||||
) -> std::result::Result<(CopyBothDuplex<Bytes>, LogicalReplicationSettings), Error> {
|
||||
let PgInfo {
|
||||
publication_name,
|
||||
replication_slot_name,
|
||||
workspace_id,
|
||||
postgres_resource_path,
|
||||
} = self.retrieve_info();
|
||||
|
||||
let authed = match self {
|
||||
PostgresConfig::Trigger(trigger) => trigger.fetch_authed(db).await?,
|
||||
PostgresConfig::Capture(capture) => capture.fetch_authed(db).await?,
|
||||
};
|
||||
|
||||
let database = get_database_resource(
|
||||
authed,
|
||||
Some(UserDB::new(db.clone())),
|
||||
&db,
|
||||
&postgres_trigger.postgres_resource_path,
|
||||
&postgres_trigger.workspace_id,
|
||||
postgres_resource_path,
|
||||
workspace_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let client = PostgresSimpleClient::new(&database).await?;
|
||||
|
||||
let (logical_replication_stream, logical_replication_settings) = client
|
||||
.get_logical_replication_stream(
|
||||
&postgres_trigger.publication_name,
|
||||
&postgres_trigger.replication_slot_name,
|
||||
)
|
||||
let publication = client
|
||||
.execute_query(&format!(
|
||||
"SELECT pubname FROM pg_publication WHERE pubname = {}",
|
||||
quote_literal(&publication_name)
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok::<_, Error>((logical_replication_stream, logical_replication_settings))
|
||||
};
|
||||
if !publication.row_exist() {
|
||||
return Err(Error::Common(error::Error::BadConfig(
|
||||
ERROR_PUBLICATION_NAME_NOT_EXISTS.to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
let replication_slot = client
|
||||
.execute_query(&format!(
|
||||
"SELECT slot_name FROM pg_replication_slots WHERE slot_name = {}",
|
||||
quote_literal(&replication_slot_name)
|
||||
))
|
||||
.await?;
|
||||
|
||||
if !replication_slot.row_exist() {
|
||||
return Err(Error::Common(error::Error::BadConfig(
|
||||
ERROR_REPLICATION_SLOT_NOT_EXISTS.to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
let (logical_replication_stream, logical_replication_settings) = client
|
||||
.get_logical_replication_stream(&publication_name, &replication_slot_name)
|
||||
.await?;
|
||||
|
||||
Ok((logical_replication_stream, logical_replication_settings))
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &str {
|
||||
match self {
|
||||
PostgresConfig::Trigger(trigger) => &trigger.path,
|
||||
PostgresConfig::Capture(capture) => &capture.path,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
db: &DB,
|
||||
args: Option<HashMap<String, Box<RawValue>>>,
|
||||
extra: Option<HashMap<String, Box<RawValue>>>,
|
||||
) -> () {
|
||||
match self {
|
||||
PostgresConfig::Trigger(trigger) => trigger.handle(&db, args, extra).await,
|
||||
PostgresConfig::Capture(capture) => capture.handle(&db, args, extra).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup(&self, db: &DB) -> Result<(), Error> {
|
||||
match self {
|
||||
PostgresConfig::Trigger(_) => Ok(()),
|
||||
PostgresConfig::Capture(capture) => {
|
||||
let publication_name = capture.trigger_config.publication_name.as_ref().unwrap();
|
||||
let replication_slot_name = capture
|
||||
.trigger_config
|
||||
.replication_slot_name
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let postgres_resource_path = &capture.trigger_config.postgres_resource_path;
|
||||
let workspace_id = &capture.workspace_id;
|
||||
let authed = capture.fetch_authed(&db).await?;
|
||||
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
let mut connection = get_database_connection(
|
||||
authed.clone(),
|
||||
Some(user_db.clone()),
|
||||
&db,
|
||||
postgres_resource_path,
|
||||
workspace_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = drop_logical_replication_slot_query(replication_slot_name);
|
||||
|
||||
let _ = sqlx::query(&query).execute(&mut connection).await;
|
||||
|
||||
let query = drop_publication_query(publication_name);
|
||||
|
||||
let _ = sqlx::query(&query).execute(&mut connection).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_transactions(
|
||||
pg: PostgresConfig,
|
||||
db: DB,
|
||||
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
let _ = pg.cleanup(&db).await;
|
||||
return;
|
||||
}
|
||||
_ = loop_ping(&db, postgres_trigger, Some("Connecting...")) => {
|
||||
_ = loop_ping(&db, &pg, Some("Connecting...")) => {
|
||||
let _ = pg.cleanup(&db).await;
|
||||
return;
|
||||
}
|
||||
result = start_logical_replication_streaming => {
|
||||
result = pg.start_logical_replication_streaming(&db) => {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
let _ = pg.cleanup(&db).await;
|
||||
return;
|
||||
}
|
||||
_ = loop_ping(&db, postgres_trigger, None) => {
|
||||
_ = loop_ping(&db, &pg, None) => {
|
||||
let _ = pg.cleanup(&db).await;
|
||||
return;
|
||||
}
|
||||
_ = {
|
||||
@@ -318,15 +585,15 @@ async fn listen_to_transactions(
|
||||
Ok((logical_replication_stream, logical_replication_settings)) => {
|
||||
pin_mut!(logical_replication_stream);
|
||||
let mut relations = RelationConverter::new();
|
||||
tracing::info!("Starting to listen for postgres trigger {}", postgres_trigger.path);
|
||||
tracing::info!("Starting to listen for postgres trigger {}", pg.get_path());
|
||||
loop {
|
||||
let message = logical_replication_stream.next().await;
|
||||
|
||||
let message = match message {
|
||||
Some(message) => message,
|
||||
None => {
|
||||
tracing::error!("Stream for postgres trigger {} closed", postgres_trigger.path);
|
||||
if let None = update_ping(&db, postgres_trigger, Some("Stream closed")).await {
|
||||
tracing::error!("Stream for postgres trigger {} closed", pg.get_path());
|
||||
if let None = pg.update_ping(&db, Some("Stream closed")).await {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
@@ -336,8 +603,8 @@ async fn listen_to_transactions(
|
||||
let message = match message {
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
let err = format!("Postgres trigger named {} had an error while receiving a message : {}", &postgres_trigger.path, err.to_string());
|
||||
disable_with_error(&postgres_trigger, &db, err).await;
|
||||
let err = format!("Postgres trigger named {} had an error while receiving a message : {}", pg.get_path(), err.to_string());
|
||||
pg.disable_with_error(&db, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -345,8 +612,8 @@ async fn listen_to_transactions(
|
||||
let logical_message = match ReplicationMessage::parse(message) {
|
||||
Ok(logical_message) => logical_message,
|
||||
Err(err) => {
|
||||
let err = format!("Postgres trigger named: {} had an error while parsing message: {}", postgres_trigger.path, err.to_string());
|
||||
disable_with_error(&postgres_trigger, &db, err).await;
|
||||
let err = format!("Postgres trigger named: {} had an error while parsing message: {}", pg.get_path(), err.to_string());
|
||||
pg.disable_with_error(&db, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -362,7 +629,7 @@ async fn listen_to_transactions(
|
||||
let logical_replication_message = match x_log_data.parse(&logical_replication_settings) {
|
||||
Ok(logical_replication_message) => logical_replication_message,
|
||||
Err(err) => {
|
||||
tracing::error!("Postgres trigger named: {} had an error while trying to parse incomming stream message: {}", &postgres_trigger.path, err.to_string());
|
||||
tracing::error!("Postgres trigger named: {} had an error while trying to parse incomming stream message: {}", pg.get_path(), err.to_string());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -390,7 +657,7 @@ async fn listen_to_transactions(
|
||||
let relation = match relations.get_relation(o_id) {
|
||||
Ok(relation) => relation,
|
||||
Err(err) => {
|
||||
tracing::error!("Postgres trigger named: {}, error: {}", &postgres_trigger.path, err.to_string());
|
||||
tracing::error!("Postgres trigger named: {}, error: {}", pg.get_path(), err.to_string());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -404,7 +671,9 @@ async fn listen_to_transactions(
|
||||
"wm_trigger".to_string(),
|
||||
to_raw_value(&serde_json::json!({"kind": "postgres", })),
|
||||
)]));
|
||||
let _ = run_job(Some(database_info), extra, &db, postgres_trigger).await;
|
||||
|
||||
|
||||
let _ = pg.handle(&db, Some(database_info), extra).await;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -413,11 +682,12 @@ async fn listen_to_transactions(
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Postgres trigger error while trying to start logical replication streaming: {}", &err);
|
||||
disable_with_error(&postgres_trigger, &db, err.to_string()).await
|
||||
pg.disable_with_error(&db, err.to_string()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
} => {
|
||||
let _ = pg.cleanup(&db).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -425,55 +695,204 @@ async fn listen_to_transactions(
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_to_listen_to_database_transactions(
|
||||
pg_trigger: PostgresTrigger,
|
||||
db: DB,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
let postgres_trigger = sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE postgres_trigger
|
||||
SET
|
||||
server_id = $1,
|
||||
last_server_ping = now(),
|
||||
error = 'Connecting...'
|
||||
WHERE
|
||||
enabled IS TRUE
|
||||
AND workspace_id = $2
|
||||
AND path = $3
|
||||
AND (last_server_ping IS NULL
|
||||
OR last_server_ping < now() - INTERVAL '15 seconds'
|
||||
)
|
||||
RETURNING true
|
||||
"#,
|
||||
*INSTANCE_NAME,
|
||||
pg_trigger.workspace_id,
|
||||
pg_trigger.path,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await;
|
||||
match postgres_trigger {
|
||||
Ok(has_lock) => {
|
||||
if has_lock.flatten().unwrap_or(false) {
|
||||
tracing::info!("Spawning new task to listen_to_database_transaction");
|
||||
tokio::spawn(async move {
|
||||
listen_to_transactions(&pg_trigger, db.clone(), killpill_rx).await;
|
||||
});
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Postgres trigger {} already being listened to",
|
||||
pg_trigger.path
|
||||
#[derive(Deserialize)]
|
||||
struct CaptureConfigForPostgresTrigger {
|
||||
trigger_config: SqlxJson<PostgresTriggerConfig>,
|
||||
path: String,
|
||||
is_flow: bool,
|
||||
workspace_id: String,
|
||||
owner: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl CaptureConfigForPostgresTrigger {
|
||||
async fn try_to_listen_to_database_transactions(
|
||||
self,
|
||||
db: DB,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) -> () {
|
||||
match sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE
|
||||
capture_config
|
||||
SET
|
||||
server_id = $1,
|
||||
last_server_ping = now(),
|
||||
error = 'Connecting...'
|
||||
WHERE
|
||||
last_client_ping > NOW() - INTERVAL '10 seconds' AND
|
||||
workspace_id = $2 AND
|
||||
path = $3 AND
|
||||
is_flow = $4 AND
|
||||
trigger_kind = 'postgres' AND
|
||||
(last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')
|
||||
RETURNING true
|
||||
"#,
|
||||
*INSTANCE_NAME,
|
||||
self.workspace_id,
|
||||
self.path,
|
||||
self.is_flow,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
{
|
||||
Ok(has_lock) => {
|
||||
if has_lock.flatten().unwrap_or(false) {
|
||||
tokio::spawn(listen_to_transactions(
|
||||
PostgresConfig::Capture(self),
|
||||
db,
|
||||
killpill_rx,
|
||||
));
|
||||
} else {
|
||||
tracing::info!("Postgres {} already being listened to", self.path);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Error acquiring lock for capture postgres {}: {:?}",
|
||||
self.path,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async fn update_ping(&self, db: &DB, error: Option<&str>) -> Option<()> {
|
||||
match sqlx::query_scalar!(
|
||||
r#"
|
||||
UPDATE
|
||||
capture_config
|
||||
SET
|
||||
last_server_ping = now(),
|
||||
error = $1
|
||||
WHERE
|
||||
workspace_id = $2 AND
|
||||
path = $3 AND
|
||||
is_flow = $4 AND
|
||||
trigger_kind = 'postgres' AND
|
||||
server_id = $5 AND
|
||||
last_client_ping > NOW() - INTERVAL '10 seconds'
|
||||
RETURNING 1
|
||||
"#,
|
||||
error,
|
||||
self.workspace_id,
|
||||
self.path,
|
||||
self.is_flow,
|
||||
*INSTANCE_NAME
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
{
|
||||
Ok(updated) => {
|
||||
if updated.flatten().is_none() {
|
||||
// allow faster restart of postgres capture
|
||||
sqlx::query!(
|
||||
r#"UPDATE
|
||||
capture_config
|
||||
SET
|
||||
last_server_ping = NULL
|
||||
WHERE
|
||||
workspace_id = $1 AND
|
||||
path = $2 AND
|
||||
is_flow = $3 AND
|
||||
trigger_kind = 'postgres' AND
|
||||
server_id IS NULL
|
||||
"#,
|
||||
self.workspace_id,
|
||||
self.path,
|
||||
self.is_flow,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.ok();
|
||||
tracing::info!(
|
||||
"Postgres capture {} changed, disabled, or deleted, stopping...",
|
||||
self.path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Error updating ping of capture postgres {}: {:?}",
|
||||
self.path,
|
||||
err
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
async fn fetch_authed(&self, db: &DB) -> error::Result<ApiAuthed> {
|
||||
fetch_api_authed(
|
||||
self.owner.clone(),
|
||||
self.email.clone(),
|
||||
&self.workspace_id,
|
||||
db,
|
||||
Some(format!("postgres-{}", self.get_trigger_path())),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_trigger_path(&self) -> String {
|
||||
format!(
|
||||
"{}-{}",
|
||||
if self.is_flow { "flow" } else { "script" },
|
||||
self.path
|
||||
)
|
||||
}
|
||||
|
||||
async fn disable_with_error(&self, db: &DB, error: String) -> () {
|
||||
if let Err(err) = sqlx::query!(
|
||||
r#"
|
||||
UPDATE
|
||||
capture_config
|
||||
SET
|
||||
error = $1,
|
||||
server_id = NULL,
|
||||
last_server_ping = NULL
|
||||
WHERE
|
||||
workspace_id = $2 AND
|
||||
path = $3 AND
|
||||
is_flow = $4 AND
|
||||
trigger_kind = 'postgres'
|
||||
"#,
|
||||
error,
|
||||
self.workspace_id,
|
||||
self.path,
|
||||
self.is_flow,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Could not disable postgres capture {} ({}) with err {}, disabling because of error {}", self.path, self.workspace_id, err, error);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Error acquiring lock for postgres trigger {}: {:?}",
|
||||
pg_trigger.path,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
db: &DB,
|
||||
args: Option<HashMap<String, Box<RawValue>>>,
|
||||
extra: Option<HashMap<String, Box<RawValue>>>,
|
||||
) -> () {
|
||||
let args = PushArgsOwned { args: args.unwrap_or_default(), extra };
|
||||
let extra = args.extra.as_ref().map(to_raw_value);
|
||||
if let Err(err) = insert_capture_payload(
|
||||
db,
|
||||
&self.workspace_id,
|
||||
&self.path,
|
||||
self.is_flow,
|
||||
&TriggerKind::Postgres,
|
||||
args,
|
||||
extra,
|
||||
&self.owner,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error inserting capture payload: {:?}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_unlistened_database_events(
|
||||
@@ -515,18 +934,51 @@ async fn listen_to_unlistened_database_events(
|
||||
Ok(mut triggers) => {
|
||||
triggers.shuffle(&mut rand::rng());
|
||||
for trigger in triggers {
|
||||
try_to_listen_to_database_transactions(
|
||||
trigger,
|
||||
db.clone(),
|
||||
killpill_rx.resubscribe(),
|
||||
)
|
||||
.await;
|
||||
trigger
|
||||
.try_to_listen_to_database_transactions(db.clone(), killpill_rx.resubscribe())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error fetching postgres triggers: {:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
let postgres_triggers_capture = sqlx::query_as!(
|
||||
CaptureConfigForPostgresTrigger,
|
||||
r#"
|
||||
SELECT
|
||||
path,
|
||||
is_flow,
|
||||
workspace_id,
|
||||
owner,
|
||||
email,
|
||||
trigger_config as "trigger_config!: _"
|
||||
FROM
|
||||
capture_config
|
||||
WHERE
|
||||
trigger_kind = 'postgres' AND
|
||||
last_client_ping > NOW() - INTERVAL '10 seconds' AND
|
||||
trigger_config IS NOT NULL AND
|
||||
(last_server_ping IS NULL OR last_server_ping < now() - interval '15 seconds')
|
||||
"#
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await;
|
||||
|
||||
match postgres_triggers_capture {
|
||||
Ok(mut captures) => {
|
||||
captures.shuffle(&mut rand::rng());
|
||||
for capture in captures {
|
||||
capture
|
||||
.try_to_listen_to_database_transactions(db.clone(), killpill_rx.resubscribe())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error fetching captures postgres triggers: {:?}", err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn start_database(db: DB, mut killpill_rx: tokio::sync::broadcast::Receiver<()>) {
|
||||
|
||||
@@ -336,8 +336,8 @@ pub(crate) async fn tarball_workspace(
|
||||
{
|
||||
let scripts = sqlx::query_as::<_, Script>(
|
||||
"SELECT * FROM script as o WHERE workspace_id = $1 AND archived = false
|
||||
AND created_at = (select max(created_at) from script where path = o.path AND \
|
||||
workspace_id = $1)",
|
||||
AND created_at = (select max(created_at) from script where path = o.path AND \
|
||||
workspace_id = $1)",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -413,12 +413,12 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
if !skip_resources.unwrap_or(false) {
|
||||
let resources = sqlx::query_as!(
|
||||
Resource,
|
||||
"SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
Resource,
|
||||
"SELECT * FROM resource WHERE workspace_id = $1 AND resource_type != 'state' AND resource_type != 'cache'",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for resource in resources {
|
||||
let resource_str = &to_string_without_metadata(&resource, false, None).unwrap();
|
||||
@@ -450,14 +450,14 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
{
|
||||
let flows = sqlx::query_as::<_, Flow>(
|
||||
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.draft_only, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
|
||||
FROM flow
|
||||
LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.workspace_id = $1 AND flow.archived = false",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
"SELECT flow.workspace_id, flow.path, flow.summary, flow.description, flow.archived, flow.extra_perms, flow.draft_only, flow.dedicated_worker, flow.tag, flow.ws_error_handler_muted, flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, flow_version.schema, flow_version.value, flow_version.created_at as edited_at, flow_version.created_by as edited_by
|
||||
FROM flow
|
||||
LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.workspace_id = $1 AND flow.archived = false",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for flow in flows {
|
||||
let flow_str = &to_string_without_metadata(&flow, false, None).unwrap();
|
||||
@@ -469,14 +469,14 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
if !skip_variables.unwrap_or(false) {
|
||||
let variables =
|
||||
sqlx::query_as::<_, ExportableListableVariable>(if !skip_secrets.unwrap_or(false) {
|
||||
"SELECT * FROM variable WHERE workspace_id = $1 AND expires_at IS NULL"
|
||||
} else {
|
||||
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false AND expires_at IS NULL"
|
||||
})
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query_as::<_, ExportableListableVariable>(if !skip_secrets.unwrap_or(false) {
|
||||
"SELECT * FROM variable WHERE workspace_id = $1 AND expires_at IS NULL"
|
||||
} else {
|
||||
"SELECT * FROM variable WHERE workspace_id = $1 AND is_secret = false AND expires_at IS NULL"
|
||||
})
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let mc = build_crypt(&db, &w_id).await?;
|
||||
|
||||
@@ -496,14 +496,14 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
{
|
||||
let apps = sqlx::query_as::<_, AppWithLastVersion>(
|
||||
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
|
||||
app.extra_perms, app_version.value,
|
||||
app_version.created_at, app_version.created_by from app, app_version
|
||||
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
|
||||
app.extra_perms, app_version.value,
|
||||
app_version.created_at, app_version.created_by from app, app_version
|
||||
WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for app in apps {
|
||||
let app_str = &to_string_without_metadata(&app, false, None).unwrap();
|
||||
@@ -516,7 +516,7 @@ pub(crate) async fn tarball_workspace(
|
||||
if include_schedules.unwrap_or(false) {
|
||||
let schedules = sqlx::query_as::<_, Schedule>(
|
||||
"SELECT * FROM schedule
|
||||
WHERE workspace_id = $1",
|
||||
WHERE workspace_id = $1",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -534,13 +534,13 @@ pub(crate) async fn tarball_workspace(
|
||||
#[cfg(feature = "http_trigger")]
|
||||
{
|
||||
let http_triggers = sqlx::query_as!(
|
||||
crate::http_triggers::HttpTrigger,
|
||||
"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
crate::http_triggers::HttpTrigger,
|
||||
"SELECT workspace_id, path, route_path, route_path_key, script_path, is_flow, edited_by, edited_at, email, extra_perms, is_async, requires_auth, http_method as \"http_method: _\", static_asset_config as \"static_asset_config: _\", is_static_website FROM http_trigger
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for trigger in http_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
@@ -553,13 +553,13 @@ pub(crate) async fn tarball_workspace(
|
||||
#[cfg(feature = "websocket")]
|
||||
{
|
||||
let websocket_triggers = sqlx::query_as!(
|
||||
crate::websocket_triggers::WebsocketTrigger,
|
||||
"SELECT workspace_id, path, url, script_path, is_flow, edited_by, email, edited_at, server_id, last_server_ping, extra_perms, error, enabled, filters as \"filters: _\", initial_messages as \"initial_messages: _\", url_runnable_args as \"url_runnable_args: _\", can_return_message FROM websocket_trigger
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
crate::websocket_triggers::WebsocketTrigger,
|
||||
"SELECT workspace_id, path, url, script_path, is_flow, edited_by, email, edited_at, server_id, last_server_ping, extra_perms, error, enabled, filters as \"filters: _\", initial_messages as \"initial_messages: _\", url_runnable_args as \"url_runnable_args: _\", can_return_message FROM websocket_trigger
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for trigger in websocket_triggers {
|
||||
let trigger_str = &to_string_without_metadata(&trigger, false, None).unwrap();
|
||||
@@ -577,7 +577,7 @@ pub(crate) async fn tarball_workspace(
|
||||
let kafka_triggers = sqlx::query_as!(
|
||||
crate::kafka_triggers_ee::KafkaTrigger,
|
||||
"SELECT * FROM kafka_trigger
|
||||
WHERE workspace_id = $1",
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -599,7 +599,7 @@ pub(crate) async fn tarball_workspace(
|
||||
let nats_triggers = sqlx::query_as!(
|
||||
crate::nats_triggers_ee::NatsTrigger,
|
||||
"SELECT * FROM nats_trigger
|
||||
WHERE workspace_id = $1",
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -619,7 +619,7 @@ pub(crate) async fn tarball_workspace(
|
||||
let postgres_triggers = sqlx::query_as!(
|
||||
crate::postgres_triggers::PostgresTrigger,
|
||||
"SELECT * FROM postgres_trigger
|
||||
WHERE workspace_id = $1",
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -640,7 +640,7 @@ pub(crate) async fn tarball_workspace(
|
||||
if include_users.unwrap_or(false) {
|
||||
let users = sqlx::query!(
|
||||
"SELECT * FROM usr
|
||||
WHERE workspace_id = $1",
|
||||
WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
@@ -668,16 +668,16 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
if include_groups.unwrap_or(false) {
|
||||
let groups = sqlx::query!(
|
||||
r#"SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members
|
||||
FROM usr u
|
||||
JOIN usr_to_group u2g ON u2g.usr = u.username AND u2g.workspace_id = u.workspace_id
|
||||
RIGHT JOIN group_ g_ ON g_.workspace_id = u.workspace_id AND g_.name = u2g.group_
|
||||
WHERE g_.workspace_id = $1 AND g_.name != 'all'
|
||||
GROUP BY g_.workspace_id, name, summary, extra_perms"#,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
r#"SELECT g_.workspace_id, name, summary, extra_perms, array_agg(u2g.usr) filter (where u2g.usr is not null) as members
|
||||
FROM usr u
|
||||
JOIN usr_to_group u2g ON u2g.usr = u.username AND u2g.workspace_id = u.workspace_id
|
||||
RIGHT JOIN group_ g_ ON g_.workspace_id = u.workspace_id AND g_.name = u2g.group_
|
||||
WHERE g_.workspace_id = $1 AND g_.name != 'all'
|
||||
GROUP BY g_.workspace_id, name, summary, extra_perms"#,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
for group in groups {
|
||||
let extra_perms: HashMap<String, bool> = serde_json::from_value(group.extra_perms)
|
||||
@@ -728,36 +728,36 @@ pub(crate) async fn tarball_workspace(
|
||||
|
||||
if include_settings.unwrap_or(false) {
|
||||
let settings = sqlx::query_as!(
|
||||
SimplifiedSettings,
|
||||
r#"SELECT
|
||||
-- slack_team_id,
|
||||
-- slack_name,
|
||||
-- slack_command_script,
|
||||
-- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,
|
||||
auto_invite_domain IS NOT NULL AS "auto_invite_enabled!",
|
||||
CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS "auto_invite_as!",
|
||||
CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS "auto_invite_mode!",
|
||||
webhook,
|
||||
deploy_to,
|
||||
error_handler,
|
||||
ai_resource,
|
||||
ai_models,
|
||||
code_completion_model,
|
||||
error_handler_extra_args,
|
||||
error_handler_muted_on_cancel,
|
||||
large_file_storage,
|
||||
git_sync,
|
||||
default_app,
|
||||
default_scripts,
|
||||
workspace.name,
|
||||
mute_critical_alerts,
|
||||
color,
|
||||
operator_settings
|
||||
FROM workspace_settings
|
||||
LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id
|
||||
WHERE workspace_id = $1"#,
|
||||
&w_id
|
||||
).fetch_one(&mut *tx).await?;
|
||||
SimplifiedSettings,
|
||||
r#"SELECT
|
||||
-- slack_team_id,
|
||||
-- slack_name,
|
||||
-- slack_command_script,
|
||||
-- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,
|
||||
auto_invite_domain IS NOT NULL AS "auto_invite_enabled!",
|
||||
CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS "auto_invite_as!",
|
||||
CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS "auto_invite_mode!",
|
||||
webhook,
|
||||
deploy_to,
|
||||
error_handler,
|
||||
ai_resource,
|
||||
ai_models,
|
||||
code_completion_model,
|
||||
error_handler_extra_args,
|
||||
error_handler_muted_on_cancel,
|
||||
large_file_storage,
|
||||
git_sync,
|
||||
default_app,
|
||||
default_scripts,
|
||||
workspace.name,
|
||||
mute_critical_alerts,
|
||||
color,
|
||||
operator_settings
|
||||
FROM workspace_settings
|
||||
LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id
|
||||
WHERE workspace_id = $1"#,
|
||||
&w_id
|
||||
).fetch_one(&mut *tx).await?;
|
||||
|
||||
let settings_str = serde_json::to_value(settings)
|
||||
.map(|v| serde_json::to_string_pretty(&v).ok())
|
||||
|
||||
@@ -104,10 +104,9 @@ mount {
|
||||
iface_no_lo: true
|
||||
|
||||
mount {
|
||||
src: "{CACHE_DIR}"
|
||||
dst: "/tmp/.cache/csharp"
|
||||
src: "{CACHE_DIR}/{CACHE_HASH}"
|
||||
dst: "/tmp/.cache/csharp/{CACHE_HASH}"
|
||||
is_bind: true
|
||||
rw: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
|
||||
@@ -97,10 +97,9 @@ mount {
|
||||
iface_no_lo: true
|
||||
|
||||
mount {
|
||||
src: "{CACHE_DIR}"
|
||||
dst: "/tmp/.cache/rust"
|
||||
src: "{CACHE_DIR}/{CACHE_HASH}"
|
||||
dst: "/tmp/.cache/rust/{CACHE_HASH}"
|
||||
is_bind: true
|
||||
rw: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
|
||||
@@ -543,6 +543,7 @@ pub async fn handle_csharp_job(
|
||||
&NSJAIL_CONFIG_RUN_CSHARP_CONTENT
|
||||
.replace("{JOB_DIR}", job_dir)
|
||||
.replace("{CACHE_DIR}", CSHARP_CACHE_DIR)
|
||||
.replace("{CACHE_HASH}", &hash)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
.replace("{SHARED_MOUNT}", shared_mount),
|
||||
)?;
|
||||
|
||||
@@ -353,6 +353,7 @@ pub async fn handle_rust_job(
|
||||
&NSJAIL_CONFIG_RUN_RUST_CONTENT
|
||||
.replace("{JOB_DIR}", job_dir)
|
||||
.replace("{CACHE_DIR}", RUST_CACHE_DIR)
|
||||
.replace("{CACHE_HASH}", &hash)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
.replace("{SHARED_MOUNT}", shared_mount),
|
||||
)?;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.458.4";
|
||||
export const VERSION = "v1.460.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.458.4";
|
||||
export const VERSION = "1.460.1";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.458.4",
|
||||
"version": "1.460.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.458.4",
|
||||
"version": "1.460.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.458.4",
|
||||
"version": "1.460.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
{#if !headless}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<span class="{primary ? 'text-primary' : 'text-secondary'} text-sm leading-6">{label}</span>
|
||||
{#if required}
|
||||
<Required required={true} />
|
||||
{/if}
|
||||
<span class="{primary ? 'text-primary' : 'text-secondary'} text-sm leading-6"
|
||||
>{label}
|
||||
{#if required}
|
||||
<Required required={true} />
|
||||
{/if}
|
||||
</span>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
<slot name="header" />
|
||||
{#if tooltip}
|
||||
<Tooltip {documentationLink}>{tooltip}</Tooltip>
|
||||
{:else if $$slots.tooltip}
|
||||
<slot name="tooltip" />
|
||||
{/if}
|
||||
{#if eeOnly}
|
||||
{#if !$enterpriseLicense}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
| 'websocket_trigger'
|
||||
| 'kafka_trigger'
|
||||
| 'nats_trigger'
|
||||
| 'postgres_trigger'
|
||||
let kind: Kind
|
||||
|
||||
let path: string = ''
|
||||
|
||||
@@ -89,75 +89,79 @@
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
<AlwaysMountedModal {css} title={resolvedConfig.modalTitle ?? ''} bind:this={modal}>
|
||||
<div class="flex flex-col gap-2 px-4 w-full pt-2">
|
||||
<RunnableWrapper
|
||||
bind:this={runnableWrapper}
|
||||
bind:loading
|
||||
{recomputeIds}
|
||||
{render}
|
||||
bind:runnableComponent
|
||||
{componentInput}
|
||||
{id}
|
||||
{extraQueryParams}
|
||||
autoRefresh={false}
|
||||
forceSchemaDisplay={true}
|
||||
runnableClass="!block"
|
||||
{outputs}
|
||||
doOnSuccess={resolvedConfig.onSuccess}
|
||||
doOnError={resolvedConfig.onError}
|
||||
{errorHandledByComponent}
|
||||
>
|
||||
{#if noInputs}
|
||||
<div class="text-secondary italic text-sm my-4">
|
||||
Run forms are associated with a runnable that has user inputs.
|
||||
<br />
|
||||
Once a script or flow is chosen, set some <strong>Runnable Inputs</strong> to
|
||||
<strong>
|
||||
User Input
|
||||
<User size={20} class="rounded-sm bg-gray-200 p-1 ml-0.5" />
|
||||
</strong>
|
||||
{#if render}
|
||||
<AlwaysMountedModal {css} title={resolvedConfig.modalTitle ?? ''} bind:this={modal}>
|
||||
<div class="flex flex-col gap-2 px-4 w-full pt-2">
|
||||
<RunnableWrapper
|
||||
bind:this={runnableWrapper}
|
||||
bind:loading
|
||||
{recomputeIds}
|
||||
{render}
|
||||
bind:runnableComponent
|
||||
{componentInput}
|
||||
{id}
|
||||
{extraQueryParams}
|
||||
autoRefresh={false}
|
||||
forceSchemaDisplay={true}
|
||||
runnableClass="!block"
|
||||
{outputs}
|
||||
doOnSuccess={resolvedConfig.onSuccess}
|
||||
doOnError={resolvedConfig.onError}
|
||||
{errorHandledByComponent}
|
||||
>
|
||||
{#if noInputs}
|
||||
<div class="text-secondary italic text-sm my-4">
|
||||
Run forms are associated with a runnable that has user inputs.
|
||||
<br />
|
||||
Once a script or flow is chosen, set some <strong>Runnable Inputs</strong> to
|
||||
<strong>
|
||||
User Input
|
||||
<User size={20} class="rounded-sm bg-gray-200 p-1 ml-0.5" />
|
||||
</strong>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-3 p-2">
|
||||
<Button
|
||||
{loading}
|
||||
btnClasses="my-1"
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
on:click={async () => {
|
||||
if (!runnableComponent) {
|
||||
runnableWrapper?.handleSideEffect(true)
|
||||
} else {
|
||||
await runnableComponent?.runComponent()
|
||||
}
|
||||
modal?.close()
|
||||
}}
|
||||
size="xs"
|
||||
color="dark"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex justify-end gap-3 p-2">
|
||||
<Button
|
||||
{loading}
|
||||
btnClasses="my-1"
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
on:click={async () => {
|
||||
if (!runnableComponent) {
|
||||
runnableWrapper?.handleSideEffect(true)
|
||||
} else {
|
||||
await runnableComponent?.runComponent()
|
||||
}
|
||||
modal?.close()
|
||||
}}
|
||||
size="xs"
|
||||
color="dark"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
</AlwaysMountedModal>
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
</AlwaysMountedModal>
|
||||
|
||||
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
|
||||
{#if errorsMessage}
|
||||
<div class="text-red-500 text-xs">{errorsMessage}</div>
|
||||
{/if}
|
||||
<Button
|
||||
disabled={resolvedConfig.disabled ?? false}
|
||||
size={resolvedConfig.size ?? 'md'}
|
||||
color={resolvedConfig.color}
|
||||
btnClasses={twMerge(css?.button?.class, 'wm-button', 'wm-modal-form-button')}
|
||||
style={css?.button?.style ?? ''}
|
||||
on:click={(e) => {
|
||||
modal?.open()
|
||||
}}
|
||||
>
|
||||
{resolvedConfig.label}
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
|
||||
{#if errorsMessage}
|
||||
<div class="text-red-500 text-xs">{errorsMessage}</div>
|
||||
{/if}
|
||||
<Button
|
||||
disabled={resolvedConfig.disabled ?? false}
|
||||
size={resolvedConfig.size ?? 'md'}
|
||||
color={resolvedConfig.color}
|
||||
btnClasses={twMerge(css?.button?.class, 'wm-button', 'wm-modal-form-button')}
|
||||
style={css?.button?.style ?? ''}
|
||||
on:click={(e) => {
|
||||
modal?.open()
|
||||
}}
|
||||
>
|
||||
{resolvedConfig.label}
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
{:else}
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -18,17 +18,21 @@
|
||||
export let initializing: boolean | undefined
|
||||
export let componentContainerHeight: number
|
||||
|
||||
type AccordionListValue = { header: string; [key: string]: any };
|
||||
type AccordionListValue = { header: string; [key: string]: any }
|
||||
|
||||
type InternalAccordionListInput = AppInput & {
|
||||
value: AccordionListValue[];
|
||||
};
|
||||
value: AccordionListValue[]
|
||||
}
|
||||
|
||||
$: accordionInput = componentInput as InternalAccordionListInput;
|
||||
$: accordionInput = componentInput as InternalAccordionListInput
|
||||
|
||||
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let everRender = render
|
||||
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
let activeIndex: number = 0
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
@@ -61,7 +65,6 @@
|
||||
activeIndex = activeIndex === index ? -1 : index
|
||||
outputs.activeIndex.set(activeIndex)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#each Object.keys(css ?? {}) as key (key)}
|
||||
@@ -85,27 +88,27 @@
|
||||
bind:initializing
|
||||
bind:result
|
||||
>
|
||||
<div class="w-full flex flex-col overflow-auto max-h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
{#if Array.isArray(result) && result.length > 0}
|
||||
{#each result ?? [] as value, index}
|
||||
<div class="border-b">
|
||||
<button
|
||||
on:pointerdown|stopPropagation
|
||||
on:click={() => toggleAccordion(index)}
|
||||
class={twMerge(
|
||||
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
|
||||
'wm-tabs-alltabs',
|
||||
activeIndex === index
|
||||
? twMerge('bg-surface text-primary ', 'wm-tabs-selectedTab')
|
||||
: 'text-secondary'
|
||||
)}
|
||||
>
|
||||
<span class="mr-2 w-8 font-mono">{activeIndex === index ? '-' : '+'}</span>
|
||||
{result[index]?.header || `Header ${index}`}
|
||||
</button>
|
||||
{#if activeIndex === index}
|
||||
<div class="p-2 overflow-auto w-full">
|
||||
{#if everRender}
|
||||
<div class="w-full flex flex-col overflow-auto max-h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
{#if Array.isArray(result) && result.length > 0}
|
||||
{#each result ?? [] as value, index}
|
||||
<div class="border-b">
|
||||
<button
|
||||
on:pointerdown|stopPropagation
|
||||
on:click={() => toggleAccordion(index)}
|
||||
class={twMerge(
|
||||
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
|
||||
'wm-tabs-alltabs',
|
||||
activeIndex === index
|
||||
? twMerge('bg-surface text-primary ', 'wm-tabs-selectedTab')
|
||||
: 'text-secondary'
|
||||
)}
|
||||
>
|
||||
<span class="mr-2 w-8 font-mono">{activeIndex === index ? '-' : '+'}</span>
|
||||
{result[index]?.header || `Header ${index}`}
|
||||
</button>
|
||||
<div class="overflow-auto w-full">
|
||||
<ListWrapper
|
||||
onSet={(id, value) => {
|
||||
if (!inputs[id]) {
|
||||
@@ -133,8 +136,8 @@
|
||||
>
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render}
|
||||
class={twMerge(css?.container?.class, 'wm-accordion')}
|
||||
visible={render && index === activeIndex}
|
||||
class={twMerge(css?.container?.class, 'wm-accordion p-2')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight -
|
||||
@@ -148,17 +151,21 @@
|
||||
/>
|
||||
</ListWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{#if !Array.isArray(result)}
|
||||
<div class="text-center text-tertiary">Input data is not an array</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{#if !Array.isArray(result)}
|
||||
<div class="text-center text-tertiary">Input data is not an array</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if $app.subgrids}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
loading: false,
|
||||
@@ -90,117 +93,123 @@
|
||||
bind:initializing
|
||||
bind:result
|
||||
>
|
||||
<div class="w-full flex flex-wrap overflow-auto divide-y max-h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
{#if Array.isArray(result) && result.length > 0}
|
||||
{#key result}
|
||||
<Carousel
|
||||
particlesToShow={1}
|
||||
particlesToScroll={1}
|
||||
autoplay={false}
|
||||
autoplayProgressVisible={false}
|
||||
timingFunction={resolvedConfig.timingFunction}
|
||||
dots={true}
|
||||
arrows={true}
|
||||
swiping={false}
|
||||
bind:this={carousel}
|
||||
on:pageChange={(event) => {
|
||||
currentPageIndex = event.detail
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: event.detail
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div slot="prev" class="h-full flex justify-center flex-col p-2">
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
on:click={() => {
|
||||
const pagesCount = result?.length ?? 0
|
||||
{#if everRender}
|
||||
<div class="w-full flex flex-wrap overflow-auto divide-y max-h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
{#if Array.isArray(result) && result.length > 0}
|
||||
{#key result}
|
||||
<Carousel
|
||||
particlesToShow={1}
|
||||
particlesToScroll={1}
|
||||
autoplay={false}
|
||||
autoplayProgressVisible={false}
|
||||
timingFunction={resolvedConfig.timingFunction}
|
||||
dots={true}
|
||||
arrows={true}
|
||||
swiping={false}
|
||||
bind:this={carousel}
|
||||
on:pageChange={(event) => {
|
||||
currentPageIndex = event.detail
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: event.detail
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div slot="prev" class="h-full flex justify-center flex-col p-2">
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
on:click={() => {
|
||||
const pagesCount = result?.length ?? 0
|
||||
|
||||
if (currentPageIndex > 0) {
|
||||
carousel.goTo(currentPageIndex - 1)
|
||||
} else {
|
||||
carousel.goTo(pagesCount - 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowLeftCircle size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="next" class="h-full flex justify-center flex-col p-2">
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
on:click={() => {
|
||||
const pagesCount = result?.length ?? 0
|
||||
if (currentPageIndex < pagesCount - 1) {
|
||||
carousel.goTo(currentPageIndex + 1)
|
||||
} else {
|
||||
carousel.goTo(0)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowRightCircle size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#each result ?? [] as value, index}
|
||||
<div class="overflow-auto w-full">
|
||||
<ListWrapper
|
||||
onSet={(id, value) => {
|
||||
if (!inputs[id]) {
|
||||
inputs[id] = { [index]: value }
|
||||
} else {
|
||||
inputs[id] = { ...inputs[id], [index]: value }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
onRemove={(id) => {
|
||||
if (inputs?.[id] == undefined) {
|
||||
return
|
||||
}
|
||||
if (index == 0) {
|
||||
delete inputs[id]
|
||||
inputs = { ...inputs }
|
||||
} else {
|
||||
delete inputs[id][index]
|
||||
inputs[id] = { ...inputs[id] }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
{value}
|
||||
{index}
|
||||
>
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render}
|
||||
class={twMerge(css?.container?.class, 'wm-carousel')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight - 40}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
if (currentPageIndex > 0) {
|
||||
carousel.goTo(currentPageIndex - 1)
|
||||
} else {
|
||||
carousel.goTo(pagesCount - 1)
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
</ListWrapper>
|
||||
>
|
||||
<ArrowLeftCircle size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Carousel>
|
||||
{/key}
|
||||
{:else}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{#if !Array.isArray(result)}
|
||||
<div class="text-center text-tertiary">Input data is not an array</div>
|
||||
<div slot="next" class="h-full flex justify-center flex-col p-2">
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
on:click={() => {
|
||||
const pagesCount = result?.length ?? 0
|
||||
if (currentPageIndex < pagesCount - 1) {
|
||||
carousel.goTo(currentPageIndex + 1)
|
||||
} else {
|
||||
carousel.goTo(0)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowRightCircle size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#each result ?? [] as value, index}
|
||||
<div class="overflow-auto w-full">
|
||||
<ListWrapper
|
||||
onSet={(id, value) => {
|
||||
if (!inputs[id]) {
|
||||
inputs[id] = { [index]: value }
|
||||
} else {
|
||||
inputs[id] = { ...inputs[id], [index]: value }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
onRemove={(id) => {
|
||||
if (inputs?.[id] == undefined) {
|
||||
return
|
||||
}
|
||||
if (index == 0) {
|
||||
delete inputs[id]
|
||||
inputs = { ...inputs }
|
||||
} else {
|
||||
delete inputs[id][index]
|
||||
inputs[id] = { ...inputs[id] }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
{value}
|
||||
{index}
|
||||
>
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render}
|
||||
class={twMerge(css?.container?.class, 'wm-carousel')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight - 40}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
</ListWrapper>
|
||||
</div>
|
||||
{/each}
|
||||
</Carousel>
|
||||
{/key}
|
||||
{:else}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{#if !Array.isArray(result)}
|
||||
<div class="text-center text-tertiary">Input data is not an array</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if $app.subgrids}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
|
||||
@@ -119,4 +119,6 @@
|
||||
<div id={divId} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -35,25 +35,29 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<div
|
||||
on:pointerdown={(e) => {
|
||||
e?.preventDefault()
|
||||
}}
|
||||
class="h-full w-full"
|
||||
>
|
||||
<RunnableWrapper
|
||||
{outputs}
|
||||
{render}
|
||||
autoRefresh
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
{#if render}
|
||||
<div
|
||||
on:pointerdown={(e) => {
|
||||
e?.preventDefault()
|
||||
}}
|
||||
class="h-full w-full"
|
||||
>
|
||||
<div class="w-full h-full overflow-auto">
|
||||
{#key result}
|
||||
{@html result}
|
||||
{/key}
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
<RunnableWrapper
|
||||
{outputs}
|
||||
{render}
|
||||
autoRefresh
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
>
|
||||
<div class="w-full h-full overflow-auto">
|
||||
{#key result}
|
||||
{@html result}
|
||||
{/key}
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<RunnableWrapper {outputs} render={false} autoRefresh {componentInput} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -62,32 +62,36 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<div
|
||||
on:pointerdown={(e) => {
|
||||
e?.preventDefault()
|
||||
}}
|
||||
class={classNames(
|
||||
'h-full w-full overflow-y-auto prose max-w-full',
|
||||
resolvedConfig?.size ? proseMapping[resolvedConfig.size] : '',
|
||||
css?.container?.class,
|
||||
' dark:prose-invert',
|
||||
'wm-markdown'
|
||||
)}
|
||||
style={css?.container?.style}
|
||||
>
|
||||
<RunnableWrapper
|
||||
{outputs}
|
||||
{render}
|
||||
autoRefresh
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
{#if render}
|
||||
<div
|
||||
on:pointerdown={(e) => {
|
||||
e?.preventDefault()
|
||||
}}
|
||||
class={classNames(
|
||||
'h-full w-full overflow-y-auto prose max-w-full',
|
||||
resolvedConfig?.size ? proseMapping[resolvedConfig.size] : '',
|
||||
css?.container?.class,
|
||||
' dark:prose-invert',
|
||||
'wm-markdown'
|
||||
)}
|
||||
style={css?.container?.style}
|
||||
>
|
||||
{#if result}
|
||||
{#key result}
|
||||
<Markdown md={result} plugins={[gfmPlugin()]} />
|
||||
{/key}
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
<RunnableWrapper
|
||||
{outputs}
|
||||
render={true}
|
||||
autoRefresh
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
>
|
||||
{#if result}
|
||||
{#key result}
|
||||
<Markdown md={result} plugins={[gfmPlugin()]} />
|
||||
{/key}
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<RunnableWrapper {outputs} render={false} autoRefresh {componentInput} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex flex-wrap gap-2 pb-0.5 w-full {$mode === 'dnd' &&
|
||||
class="flex flex-wrap gap-0.5 pb-0.5 w-full {$mode === 'dnd' &&
|
||||
(componentInput?.type == 'template' || componentInput?.type == 'templatev2')
|
||||
? 'cursor-text'
|
||||
: 'overflow-auto'}"
|
||||
@@ -225,7 +225,7 @@
|
||||
<svelte:element
|
||||
this={component}
|
||||
class={twMerge(
|
||||
'whitespace-pre-wrap w-full',
|
||||
'whitespace-pre-wrap grow',
|
||||
|
||||
css?.text?.class,
|
||||
classes,
|
||||
@@ -244,7 +244,7 @@
|
||||
</svelte:element>
|
||||
|
||||
{#if resolvedConfig.copyButton && result}
|
||||
<div class="flex">
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
title="Copy to clipboard"
|
||||
variant="border"
|
||||
|
||||
@@ -103,15 +103,19 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
{#if error != ''}
|
||||
<div class="flex flex-col h-full w-full overflow-auto">
|
||||
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
|
||||
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<div on:pointerdown bind:this={divEl} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{#if render}
|
||||
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
{#if error != ''}
|
||||
<div class="flex flex-col h-full w-full overflow-auto">
|
||||
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
|
||||
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<div on:pointerdown bind:this={divEl} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -169,15 +169,19 @@
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
{#if error != ''}
|
||||
<div class="flex flex-col h-full w-full overflow-auto">
|
||||
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
|
||||
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<div on:pointerdown bind:this={divEl} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{#if render}
|
||||
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
{#if error != ''}
|
||||
<div class="flex flex-col h-full w-full overflow-auto">
|
||||
<Alert title="Plotly error" type="error" size="xs" class="h-full w-full ">
|
||||
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">{error}</pre>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<div on:pointerdown bind:this={divEl} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -67,8 +67,12 @@
|
||||
|
||||
<InputValue key={'canvas'} {id} input={configuration.canvas} bind:value={canvas} />
|
||||
|
||||
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
<div on:pointerdown bind:this={divEl} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{#if render}
|
||||
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
<div on:pointerdown bind:this={divEl} />
|
||||
</RunnableWrapper>
|
||||
</div>
|
||||
{:else}
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} />
|
||||
{/if}
|
||||
|
||||
@@ -695,6 +695,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
|
||||
<Portal name="db-explorer">
|
||||
<Drawer bind:this={insertDrawer} size="800px">
|
||||
<DrawerContent title="Insert row" on:close={insertDrawer.closeDrawer}>
|
||||
|
||||
@@ -84,6 +84,4 @@
|
||||
<RefreshIndicator {loading} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-full" />
|
||||
{/if}
|
||||
|
||||
@@ -28,17 +28,15 @@ function create_context_function_template(
|
||||
return `
|
||||
return async function (context, state, createProxy, goto, setTab, recompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob, askNewResource, downloadFile) {
|
||||
"use strict";
|
||||
${
|
||||
contextKeys && contextKeys.length > 0
|
||||
? `let ${contextKeys.map((key) => ` ${key} = createProxy('${key}', context['${key}'])`)};`
|
||||
: ``
|
||||
}
|
||||
${
|
||||
hasReturnAsLastLine
|
||||
? eval_string
|
||||
: `
|
||||
${contextKeys && contextKeys.length > 0
|
||||
? `let ${contextKeys.map((key) => ` ${key} = createProxy('${key}', context['${key}'])`)};`
|
||||
: ``
|
||||
}
|
||||
${hasReturnAsLastLine
|
||||
? eval_string
|
||||
: `
|
||||
return ${eval_string.startsWith('return ') ? eval_string.substring(7) : eval_string}`
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
`
|
||||
@@ -271,9 +269,8 @@ export async function eval_like(
|
||||
|
||||
if (typeof input === 'object' && input.s3) {
|
||||
const workspaceId = computeGlobalContext(worldStore).ctx.workspace
|
||||
const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${
|
||||
input?.s3
|
||||
}${input?.storage ? `&storage=${input.storage}` : ''}`
|
||||
const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${input?.s3
|
||||
}${input?.storage ? `&storage=${input.storage}` : ''}`
|
||||
downloadFile(s3href, filename || input.s3)
|
||||
} else if (typeof input === 'string') {
|
||||
if (input.startsWith('data:')) {
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
selectedTabIndex: 0
|
||||
})
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
function onFocus() {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
@@ -40,7 +43,7 @@
|
||||
|
||||
let css = initCss($app.css?.conditionalwrapper, customCss)
|
||||
|
||||
let resolvedConditions: boolean[] = []
|
||||
let resolvedConditions: boolean[] = conditions.map((_x) => false)
|
||||
let selectedConditionIndex = 0
|
||||
|
||||
function handleResolvedConditions() {
|
||||
@@ -97,23 +100,29 @@
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<div class="w-full h-full">
|
||||
{#if $app.subgrids}
|
||||
{#each resolvedConditions ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
visible={render && i == selectedConditionIndex}
|
||||
{id}
|
||||
class={twMerge(css?.container?.class, 'wm-conditional-tabs')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${i}`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={(e) => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if everRender}
|
||||
<div class="w-full h-full">
|
||||
{#if $app.subgrids}
|
||||
{#each resolvedConditions ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
visible={render && i == selectedConditionIndex}
|
||||
{id}
|
||||
class={twMerge(css?.container?.class, 'wm-conditional-tabs')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${i}`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={(e) => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $app.subgrids}
|
||||
{#each resolvedConditions ?? [] as _res, i}
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-${i}`} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
const { app, focusedGrid, selectedComponent, worldStore, connectingInput, componentControl } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
let groupContext = writable({})
|
||||
|
||||
let outputs = initOutput($worldStore, id, { group: $groupContext })
|
||||
@@ -66,23 +69,29 @@
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<div class="w-full h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
<GroupWrapper {id} context={groupContext}>
|
||||
<SubGridEditor
|
||||
visible={render}
|
||||
{id}
|
||||
class={twMerge(css?.container?.class, 'wm-container')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
</GroupWrapper>
|
||||
{/if}
|
||||
</div>
|
||||
{#if everRender}
|
||||
<div class="w-full h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
<GroupWrapper {id} context={groupContext}>
|
||||
<SubGridEditor
|
||||
visible={render}
|
||||
{id}
|
||||
class={twMerge(css?.container?.class, 'wm-container')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
</GroupWrapper>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $app.subgrids?.[`${id}-0`]}
|
||||
<GroupWrapper {id} context={groupContext}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</GroupWrapper>
|
||||
{/if}
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
export let render: boolean
|
||||
export let nodes: DecisionTreeNode[]
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
const {
|
||||
app,
|
||||
focusedGrid,
|
||||
@@ -182,55 +185,63 @@
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<div class="w-full overflow-auto">
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each Object.values(nodes) ?? [] as node, i}
|
||||
<SubGridEditor
|
||||
visible={render && node.id === currentNodeId}
|
||||
{id}
|
||||
class={twMerge(css?.container?.class, 'wm-decision-tree')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${i}`}
|
||||
containerHeight={componentContainerHeight - 40}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus(i)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if everRender}
|
||||
<div class="w-full overflow-auto">
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each Object.values(nodes) ?? [] as node, i}
|
||||
<SubGridEditor
|
||||
visible={render && node.id === currentNodeId}
|
||||
{id}
|
||||
class={twMerge(css?.container?.class, 'wm-decision-tree')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${i}`}
|
||||
containerHeight={componentContainerHeight - 40}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus(i)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-8 flex flex-row gap-2 justify-end items-center px-2 bg-surface-primary z-50">
|
||||
{#if isDebugging($debuggingComponents, id)}
|
||||
<Badge color="red" size="xs2">
|
||||
{`Debugging. Actions are disabled.`}
|
||||
</Badge>
|
||||
{#if render}
|
||||
<div class="h-8 flex flex-row gap-2 justify-end items-center px-2 bg-surface-primary z-50">
|
||||
{#if isDebugging($debuggingComponents, id)}
|
||||
<Badge color="red" size="xs2">
|
||||
{`Debugging. Actions are disabled.`}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if getFirstNode(nodes)?.id !== currentNodeId}
|
||||
<Button
|
||||
on:click={prev}
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: ArrowLeft }}
|
||||
disabled={isDebugging($debuggingComponents, id)}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={next}
|
||||
size="xs2"
|
||||
color="dark"
|
||||
endIcon={{ icon: ArrowRight }}
|
||||
disabled={isNextDisabled ||
|
||||
currentNodeId === lastNodeId ||
|
||||
isDebugging($debuggingComponents, id)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if getFirstNode(nodes)?.id !== currentNodeId}
|
||||
<Button
|
||||
on:click={prev}
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: ArrowLeft }}
|
||||
disabled={isDebugging($debuggingComponents, id)}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={next}
|
||||
size="xs2"
|
||||
color="dark"
|
||||
endIcon={{ icon: ArrowRight }}
|
||||
disabled={isNextDisabled ||
|
||||
currentNodeId === lastNodeId ||
|
||||
isDebugging($debuggingComponents, id)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
{:else if $app.subgrids}
|
||||
{#each Object.values(nodes) ?? [] as _node, i}
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-${i}`} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
export let onOpenRecomputeIds: string[] | undefined = undefined
|
||||
export let onCloseRecomputeIds: string[] | undefined = undefined
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
const {
|
||||
app,
|
||||
focusedGrid,
|
||||
@@ -77,98 +80,103 @@
|
||||
{/each}
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<div class="h-full w-full">
|
||||
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
|
||||
<Button
|
||||
btnClasses={twMerge(css?.button?.class, 'wm-drawer-button')}
|
||||
wrapperClasses={twMerge(
|
||||
css?.container?.class,
|
||||
'wm-drawer-button-container',
|
||||
resolvedConfig?.fillContainer ? 'w-full h-full' : '',
|
||||
resolvedConfig?.hideButtonOnView && $mode == 'preview'
|
||||
? 'invisible h-0 overflow-hidden'
|
||||
: ''
|
||||
)}
|
||||
wrapperStyle={css?.container?.style}
|
||||
disabled={resolvedConfig?.disabled}
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
on:click={async (e) => {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
appDrawer.toggleDrawer()
|
||||
}}
|
||||
size={resolvedConfig.size}
|
||||
color={resolvedConfig.color}
|
||||
style={css?.button?.style}
|
||||
>
|
||||
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
|
||||
<div>{resolvedConfig.label}</div>
|
||||
{/if}
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
</div>
|
||||
|
||||
<Portal target="#app-editor-top-level-drawer" name="app-drawer">
|
||||
<Drawer
|
||||
let:open
|
||||
bind:this={appDrawer}
|
||||
size="800px"
|
||||
alwaysOpen
|
||||
positionClass={$mode == 'dnd' ? '!absolute' : '!fixed'}
|
||||
shouldUsePortal={false}
|
||||
on:open={() => {
|
||||
outputs?.open.set(true)
|
||||
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
on:close={() => {
|
||||
outputs?.open.set(false)
|
||||
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
>
|
||||
<DrawerContent
|
||||
title={resolvedConfig.drawerTitle}
|
||||
on:close={() => {
|
||||
appDrawer?.toggleDrawer()
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
fullScreen={$mode !== 'dnd'}
|
||||
>
|
||||
<div
|
||||
class={twMerge('h-full', css?.drawer?.class, 'wm-drawer')}
|
||||
style={css?.drawer?.style}
|
||||
{#if render}
|
||||
<div class="h-full w-full">
|
||||
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
|
||||
<Button
|
||||
btnClasses={twMerge(css?.button?.class, 'wm-drawer-button')}
|
||||
wrapperClasses={twMerge(
|
||||
css?.container?.class,
|
||||
'wm-drawer-button-container',
|
||||
resolvedConfig?.fillContainer ? 'w-full h-full' : '',
|
||||
resolvedConfig?.hideButtonOnView && $mode == 'preview'
|
||||
? 'invisible h-0 overflow-hidden'
|
||||
: ''
|
||||
)}
|
||||
wrapperStyle={css?.container?.style}
|
||||
disabled={resolvedConfig?.disabled}
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:click={async (e) => {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
appDrawer.toggleDrawer()
|
||||
}}
|
||||
size={resolvedConfig.size}
|
||||
color={resolvedConfig.color}
|
||||
style={css?.button?.style}
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
<SubGridEditor
|
||||
visible={open && render}
|
||||
{id}
|
||||
subGridId={`${id}-0`}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
|
||||
<div>{resolvedConfig.label}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Portal>
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if everRender}
|
||||
<Portal target="#app-editor-top-level-drawer" name="app-drawer">
|
||||
<Drawer
|
||||
let:open
|
||||
bind:this={appDrawer}
|
||||
size="800px"
|
||||
alwaysOpen
|
||||
positionClass={$mode == 'dnd' ? '!absolute' : '!fixed'}
|
||||
shouldUsePortal={false}
|
||||
on:open={() => {
|
||||
outputs?.open.set(true)
|
||||
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
on:close={() => {
|
||||
outputs?.open.set(false)
|
||||
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
>
|
||||
<DrawerContent
|
||||
title={resolvedConfig.drawerTitle}
|
||||
on:close={() => {
|
||||
appDrawer?.toggleDrawer()
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
fullScreen={$mode !== 'dnd'}
|
||||
>
|
||||
<div
|
||||
class={twMerge('h-full', css?.drawer?.class, 'wm-drawer')}
|
||||
style={css?.drawer?.style}
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
<SubGridEditor
|
||||
visible={open && render}
|
||||
{id}
|
||||
subGridId={`${id}-0`}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</Portal>
|
||||
{:else if $app.subgrids?.[`${id}-0`]}
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
{/if}
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let page = 0
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
loading: false,
|
||||
@@ -72,8 +75,8 @@
|
||||
const l = initialData ? initialData.length : 0
|
||||
if (mode === 'auto') {
|
||||
const pageSize: number = configuration.auto.pageSize ?? 0
|
||||
const shouldDisplayPagination = pageSize < l ?? false
|
||||
const total = Math.ceil(l / pageSize ?? 0)
|
||||
const shouldDisplayPagination = (pageSize ?? 0) < l
|
||||
const total = Math.ceil(l / (pageSize ?? 0))
|
||||
|
||||
return {
|
||||
shouldDisplayPagination,
|
||||
@@ -136,21 +139,21 @@
|
||||
bind:result
|
||||
bind:loading
|
||||
>
|
||||
<div
|
||||
class={twMerge('w-full h-full', css?.container?.class, 'wm-list')}
|
||||
style={css?.container?.style}
|
||||
>
|
||||
{#if everRender}
|
||||
<div
|
||||
class="w-full h-full shrink flex {$allIdsInPath.includes(id) && $mode == 'dnd'
|
||||
? 'overflow-visible'
|
||||
: 'overflow-auto'} {isCard
|
||||
? 'gap-2 flex-wrap'
|
||||
: resolvedConfig?.displayBorders
|
||||
? 'divide-y flex-col'
|
||||
: 'flex-col'}"
|
||||
class={twMerge('w-full h-full', css?.container?.class, 'wm-list')}
|
||||
style={css?.container?.style}
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
{#if Array.isArray(result) && result.length > 0}
|
||||
<div
|
||||
class="w-full h-full shrink flex {$allIdsInPath.includes(id) && $mode == 'dnd'
|
||||
? 'overflow-visible'
|
||||
: 'overflow-auto'} {isCard
|
||||
? 'gap-2 flex-wrap'
|
||||
: resolvedConfig?.displayBorders
|
||||
? 'divide-y flex-col'
|
||||
: 'flex-col'}"
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-0`] && Array.isArray(result) && result.length > 0}
|
||||
{#each result ?? [] as value, index (index)}
|
||||
{@const inRange = index <= pagination.maxIndex && index >= pagination.indexOffset}
|
||||
<div
|
||||
@@ -219,53 +222,59 @@
|
||||
<div class="text-center text-tertiary">Input data is not an array</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if pagination.shouldDisplayPagination}
|
||||
<div
|
||||
class="bg-surface-secondary z-20 h-8 flex flex-row gap-1 p-1 items-center wm-list-pagination absolute bottom-0 w-full"
|
||||
>
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
color="light"
|
||||
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
|
||||
on:click={() => {
|
||||
isPreviousLoading = true
|
||||
page = page - 1
|
||||
outputs?.page.set(page, true)
|
||||
}}
|
||||
disabled={page === 0}
|
||||
>
|
||||
{#if isPreviousLoading && loading}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
{:else}
|
||||
<ChevronLeft size={14} />
|
||||
{/if}
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
color="light"
|
||||
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
|
||||
on:click={() => {
|
||||
isNextLoading = true
|
||||
page = page + 1
|
||||
outputs?.page.set(page, true)
|
||||
}}
|
||||
disabled={pagination.disableNext && pagination.total > 0}
|
||||
>
|
||||
Next
|
||||
|
||||
{#if isNextLoading && loading}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
{:else}
|
||||
<ChevronRight size={14} />
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="text-xs"
|
||||
>{page + 1} {pagination.total > 0 ? `of ${pagination.total}` : ''}</div
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if pagination.shouldDisplayPagination}
|
||||
<div
|
||||
class="bg-surface-secondary z-20 h-8 flex flex-row gap-1 p-1 items-center wm-list-pagination absolute bottom-0 w-full"
|
||||
>
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
color="light"
|
||||
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
|
||||
on:click={() => {
|
||||
isPreviousLoading = true
|
||||
page = page - 1
|
||||
outputs?.page.set(page, true)
|
||||
}}
|
||||
disabled={page === 0}
|
||||
>
|
||||
{#if isPreviousLoading && loading}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
{:else}
|
||||
<ChevronLeft size={14} />
|
||||
{/if}
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
color="light"
|
||||
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
|
||||
on:click={() => {
|
||||
isNextLoading = true
|
||||
page = page + 1
|
||||
outputs?.page.set(page, true)
|
||||
}}
|
||||
disabled={pagination.disableNext && pagination.total > 0}
|
||||
>
|
||||
Next
|
||||
|
||||
{#if isNextLoading && loading}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
{:else}
|
||||
<ChevronRight size={14} />
|
||||
{/if}
|
||||
</Button>
|
||||
<div class="text-xs">{page + 1} {pagination.total > 0 ? `of ${pagination.total}` : ''}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $app.subgrids}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
breakpoint
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
//used so that we can count number of outputs setup for first refresh
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
open: false
|
||||
@@ -110,7 +113,7 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if render}
|
||||
{#if everRender}
|
||||
<div class="h-full w-full">
|
||||
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
|
||||
<Button
|
||||
@@ -144,92 +147,94 @@
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
<Portal target="#app-editor-top-level-drawer" name="app-modal">
|
||||
<Disposable
|
||||
{id}
|
||||
let:handleClickAway
|
||||
let:zIndex
|
||||
let:open
|
||||
bind:this={disposable}
|
||||
on:open={() => {
|
||||
outputs?.open.set(true)
|
||||
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
on:close={() => {
|
||||
outputs?.open.set(false)
|
||||
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
`${
|
||||
$mode == 'dnd' ? 'absolute' : 'fixed'
|
||||
} top-0 bottom-0 left-0 right-0 transition-all duration-50`,
|
||||
open ? ' bg-black bg-opacity-60' : 'h-0 overflow-hidden invisible'
|
||||
)}
|
||||
style="z-index: {zIndex}"
|
||||
bind:clientHeight={wrapperHeight}
|
||||
<Portal target="#app-editor-top-level-drawer" name="app-modal">
|
||||
<Disposable
|
||||
{id}
|
||||
let:handleClickAway
|
||||
let:zIndex
|
||||
let:open
|
||||
bind:this={disposable}
|
||||
on:open={() => {
|
||||
outputs?.open.set(true)
|
||||
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
on:close={() => {
|
||||
outputs?.open.set(false)
|
||||
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={css?.popup?.style}
|
||||
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
|
||||
use:clickOutside={false}
|
||||
on:click_outside={(e) => {
|
||||
if ($mode !== 'dnd' && !unclickableOutside) {
|
||||
handleClickAway(e)
|
||||
}
|
||||
}}
|
||||
class={twMerge(
|
||||
`${
|
||||
$mode == 'dnd' ? 'absolute' : 'fixed'
|
||||
} top-0 bottom-0 left-0 right-0 transition-all duration-50`,
|
||||
open ? ' bg-black bg-opacity-60' : 'h-0 overflow-hidden invisible'
|
||||
)}
|
||||
style="z-index: {zIndex}"
|
||||
bind:clientHeight={wrapperHeight}
|
||||
>
|
||||
<div
|
||||
class="px-4 py-2 border-b flex justify-between items-center"
|
||||
bind:clientHeight={headerHeight}
|
||||
>
|
||||
<div>{resolvedConfig.modalTitle}</div>
|
||||
<div class="w-8">
|
||||
<button
|
||||
on:click|stopPropagation={() => {
|
||||
disposable?.closeDrawer()
|
||||
}}
|
||||
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
|
||||
>
|
||||
<X class="text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={twMerge('wm-modal h-full', 'overflow-y-auto')}
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
style={css?.popup?.style}
|
||||
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
|
||||
use:clickOutside={false}
|
||||
on:click_outside={(e) => {
|
||||
if ($mode !== 'dnd' && !unclickableOutside) {
|
||||
handleClickAway(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
<SubGridEditor
|
||||
visible={open && render}
|
||||
{id}
|
||||
{containerHeight}
|
||||
subGridId={`${id}-0`}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
<div
|
||||
class="px-4 py-2 border-b flex justify-between items-center"
|
||||
bind:clientHeight={headerHeight}
|
||||
>
|
||||
<div>{resolvedConfig.modalTitle}</div>
|
||||
<div class="w-8">
|
||||
<button
|
||||
on:click|stopPropagation={() => {
|
||||
disposable?.closeDrawer()
|
||||
}}
|
||||
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
|
||||
>
|
||||
<X class="text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={twMerge('wm-modal h-full', 'overflow-y-auto')}
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
<SubGridEditor
|
||||
visible={open && render}
|
||||
{id}
|
||||
{containerHeight}
|
||||
subGridId={`${id}-0`}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Disposable>
|
||||
</Portal>
|
||||
</Disposable>
|
||||
</Portal>
|
||||
{:else if $app.subgrids?.[`${id}-0`]}
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
{/if}
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
//used so that we can count number of outputs setup for first refresh
|
||||
initOutput($worldStore, id, {})
|
||||
|
||||
let everRender = render
|
||||
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
function onFocus() {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
@@ -82,47 +86,53 @@
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<div class="h-full w-full border" on:pointerdown={onFocus}>
|
||||
{#key sumedup}
|
||||
<Splitpanes {horizontal}>
|
||||
{#each sumedup as paneSize, index (index)}
|
||||
<Pane size={paneSize} minSize={20}>
|
||||
<div
|
||||
class="w-full h-full"
|
||||
on:pointerdown|stopPropagation={() => {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: index
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-${index}`]}
|
||||
<SubGridEditor
|
||||
visible={render}
|
||||
{id}
|
||||
shouldHighlight={$focusedGrid?.subGridIndex === index}
|
||||
class={twMerge(
|
||||
css?.container?.class,
|
||||
horizontal ? 'wm-horizontal-split-panes' : 'wm-vertical-split-panes'
|
||||
)}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${index}`}
|
||||
containerHeight={horizontal ? undefined : componentContainerHeight - 8}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: index
|
||||
{#if everRender}
|
||||
<div class="h-full w-full border" on:pointerdown={onFocus}>
|
||||
{#key sumedup}
|
||||
<Splitpanes {horizontal}>
|
||||
{#each sumedup as paneSize, index (index)}
|
||||
<Pane size={paneSize} minSize={20}>
|
||||
<div
|
||||
class="w-full h-full"
|
||||
on:pointerdown|stopPropagation={() => {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: index
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $app.subgrids?.[`${id}-${index}`]}
|
||||
<SubGridEditor
|
||||
visible={render}
|
||||
{id}
|
||||
shouldHighlight={$focusedGrid?.subGridIndex === index}
|
||||
class={twMerge(
|
||||
css?.container?.class,
|
||||
horizontal ? 'wm-horizontal-split-panes' : 'wm-vertical-split-panes'
|
||||
)}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-${index}`}
|
||||
containerHeight={horizontal ? undefined : componentContainerHeight - 8}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: index
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
{/each}
|
||||
</Splitpanes>
|
||||
{/key}
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
{/each}
|
||||
</Splitpanes>
|
||||
{/key}
|
||||
</div>
|
||||
{:else}
|
||||
{#each sumedup as _paneSize, index (index)}
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-${index}`} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
runnableComponents
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
let selected = tabs[0]
|
||||
let tabHeight: number = 0
|
||||
let footerHeight: number = 0
|
||||
@@ -155,97 +158,109 @@
|
||||
bind:result
|
||||
errorHandledByComponent={true}
|
||||
>
|
||||
<div class="w-full overflow-auto">
|
||||
<div bind:clientHeight={tabHeight}>
|
||||
<Stepper
|
||||
on:click={(e) => {
|
||||
const index = e.detail.index
|
||||
if (index <= maxReachedIndex || $mode === 'dnd') {
|
||||
runStep(index)
|
||||
}
|
||||
}}
|
||||
{tabs}
|
||||
{selectedIndex}
|
||||
{maxReachedIndex}
|
||||
{statusByStep}
|
||||
hasValidations={Boolean(runnableComponent)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && i === selectedIndex}
|
||||
subGridId={`${id}-${i}`}
|
||||
class={twMerge(css?.container?.class, 'wm-stepper')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={componentContainerHeight - tabHeight - footerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
{#if everRender}
|
||||
<div class="w-full overflow-auto">
|
||||
{#if render}
|
||||
<div bind:clientHeight={tabHeight}>
|
||||
<Stepper
|
||||
on:click={(e) => {
|
||||
const index = e.detail.index
|
||||
if (index <= maxReachedIndex || $mode === 'dnd') {
|
||||
runStep(index)
|
||||
}
|
||||
}}
|
||||
{tabs}
|
||||
{selectedIndex}
|
||||
{maxReachedIndex}
|
||||
{statusByStep}
|
||||
hasValidations={Boolean(runnableComponent)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && i === selectedIndex}
|
||||
subGridId={`${id}-${i}`}
|
||||
class={twMerge(css?.container?.class, 'wm-stepper')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={componentContainerHeight - tabHeight - footerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if render}
|
||||
<div bind:clientHeight={footerHeight}>
|
||||
<div class="flex justify-between h-10 p-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-tertiary">
|
||||
Step {selectedIndex + 1} of {tabs.length}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="contained"
|
||||
disabled={selectedIndex === 0}
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
directionClicked = 'left'
|
||||
onPrevious?.forEach((id) =>
|
||||
$runnableComponents?.[id]?.cb?.forEach((cb) => cb?.())
|
||||
)
|
||||
runStep(selectedIndex - 1)
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'left'}
|
||||
<Loader2 class="w-4 h-4 animate-spin" />
|
||||
{:else}
|
||||
<ArrowLeftIcon class="w-4 h-4" />
|
||||
{/if}
|
||||
Previous
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="contained"
|
||||
disabled={lastStep}
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
directionClicked = 'right'
|
||||
onNext?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
runStep(selectedIndex + 1)
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
Next
|
||||
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'right'}
|
||||
<Loader2 class="w-4 h-4 animate-spin" />
|
||||
{:else}
|
||||
<ArrowRightIcon class="w-4 h-4" />
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div bind:clientHeight={footerHeight}>
|
||||
<div class="flex justify-between h-10 p-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-tertiary">
|
||||
Step {selectedIndex + 1} of {tabs.length}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="contained"
|
||||
disabled={selectedIndex === 0}
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
directionClicked = 'left'
|
||||
onPrevious?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
runStep(selectedIndex - 1)
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'left'}
|
||||
<Loader2 class="w-4 h-4 animate-spin" />
|
||||
{:else}
|
||||
<ArrowLeftIcon class="w-4 h-4" />
|
||||
{/if}
|
||||
Previous
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="contained"
|
||||
disabled={lastStep}
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
directionClicked = 'right'
|
||||
onNext?.forEach((id) => $runnableComponents?.[id]?.cb?.forEach((cb) => cb?.()))
|
||||
runStep(selectedIndex + 1)
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
Next
|
||||
{#if statusByStep[selectedIndex] === 'pending' && directionClicked === 'right'}
|
||||
<Loader2 class="w-4 h-4 animate-spin" />
|
||||
{:else}
|
||||
<ArrowRightIcon class="w-4 h-4" />
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor {id} visible={false} subGridId={`${id}-${i}`} />
|
||||
{/each}
|
||||
{/if}
|
||||
</RunnableWrapper>
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
components['tabscomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
let everRender = render
|
||||
$: render && !everRender && (everRender = true)
|
||||
|
||||
const {
|
||||
app,
|
||||
@@ -117,128 +119,134 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<div class={resolvedConfig.tabsKind == 'sidebar' ? 'flex gap-4 w-full h-full' : 'w-full'}>
|
||||
{#if !resolvedConfig.tabsKind || resolvedConfig.tabsKind == 'tabs' || (resolvedConfig.tabsKind == 'invisibleOnView' && $mode == 'dnd')}
|
||||
<div bind:clientHeight={tabHeight}>
|
||||
<Tabs
|
||||
bind:selected
|
||||
class={twMerge(css?.tabRow?.class, 'wm-tabs-tabRow')}
|
||||
{#if everRender}
|
||||
<div class={resolvedConfig.tabsKind == 'sidebar' ? 'flex gap-4 w-full h-full' : 'w-full'}>
|
||||
{#if !resolvedConfig.tabsKind || resolvedConfig.tabsKind == 'tabs' || (resolvedConfig.tabsKind == 'invisibleOnView' && $mode == 'dnd')}
|
||||
<div bind:clientHeight={tabHeight}>
|
||||
<Tabs
|
||||
bind:selected
|
||||
class={twMerge(css?.tabRow?.class, 'wm-tabs-tabRow')}
|
||||
style={css?.tabRow?.style}
|
||||
>
|
||||
{#each tabs ?? [] as res, index}
|
||||
<Tab
|
||||
value={res}
|
||||
class={twMerge(css?.allTabs?.class, 'wm-tabs-alltabs')}
|
||||
style={css?.allTabs?.style}
|
||||
selectedClass={twMerge(css?.selectedTab?.class, 'wm-tabs-selectedTab')}
|
||||
selectedStyle={css?.selectedTab?.style}
|
||||
disabled={resolvedDisabledTabs[index]}
|
||||
>
|
||||
<span class="font-semibold">{res}</span>
|
||||
</Tab>
|
||||
{/each}
|
||||
</Tabs>
|
||||
</div>
|
||||
{:else if resolvedConfig.tabsKind == 'sidebar'}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex gap-y-2 flex-col w-1/6 max-w-[160px] bg-surface text-secondary opacity-80 px-4 pt-4 border-r ',
|
||||
css?.tabRow?.class,
|
||||
'wm-tabs-tabRow'
|
||||
)}
|
||||
style={css?.tabRow?.style}
|
||||
>
|
||||
{#each tabs ?? [] as res, index}
|
||||
<Tab
|
||||
value={res}
|
||||
class={twMerge(css?.allTabs?.class, 'wm-tabs-alltabs')}
|
||||
style={css?.allTabs?.style}
|
||||
selectedClass={twMerge(css?.selectedTab?.class, 'wm-tabs-selectedTab')}
|
||||
selectedStyle={css?.selectedTab?.style}
|
||||
disabled={resolvedDisabledTabs[index]}
|
||||
>
|
||||
<span class="font-semibold">{res}</span>
|
||||
</Tab>
|
||||
{/each}
|
||||
</Tabs>
|
||||
</div>
|
||||
{:else if resolvedConfig.tabsKind == 'sidebar'}
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex gap-y-2 flex-col w-1/6 max-w-[160px] bg-surface text-secondary opacity-80 px-4 pt-4 border-r ',
|
||||
css?.tabRow?.class,
|
||||
'wm-tabs-tabRow'
|
||||
)}
|
||||
style={css?.tabRow?.style}
|
||||
>
|
||||
{#each tabs ?? [] as res}
|
||||
<button
|
||||
on:pointerdown|stopPropagation
|
||||
on:click={() => (selected = res)}
|
||||
class={twMerge(
|
||||
'rounded-sm !truncate text-sm hover:text-primary px-1 py-2',
|
||||
css?.allTabs?.class,
|
||||
'wm-tabs-alltabs',
|
||||
selected == res
|
||||
? twMerge(
|
||||
'border-r border-primary border-l bg-surface text-primary',
|
||||
css?.selectedTab?.class,
|
||||
'wm-tabs-selectedTab'
|
||||
)
|
||||
: ''
|
||||
)}
|
||||
style={selected == res
|
||||
? [css?.allTabs?.style, css?.selectedTab?.style].filter(Boolean).join(';')
|
||||
: css?.allTabs?.style}
|
||||
>
|
||||
{res}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if resolvedConfig.tabsKind == 'accordion'}
|
||||
<div class="flex flex-col w-full">
|
||||
{#each tabs ?? [] as res, index}
|
||||
<div class="border-b">
|
||||
{#each tabs ?? [] as res}
|
||||
<button
|
||||
on:pointerdown|stopPropagation
|
||||
on:click={() => (selected = res)}
|
||||
class={twMerge(
|
||||
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
|
||||
'rounded-sm !truncate text-sm hover:text-primary px-1 py-2',
|
||||
css?.allTabs?.class,
|
||||
'wm-tabs-alltabs',
|
||||
selected == res
|
||||
? twMerge(
|
||||
'bg-surface text-primary ',
|
||||
'border-r border-primary border-l bg-surface text-primary',
|
||||
css?.selectedTab?.class,
|
||||
'wm-tabs-selectedTab'
|
||||
)
|
||||
: 'text-secondary'
|
||||
: ''
|
||||
)}
|
||||
style={selected == res
|
||||
? [css?.allTabs?.style, css?.selectedTab?.style].filter(Boolean).join(';')
|
||||
: css?.allTabs?.style}
|
||||
>
|
||||
<span class="mr-2 w-8 font-mono">{selected == res ? '-' : '+'}</span>
|
||||
{res}
|
||||
</button>
|
||||
{#if selected == res}
|
||||
<div class="p-2 border-t">
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && index === selectedIndex}
|
||||
subGridId={`${id}-${index}`}
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={componentContainerHeight - (titleBarHeight * tabs.length + 40)}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && i === selectedIndex}
|
||||
subGridId={`${id}-${i}`}
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
|
||||
? componentContainerHeight - tabHeight
|
||||
: componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if resolvedConfig.tabsKind == 'accordion'}
|
||||
<div class="flex flex-col w-full">
|
||||
{#each tabs ?? [] as res, index}
|
||||
<div class="border-b">
|
||||
<button
|
||||
on:pointerdown|stopPropagation
|
||||
on:click={() => (selected = res)}
|
||||
class={twMerge(
|
||||
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
|
||||
css?.allTabs?.class,
|
||||
'wm-tabs-alltabs',
|
||||
selected == res
|
||||
? twMerge(
|
||||
'bg-surface text-primary ',
|
||||
css?.selectedTab?.class,
|
||||
'wm-tabs-selectedTab'
|
||||
)
|
||||
: 'text-secondary'
|
||||
)}
|
||||
>
|
||||
<span class="mr-2 w-8 font-mono">{selected == res ? '-' : '+'}</span>
|
||||
{res}
|
||||
</button>
|
||||
{#if selected == res}
|
||||
<div class="p-2 border-t">
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && index === selectedIndex}
|
||||
subGridId={`${id}-${index}`}
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={componentContainerHeight - (titleBarHeight * tabs.length + 40)}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && i === selectedIndex}
|
||||
subGridId={`${id}-${i}`}
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
|
||||
? componentContainerHeight - tabHeight
|
||||
: componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor {id} visible={false} subGridId={`${id}-${i}`} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
try {
|
||||
localStorage.setItem(path != '' ? `app-${path}` : 'app', encodeState($appStore))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error('Error storing frontend draft in localStorage', err)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
FileClock,
|
||||
Sun,
|
||||
Moon,
|
||||
SunMoon
|
||||
SunMoon,
|
||||
Zap
|
||||
} from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
@@ -92,6 +93,7 @@
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { base } from '$lib/base'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
import LazyModePanel from './contextPanel/LazyModePanel.svelte'
|
||||
|
||||
async function hash(message) {
|
||||
try {
|
||||
@@ -175,6 +177,7 @@
|
||||
let inputsDrawerOpen = fromHub
|
||||
let historyBrowserDrawerOpen = false
|
||||
let debugAppDrawerOpen = false
|
||||
let lazyDrawerOpen = false
|
||||
let deploymentMsg: string | undefined = undefined
|
||||
|
||||
function closeSaveDrawer() {
|
||||
@@ -871,6 +874,13 @@
|
||||
action: () => {
|
||||
debugAppDrawerOpen = true
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Lazy mode',
|
||||
icon: Zap,
|
||||
action: () => {
|
||||
lazyDrawerOpen = true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1267,6 +1277,11 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:open={lazyDrawerOpen} size="800px">
|
||||
<DrawerContent title="Lazy Mode" on:close={() => (lazyDrawerOpen = false)}>
|
||||
<LazyModePanel />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Drawer bind:open={$jobsDrawerOpen} size="900px">
|
||||
<DrawerContent
|
||||
noPadding
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import GridViewer from './GridViewer.svelte'
|
||||
import Component from './component/Component.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { columnConfiguration } from '../gridUtils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { dfs, maxHeight } from './appUtils'
|
||||
import { BG_PREFIX, migrateApp } from '../utils'
|
||||
@@ -292,8 +291,6 @@
|
||||
allIdsInPath={$allIdsInPath}
|
||||
items={app.grid}
|
||||
let:dataItem
|
||||
let:hidden
|
||||
cols={columnConfiguration}
|
||||
{maxRow}
|
||||
breakpoint={$breakpoint}
|
||||
>
|
||||
@@ -308,7 +305,6 @@
|
||||
selected={false}
|
||||
locked={true}
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
{hidden}
|
||||
/>
|
||||
</div>
|
||||
</GridViewer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
|
||||
@@ -195,11 +195,9 @@
|
||||
}}
|
||||
root
|
||||
let:dataItem
|
||||
let:hidden
|
||||
let:overlapped
|
||||
let:moveMode
|
||||
let:componentDraggedId
|
||||
cols={columnConfiguration}
|
||||
on:dropped={(e) => {
|
||||
const { id, overlapped, x, y } = e.detail
|
||||
|
||||
@@ -252,7 +250,6 @@
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
>
|
||||
<Component
|
||||
{hidden}
|
||||
render={true}
|
||||
component={dataItem.data}
|
||||
selected={Boolean($selectedComponent?.includes(dataItem.id))}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { columnConfiguration, WIDE_GRID_COLUMNS } from '../gridUtils'
|
||||
|
||||
import { ROW_GAP_X, ROW_GAP_Y, ROW_HEIGHT } from './appUtils'
|
||||
|
||||
import type { EditorBreakpoint } from '../types'
|
||||
import type { AppViewerContext, EditorBreakpoint } from '../types'
|
||||
|
||||
import { onMount, createEventDispatcher } from 'svelte'
|
||||
import { onMount, createEventDispatcher, getContext } from 'svelte'
|
||||
|
||||
import type { FilledItem } from '../svelte-grid/types'
|
||||
import { getColumn, throttle } from '../svelte-grid/utils/other'
|
||||
@@ -14,9 +16,10 @@
|
||||
|
||||
type T = $$Generic
|
||||
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export let items: FilledItem<T>[]
|
||||
export let rowHeight: number = ROW_HEIGHT
|
||||
export let cols: [number, number][]
|
||||
export let gap = [ROW_GAP_X, ROW_GAP_Y]
|
||||
export let throttleUpdate = 100
|
||||
export let maxRow: number
|
||||
@@ -27,12 +30,19 @@
|
||||
|
||||
export let parentWidth: number | undefined = undefined
|
||||
|
||||
let getComputedCols
|
||||
const cols = columnConfiguration
|
||||
|
||||
let showSkeleton = false
|
||||
|
||||
let getComputedCols: 3 | 12 | undefined =
|
||||
$app.mobileViewOnSmallerScreens == false ? WIDE_GRID_COLUMNS : undefined
|
||||
|
||||
let container
|
||||
|
||||
$: [gapX, gapY] = gap
|
||||
|
||||
let xPerPx = 0
|
||||
|
||||
let yPerPx = rowHeight
|
||||
|
||||
$: containerHeight = getContainerHeight(items, yPerPx, getComputedCols)
|
||||
@@ -48,15 +58,19 @@
|
||||
}, throttleUpdate)
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
showSkeleton = true
|
||||
}, 100)
|
||||
const sizeObserver = new ResizeObserver((entries) => {
|
||||
requestAnimationFrame(() => {
|
||||
let width = entries[0].contentRect.width
|
||||
|
||||
if (width === containerWidth) return
|
||||
if ($app.mobileViewOnSmallerScreens != false || !getComputedCols) {
|
||||
getComputedCols = getColumn(parentWidth ?? width, cols)
|
||||
}
|
||||
|
||||
getComputedCols = getColumn(parentWidth ?? width, cols)
|
||||
|
||||
xPerPx = width / getComputedCols
|
||||
xPerPx = width / getComputedCols!
|
||||
|
||||
if (!containerWidth) {
|
||||
items = specifyUndefinedColumns(items, getComputedCols, cols)
|
||||
@@ -81,7 +95,7 @@
|
||||
</script>
|
||||
|
||||
<div class="svlt-grid-container" style="height: {containerHeight}px" bind:this={container}>
|
||||
{#if xPerPx}
|
||||
{#if xPerPx && getComputedCols}
|
||||
{#each items as item (item.id)}
|
||||
{@const onTop = allIdsInPath?.includes(item.id)}
|
||||
{@const width =
|
||||
@@ -103,10 +117,14 @@
|
||||
: ''} top: {top}px; left: {left}px;"
|
||||
>
|
||||
{#if item[getComputedCols]}
|
||||
<slot dataItem={item} item={item[getComputedCols]} hidden={false} />
|
||||
<slot dataItem={item} item={item[getComputedCols]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if showSkeleton}
|
||||
<div
|
||||
class="h-full w-full flex-col animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms]"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import { allItems } from '../utils'
|
||||
import type { App, AppEditorContext, AppViewerContext } from '../types'
|
||||
import { allItems, BG_PREFIX } from '../utils'
|
||||
import RecomputeAllButton from './RecomputeAllButton.svelte'
|
||||
|
||||
const { runnableComponents, app, initialized, recomputeAllContext } =
|
||||
@@ -13,10 +13,21 @@
|
||||
let firstLoad = false
|
||||
let progressTimer: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
$: !firstLoad &&
|
||||
$initialized.initializedComponents?.length ==
|
||||
allItems($app.grid, $app.subgrids).length + ($app.hiddenInlineScripts?.length ?? 0) &&
|
||||
refresh()
|
||||
$: !firstLoad && canInitializeAll($initialized?.initializedComponents, $app) && refresh()
|
||||
|
||||
function canInitializeAll(initialized: string[] | undefined, app: App) {
|
||||
if (app.lazyInitRequire == undefined) {
|
||||
return (
|
||||
initialized?.length ==
|
||||
allItems(app.grid, app.subgrids).length + (app.hiddenInlineScripts?.length ?? 0)
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
app.hiddenInlineScripts?.every((x, i) => initialized?.includes(BG_PREFIX + i)) &&
|
||||
app.lazyInitRequire?.every((x) => initialized?.includes(x))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$: $recomputeAllContext.componentNumber =
|
||||
Object.values($runnableComponents).filter((x) => x.autoRefresh).length ?? 0
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { dfs } from './appUtils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -11,16 +9,8 @@
|
||||
export let containerClass: string | undefined = undefined
|
||||
export let containerStyle: string | undefined = undefined
|
||||
|
||||
const { selectedComponent, app, connectingInput, allIdsInPath, bgRuns, recomputeAllContext } =
|
||||
const { connectingInput, bgRuns, recomputeAllContext } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let previousSelectedIds: string[] | undefined = undefined
|
||||
$: if (!deepEqual(previousSelectedIds, $selectedComponent)) {
|
||||
previousSelectedIds = $selectedComponent
|
||||
$allIdsInPath = ($selectedComponent ?? [])
|
||||
.flatMap((id) => dfs($app.grid, id, $app.subgrids ?? {}))
|
||||
.filter((x) => x != undefined) as string[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { columnConfiguration, gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { gridColumns, isFixed, toggleFixed } from '../gridUtils'
|
||||
import Grid from '../svelte-grid/Grid.svelte'
|
||||
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
|
||||
import {
|
||||
@@ -53,6 +53,10 @@
|
||||
let isActive = false
|
||||
let sber = editorContext?.componentActive?.subscribe((x) => (isActive = x))
|
||||
|
||||
let everVisible = visible
|
||||
|
||||
$: visible && !everVisible && (everVisible = true)
|
||||
|
||||
onDestroy(() => {
|
||||
sber?.()
|
||||
})
|
||||
@@ -173,160 +177,160 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="translate-x-0 translate-y-0 w-full subgrid {visible
|
||||
? 'visible'
|
||||
: 'invisible h-0 overflow-hidden'}"
|
||||
bind:this={container}
|
||||
on:pointerdown={onpointerdown}
|
||||
>
|
||||
{#if everVisible}
|
||||
<div
|
||||
class={twMerge(
|
||||
$allIdsInPath.includes(id) && $mode == 'dnd' ? 'overflow-visible' : 'overflow-auto',
|
||||
noYPadding ? '' : 'py-2',
|
||||
classes ?? '',
|
||||
noPadding ? 'px-0' : 'px-2'
|
||||
)}
|
||||
style="{containerHeight ? `height: ${containerHeight - 2}px;` : ''} {style ?? ''}"
|
||||
class="translate-x-0 translate-y-0 w-full subgrid {visible
|
||||
? 'visible'
|
||||
: 'invisible h-0 overflow-hidden'}"
|
||||
bind:this={container}
|
||||
on:pointerdown={onpointerdown}
|
||||
>
|
||||
{#if $mode !== 'preview'}
|
||||
<div
|
||||
class={highlight
|
||||
? `animate-border border-dashed border-2 min-h-full ${
|
||||
isActive && !$selectedComponent?.includes(id)
|
||||
? 'border-orange-600'
|
||||
: 'dark:border-gray-600 border-gray-400'
|
||||
}`
|
||||
: ''}
|
||||
>
|
||||
<Grid
|
||||
<div
|
||||
class={twMerge(
|
||||
$allIdsInPath.includes(id) && $mode == 'dnd' ? 'overflow-visible' : 'overflow-auto',
|
||||
noYPadding ? '' : 'py-2',
|
||||
classes ?? '',
|
||||
noPadding ? 'px-0' : 'px-2'
|
||||
)}
|
||||
style="{containerHeight ? `height: ${containerHeight - 2}px;` : ''} {style ?? ''}"
|
||||
>
|
||||
{#if $mode !== 'preview'}
|
||||
<div
|
||||
class={highlight
|
||||
? `outline !outline-dashed outline-2 min-h-full ${
|
||||
isActive && !$selectedComponent?.includes(id)
|
||||
? 'outline-orange-600'
|
||||
: 'outline-gray-400 dark:outline-gray-600'
|
||||
}`
|
||||
: ''}
|
||||
>
|
||||
<Grid
|
||||
allIdsInPath={$allIdsInPath}
|
||||
items={$app.subgrids?.[subGridId] ?? []}
|
||||
on:redraw={(e) => {
|
||||
push(editorContext?.history, $app)
|
||||
if ($app.subgrids) {
|
||||
$app.subgrids[subGridId] = e.detail
|
||||
}
|
||||
}}
|
||||
selectedIds={$selectedComponent}
|
||||
let:dataItem
|
||||
let:overlapped
|
||||
let:moveMode
|
||||
let:componentDraggedId
|
||||
scroller={container}
|
||||
parentWidth={$parentWidth - 17}
|
||||
{containerWidth}
|
||||
on:dropped={(e) => {
|
||||
const { id, overlapped, x, y } = e.detail
|
||||
|
||||
if (!overlapped) {
|
||||
moveToRoot(id, { x, y })
|
||||
} else {
|
||||
const overlappedComponent = findGridItem($app, overlapped)
|
||||
|
||||
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
|
||||
return
|
||||
}
|
||||
if (!overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
if (id === overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
moveComponentBetweenSubgrids(
|
||||
id,
|
||||
overlapped,
|
||||
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
|
||||
{ x, y }
|
||||
)
|
||||
}
|
||||
}}
|
||||
disableMove={!!$connectingInput.opened}
|
||||
>
|
||||
<ComponentWrapper
|
||||
id={dataItem.id}
|
||||
type={dataItem.data.type}
|
||||
class={classNames(
|
||||
'h-full w-full center-center',
|
||||
$selectedComponent?.includes(dataItem.id) ? 'active-grid-item' : '',
|
||||
'top-0'
|
||||
)}
|
||||
>
|
||||
<GridEditorMenu id={dataItem.id}>
|
||||
<Component
|
||||
{overlapped}
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
render={visible}
|
||||
component={dataItem.data}
|
||||
selected={Boolean($selectedComponent?.includes(dataItem.id))}
|
||||
locked={isFixed(dataItem)}
|
||||
on:lock={() => lock(dataItem)}
|
||||
on:expand={() => {
|
||||
const parentGridItem = findGridItem($app, id)
|
||||
|
||||
if (!parentGridItem) {
|
||||
return
|
||||
}
|
||||
|
||||
$selectedComponent = [dataItem.id]
|
||||
push(editorContext?.history, $app)
|
||||
|
||||
expandGriditem(
|
||||
$app.subgrids?.[subGridId] ?? [],
|
||||
dataItem.id,
|
||||
$breakpoint,
|
||||
parentGridItem
|
||||
)
|
||||
$app = $app
|
||||
}}
|
||||
on:fillHeight={() => {
|
||||
const gridItem = findGridItem($app, dataItem.id)
|
||||
const b = $breakpoint === 'sm' ? 3 : 12
|
||||
|
||||
if (gridItem?.[b]) {
|
||||
gridItem[b].fullHeight = !gridItem[b].fullHeight
|
||||
}
|
||||
$app = $app
|
||||
}}
|
||||
{moveMode}
|
||||
{componentDraggedId}
|
||||
/>
|
||||
</GridEditorMenu>
|
||||
</ComponentWrapper>
|
||||
</Grid>
|
||||
</div>
|
||||
{:else}
|
||||
<GridViewer
|
||||
allIdsInPath={$allIdsInPath}
|
||||
items={$app.subgrids?.[subGridId] ?? []}
|
||||
on:redraw={(e) => {
|
||||
push(editorContext?.history, $app)
|
||||
if ($app.subgrids) {
|
||||
$app.subgrids[subGridId] = e.detail
|
||||
}
|
||||
}}
|
||||
selectedIds={$selectedComponent}
|
||||
let:dataItem
|
||||
let:hidden
|
||||
let:overlapped
|
||||
let:moveMode
|
||||
let:componentDraggedId
|
||||
cols={columnConfiguration}
|
||||
scroller={container}
|
||||
breakpoint={$breakpoint}
|
||||
parentWidth={$parentWidth - 17}
|
||||
{containerWidth}
|
||||
on:dropped={(e) => {
|
||||
const { id, overlapped, x, y } = e.detail
|
||||
|
||||
if (!overlapped) {
|
||||
moveToRoot(id, { x, y })
|
||||
} else {
|
||||
const overlappedComponent = findGridItem($app, overlapped)
|
||||
|
||||
if (overlappedComponent && !isContainer(overlappedComponent.data.type)) {
|
||||
return
|
||||
}
|
||||
if (!overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
if (id === overlapped) {
|
||||
return
|
||||
}
|
||||
|
||||
moveComponentBetweenSubgrids(
|
||||
id,
|
||||
overlapped,
|
||||
subGridIndexKey(overlappedComponent?.data?.type, overlapped, $worldStore),
|
||||
{ x, y }
|
||||
)
|
||||
}
|
||||
}}
|
||||
disableMove={!!$connectingInput.opened}
|
||||
{maxRow}
|
||||
>
|
||||
<ComponentWrapper
|
||||
id={dataItem.id}
|
||||
type={dataItem.data.type}
|
||||
class={classNames(
|
||||
'h-full w-full center-center',
|
||||
$selectedComponent?.includes(dataItem.id) ? 'active-grid-item' : '',
|
||||
'top-0'
|
||||
)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:pointerdown|stopPropagation={(e) => selectComponent(e, dataItem.id)}
|
||||
class={classNames('h-full w-full center-center', 'top-0')}
|
||||
>
|
||||
<GridEditorMenu id={dataItem.id}>
|
||||
<Component
|
||||
{hidden}
|
||||
{overlapped}
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
render={visible}
|
||||
component={dataItem.data}
|
||||
selected={Boolean($selectedComponent?.includes(dataItem.id))}
|
||||
locked={isFixed(dataItem)}
|
||||
on:lock={() => lock(dataItem)}
|
||||
on:expand={() => {
|
||||
const parentGridItem = findGridItem($app, id)
|
||||
|
||||
if (!parentGridItem) {
|
||||
return
|
||||
}
|
||||
|
||||
$selectedComponent = [dataItem.id]
|
||||
push(editorContext?.history, $app)
|
||||
|
||||
expandGriditem(
|
||||
$app.subgrids?.[subGridId] ?? [],
|
||||
dataItem.id,
|
||||
$breakpoint,
|
||||
parentGridItem
|
||||
)
|
||||
$app = $app
|
||||
}}
|
||||
on:fillHeight={() => {
|
||||
const gridItem = findGridItem($app, dataItem.id)
|
||||
const b = $breakpoint === 'sm' ? 3 : 12
|
||||
|
||||
if (gridItem?.[b]) {
|
||||
gridItem[b].fullHeight = !gridItem[b].fullHeight
|
||||
}
|
||||
$app = $app
|
||||
}}
|
||||
{moveMode}
|
||||
{componentDraggedId}
|
||||
/>
|
||||
</GridEditorMenu>
|
||||
</ComponentWrapper>
|
||||
</Grid>
|
||||
</div>
|
||||
{:else}
|
||||
<GridViewer
|
||||
allIdsInPath={$allIdsInPath}
|
||||
items={$app.subgrids?.[subGridId] ?? []}
|
||||
let:dataItem
|
||||
let:hidden
|
||||
cols={columnConfiguration}
|
||||
breakpoint={$breakpoint}
|
||||
parentWidth={$parentWidth - 17}
|
||||
{containerWidth}
|
||||
{maxRow}
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:pointerdown|stopPropagation={(e) => selectComponent(e, dataItem.id)}
|
||||
class={classNames('h-full w-full center-center', 'top-0')}
|
||||
>
|
||||
<Component
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
render={visible}
|
||||
component={dataItem.data}
|
||||
selected={Boolean($selectedComponent?.includes(dataItem.id))}
|
||||
locked={isFixed(dataItem)}
|
||||
{hidden}
|
||||
/>
|
||||
</div>
|
||||
</GridViewer>
|
||||
{/if}
|
||||
<Component
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
render={true}
|
||||
component={dataItem.data}
|
||||
selected={Boolean($selectedComponent?.includes(dataItem.id))}
|
||||
locked={isFixed(dataItem)}
|
||||
/>
|
||||
</div>
|
||||
</GridViewer>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $app.lazyInitRequire == undefined}
|
||||
{#each $app?.subgrids?.[subGridId] ?? [] as item}
|
||||
<Component render={false} component={item.data} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
@@ -1,969 +1,39 @@
|
||||
<script lang="ts" context="module">
|
||||
let outTimeout: NodeJS.Timeout | undefined = undefined
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppEditorContext, AppViewerContext } from '../../types'
|
||||
import ComponentHeader from '../ComponentHeader.svelte'
|
||||
import ComponentInner from './ComponentInner.svelte'
|
||||
import ComponentRendered from './ComponentRendered.svelte'
|
||||
import type { AppComponent } from './components'
|
||||
|
||||
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
|
||||
import AppMultiSelectV2 from '../../components/inputs/AppMultiSelectV2.svelte'
|
||||
import AppModal from '../../components/layout/AppModal.svelte'
|
||||
import AppSchemaForm from '../../components/buttons/AppSchemaForm.svelte'
|
||||
import AppStepper from '../../components/layout/AppStepper.svelte'
|
||||
import AppSelectTab from '../../components/inputs/AppSelectTab.svelte'
|
||||
import AppConditionalWrapper from '../../components/layout/AppConditionalWrapper.svelte'
|
||||
import AppSelectStep from '../../components/inputs/AppSelectStep.svelte'
|
||||
import AppDownload from '../../components/display/AppDownload.svelte'
|
||||
import AppLogsComponent from '../../components/display/AppLogsComponent.svelte'
|
||||
import AppFlowStatusComponent from '../../components/display/AppFlowStatusComponent.svelte'
|
||||
import AppChartJs from '../../components/display/AppChartJs.svelte'
|
||||
import AppChartJsV2 from '../../components/display/AppChartJsV2.svelte'
|
||||
import AppQuillEditor from '../../components/inputs/AppQuillEditor.svelte'
|
||||
import AppList from '../../components/layout/AppList.svelte'
|
||||
import AppJobIdLogComponent from '../../components/display/AppJobIdLogComponent.svelte'
|
||||
import AppJobIdFlowStatus from '../../components/display/AppJobIdFlowStatus.svelte'
|
||||
import AppCarouselList from '../../components/display/AppCarouselList.svelte'
|
||||
import AppAccordionList from '../../components/display/AppAccordionList.svelte'
|
||||
import AppAggridTableEe from '../../components/display/table/AppAggridTableEe.svelte'
|
||||
import AppCustomComponent from '../../components/display/AppCustomComponent.svelte'
|
||||
import AppStatCard from '../../components/display/AppStatCard.svelte'
|
||||
import AppMenu from '../../components/display/AppMenu.svelte'
|
||||
import AppDecisionTree from '../../components/layout/AppDecisionTree.svelte'
|
||||
import AppAgCharts from '../../components/display/charts/AppAgCharts.svelte'
|
||||
import AppDbExplorer from '../../components/display/dbtable/AppDbExplorer.svelte'
|
||||
import AppS3FileInput from '../../components/inputs/AppS3FileInput.svelte'
|
||||
import AppAlert from '../../components/display/AppAlert.svelte'
|
||||
import AppDateSliderInput from '../../components/inputs/AppDateSliderInput.svelte'
|
||||
import AppTimeInput from '../../components/inputs/AppTimeInput.svelte'
|
||||
import AppDateTimeInput from '../../components/inputs/AppDateTimeInput.svelte'
|
||||
import AppAggridInfiniteTable from '../../components/display/table/AppAggridInfiniteTable.svelte'
|
||||
import AppAggridInfiniteTableEe from '../../components/display/table/AppAggridInfiniteTableEe.svelte'
|
||||
import AppDisplayComponent from '../../components/display/AppDisplayComponent.svelte'
|
||||
import AppTimeseries from '../../components/display/AppTimeseries.svelte'
|
||||
import AppHtml from '../../components/display/AppHtml.svelte'
|
||||
import AppMarkdown from '../../components/display/AppMarkdown.svelte'
|
||||
import VegaLiteHtml from '../../components/display/VegaLiteHtml.svelte'
|
||||
import PlotlyHtml from '../../components/display/PlotlyHtml.svelte'
|
||||
import PlotlyHtmlV2 from '../../components/display/PlotlyHtmlV2.svelte'
|
||||
import AppScatterChart from '../../components/display/AppScatterChart.svelte'
|
||||
import AppPieChart from '../../components/display/AppPieChart.svelte'
|
||||
import AppTable from '../../components/display/table/AppTable.svelte'
|
||||
import AppAggridTable from '../../components/display/table/AppAggridTable.svelte'
|
||||
import AppText from '../../components/display/AppText.svelte'
|
||||
import AppButton from '../../components/buttons/AppButton.svelte'
|
||||
import AppForm from '../../components/buttons/AppForm.svelte'
|
||||
import AppFormButton from '../../components/buttons/AppFormButton.svelte'
|
||||
import AppCheckbox from '../../components/inputs/AppCheckbox.svelte'
|
||||
import AppTextInput from '../../components/inputs/AppTextInput.svelte'
|
||||
import AppDateInput from '../../components/inputs/AppDateInput.svelte'
|
||||
import AppSelect from '../../components/inputs/AppSelect.svelte'
|
||||
import AppBarChart from '../../components/display/AppBarChart.svelte'
|
||||
import AppDivider from '../../components/layout/AppDivider.svelte'
|
||||
import AppRangeInput from '../../components/inputs/AppRangeInput.svelte'
|
||||
import AppTabs from '../../components/layout/AppTabs.svelte'
|
||||
import AppContainer from '../../components/layout/AppContainer.svelte'
|
||||
import AppSplitpanes from '../../components/layout/AppSplitpanes.svelte'
|
||||
import AppIcon from '../../components/display/AppIcon.svelte'
|
||||
import AppFileInput from '../../components/inputs/AppFileInput.svelte'
|
||||
import AppImage from '../../components/display/AppImage.svelte'
|
||||
import AppDrawer from '../../components/layout/AppDrawer.svelte'
|
||||
import AppMap from '../../components/display/AppMap.svelte'
|
||||
import AppPdf from '../../components/display/AppPdf.svelte'
|
||||
import AppCurrencyInput from '../../components/inputs/currency/AppCurrencyInput.svelte'
|
||||
import AppSliderInputs from '../../components/inputs/AppSliderInputs.svelte'
|
||||
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
|
||||
import AppNavbar from '../../components/display/AppNavbar.svelte'
|
||||
import AppDateSelect from '../../components/inputs/AppDateSelect.svelte'
|
||||
import AppDisplayComponentByJobId from '../../components/display/AppRecomputeAll.svelte'
|
||||
import AppRecomputeAll from '../../components/display/AppRecomputeAll.svelte'
|
||||
import AppUserResource from '../../components/inputs/AppUserResource.svelte'
|
||||
import { Anchor } from 'lucide-svelte'
|
||||
import { findGridItemParentGrid, isContainer } from '../appUtils'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
export let locked: boolean = false
|
||||
export let render: boolean
|
||||
export let hidden: boolean
|
||||
export let fullHeight: boolean
|
||||
export let overlapped: string | undefined = undefined
|
||||
export let moveMode: string | undefined = undefined
|
||||
export let componentDraggedId: string | undefined = undefined
|
||||
const { mode, app, hoverStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const editorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
const componentActive = editorContext?.componentActive
|
||||
export let moveMode: string | undefined = undefined
|
||||
let everRender = render
|
||||
|
||||
const movingcomponents = editorContext?.movingcomponents
|
||||
$: ismoving =
|
||||
movingcomponents != undefined && $mode == 'dnd' && $movingcomponents?.includes(component.id)
|
||||
|
||||
let initializing: boolean | undefined = undefined
|
||||
let errorHandledByComponent: boolean = false
|
||||
let componentContainerHeight: number = 0
|
||||
let componentContainerWidth: number = 0
|
||||
|
||||
let inlineEditorOpened: boolean = false
|
||||
|
||||
function mouseOut() {
|
||||
outTimeout && clearTimeout(outTimeout)
|
||||
outTimeout = setTimeout(() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
// In order to avoid flickering when hovering over table actions,
|
||||
// we leave the actions to manage the hover state
|
||||
if ($hoverStore.startsWith(`${component.id}_`)) {
|
||||
return
|
||||
}
|
||||
|
||||
$hoverStore = undefined
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function componentDraggedIsNotChild(componentDraggedId: string, componentId: string) {
|
||||
let parentGrid = findGridItemParentGrid($app, componentDraggedId)
|
||||
|
||||
return !parentGrid?.startsWith(`${componentId}-`)
|
||||
}
|
||||
|
||||
function areOnTheSameSubgrid(componentDraggedId: string, componentId: string) {
|
||||
return (
|
||||
findGridItemParentGrid($app, componentDraggedId) === findGridItemParentGrid($app, componentId)
|
||||
)
|
||||
}
|
||||
|
||||
let cachedComponentDraggedIsNotChild: boolean | undefined
|
||||
let cachedAreOnTheSameSubgrid: boolean | undefined
|
||||
|
||||
function updateCache(componentDraggedId: string | undefined) {
|
||||
if (componentDraggedId) {
|
||||
cachedComponentDraggedIsNotChild = componentDraggedIsNotChild(
|
||||
componentDraggedId,
|
||||
component.id
|
||||
)
|
||||
cachedAreOnTheSameSubgrid = areOnTheSameSubgrid(componentDraggedId, component.id)
|
||||
} else {
|
||||
cachedComponentDraggedIsNotChild = undefined
|
||||
cachedAreOnTheSameSubgrid = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$: updateCache(componentDraggedId)
|
||||
$: render && !everRender && (everRender = true)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
outTimeout && clearTimeout(outTimeout)
|
||||
if (component.id !== $hoverStore) {
|
||||
$hoverStore = component.id
|
||||
}
|
||||
}}
|
||||
on:mouseout|stopPropagation={mouseOut}
|
||||
class={twMerge(
|
||||
'h-full flex flex-col w-full component relative',
|
||||
initializing ? 'overflow-hidden h-0' : '',
|
||||
hidden && $mode === 'preview' ? 'hidden' : ''
|
||||
)}
|
||||
data-connection-button
|
||||
>
|
||||
{#if locked && componentActive && $componentActive && moveMode === 'move' && componentDraggedId && componentDraggedId !== component.id && cachedAreOnTheSameSubgrid}
|
||||
<div
|
||||
class={twMerge('absolute inset-0 bg-locked center-center flex-col z-50', 'bg-locked-hover')}
|
||||
>
|
||||
<div class="bg-surface p-2 shadow-sm rounded-md flex center-center flex-col gap-2">
|
||||
<Anchor size={24} class="text-primary " />
|
||||
<div class="text-xs"> Anchored: The component cannot be moved. </div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if moveMode === 'insert' && isContainer(component.type) && componentDraggedId && componentDraggedId !== component.id && cachedComponentDraggedIsNotChild}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
|
||||
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
|
||||
overlapped === component?.id ? 'bg-draggedover dark:bg-draggedover-dark' : ''
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
{#if $mode !== 'preview'}
|
||||
<ComponentHeader
|
||||
on:mouseover={() => {
|
||||
outTimeout && clearTimeout(outTimeout)
|
||||
|
||||
if (component.id !== $hoverStore) {
|
||||
$hoverStore = component.id
|
||||
}
|
||||
}}
|
||||
hover={$hoverStore === component.id}
|
||||
{component}
|
||||
{selected}
|
||||
{fullHeight}
|
||||
connecting={$connectingInput.opened}
|
||||
on:lock
|
||||
on:expand
|
||||
on:fillHeight
|
||||
{locked}
|
||||
{inlineEditorOpened}
|
||||
hasInlineEditor={component.type === 'textcomponent' &&
|
||||
component.componentInput &&
|
||||
component.componentInput.type !== 'connected'}
|
||||
on:triggerInlineEditor={() => {
|
||||
inlineEditorOpened = !inlineEditorOpened
|
||||
}}
|
||||
{errorHandledByComponent}
|
||||
{componentContainerWidth}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if ismoving}
|
||||
<div class="absolute -top-8 w-40">
|
||||
<button
|
||||
class="border p-0.5 text-xs"
|
||||
on:click={() => {
|
||||
$movingcomponents = undefined
|
||||
}}
|
||||
>
|
||||
Cancel move
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class={twMerge(
|
||||
'h-full outline-1',
|
||||
$mode === 'dnd' ? 'bg-surface/40' : '',
|
||||
$hoverStore === component.id && $mode !== 'preview'
|
||||
? $connectingInput.opened
|
||||
? 'outline outline-[#f8aa4b]'
|
||||
: 'outline outline-blue-400'
|
||||
: '',
|
||||
selected && $mode !== 'preview' ? 'outline outline-blue-600' : '',
|
||||
$mode != 'preview' ? 'cursor-pointer' : '',
|
||||
'relative z-auto',
|
||||
$app.css?.['app']?.['component']?.class,
|
||||
'wm-app-component',
|
||||
ismoving ? 'animate-pulse' : ''
|
||||
)}
|
||||
style={$app.css?.['app']?.['component']?.style}
|
||||
bind:clientHeight={componentContainerHeight}
|
||||
bind:clientWidth={componentContainerWidth}
|
||||
>
|
||||
{#if component.type === 'displaycomponent'}
|
||||
<AppDisplayComponent
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'logcomponent'}
|
||||
<AppLogsComponent />
|
||||
{:else if component.type === 'jobidlogcomponent'}
|
||||
<AppJobIdLogComponent
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'flowstatuscomponent'}
|
||||
<AppFlowStatusComponent />
|
||||
{:else if component.type === 'jobidflowstatuscomponent'}
|
||||
<AppJobIdFlowStatus
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'barchartcomponent'}
|
||||
<AppBarChart
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'timeseriescomponent'}
|
||||
<AppTimeseries
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'htmlcomponent'}
|
||||
<AppHtml
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'customcomponent'}
|
||||
<AppCustomComponent
|
||||
customComponent={component.customComponent}
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'mardowncomponent'}
|
||||
<AppMarkdown
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'vegalitecomponent'}
|
||||
<VegaLiteHtml
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'plotlycomponent'}
|
||||
<PlotlyHtml
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'plotlycomponentv2'}
|
||||
<PlotlyHtmlV2
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'scatterchartcomponent'}
|
||||
<AppScatterChart
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'piechartcomponent'}
|
||||
<AppPieChart
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'agchartscomponent'}
|
||||
<AppAgCharts
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'agchartscomponentee'}
|
||||
<AppAgCharts
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
license={component.license}
|
||||
ee={true}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'tablecomponent'}
|
||||
<AppTable
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
actionButtons={component.actionButtons}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dbexplorercomponent'}
|
||||
<AppDbExplorer
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
bind:initializing
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridcomponent'}
|
||||
<AppAggridTable
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
actionsOrder={component.actionsOrder ?? undefined}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridcomponentee'}
|
||||
<AppAggridTableEe
|
||||
license={component.license}
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
actionsOrder={component.actionsOrder ?? undefined}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridinfinitecomponent'}
|
||||
<AppAggridInfiniteTable
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridinfinitecomponentee'}
|
||||
<AppAggridInfiniteTableEe
|
||||
license={component.license}
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'textcomponent'}
|
||||
<AppText
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
bind:editorMode={inlineEditorOpened}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'buttoncomponent'}
|
||||
<AppButton
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'downloadcomponent'}
|
||||
<AppDownload
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
|
||||
<AppSelect
|
||||
recomputeIds={component.recomputeIds}
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
onSelect={component.onSelect}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'userresourcecomponent'}
|
||||
<AppUserResource
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'multiselectcomponent'}
|
||||
<AppMultiSelect
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'multiselectcomponentv2'}
|
||||
<AppMultiSelectV2
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'formcomponent'}
|
||||
<AppForm
|
||||
id={component.id}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'formbuttoncomponent'}
|
||||
<AppFormButton
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
recomputeIds={component.recomputeIds}
|
||||
onToggle={component.onToggle}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'textinputcomponent'}
|
||||
<AppTextInput
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'quillcomponent'}
|
||||
<AppQuillEditor id={component.id} configuration={component.configuration} {render} />
|
||||
{:else if component.type === 'textareainputcomponent'}
|
||||
<AppTextInput
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
inputType="textarea"
|
||||
appCssKey="textareainputcomponent"
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'emailinputcomponent'}
|
||||
<AppTextInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="email"
|
||||
appCssKey="emailinputcomponent"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'passwordinputcomponent'}
|
||||
<AppTextInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="password"
|
||||
appCssKey="passwordinputcomponent"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dateinputcomponent'}
|
||||
<AppDateInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="date"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'timeinputcomponent'}
|
||||
<AppTimeInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'datetimeinputcomponent'}
|
||||
<AppDateTimeInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="date"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'numberinputcomponent'}
|
||||
<AppNumberInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'currencycomponent'}
|
||||
<AppCurrencyInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'slidercomponent'}
|
||||
<AppSliderInputs
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dateslidercomponent'}
|
||||
<AppDateSliderInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'horizontaldividercomponent'}
|
||||
<AppDivider
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
position="horizontal"
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'verticaldividercomponent'}
|
||||
<AppDivider
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
position="vertical"
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'rangecomponent'}
|
||||
<AppRangeInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'tabscomponent' && component.tabs}
|
||||
<AppTabs
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
tabs={component.tabs}
|
||||
disabledTabs={component.disabledTabs}
|
||||
onTabChange={component.onTabChange}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'steppercomponent' && component.tabs}
|
||||
<AppStepper
|
||||
id={component.id}
|
||||
tabs={component.tabs}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
componentInput={component.componentInput}
|
||||
onNext={component.onNext}
|
||||
onPrevious={component.onPrevious}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'conditionalwrapper' && component.conditions}
|
||||
<AppConditionalWrapper
|
||||
id={component.id}
|
||||
conditions={component.conditions}
|
||||
customCss={component.customCss}
|
||||
onTabChange={component.onTabChange}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'containercomponent'}
|
||||
<AppContainer
|
||||
groupFields={component.groupFields}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'listcomponent'}
|
||||
<AppList
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
configuration={component.configuration}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'verticalsplitpanescomponent'}
|
||||
<AppSplitpanes
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
panes={component.panes}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'horizontalsplitpanescomponent'}
|
||||
<AppSplitpanes
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
panes={component.panes}
|
||||
{componentContainerHeight}
|
||||
horizontal={true}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'iconcomponent'}
|
||||
<AppIcon
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'fileinputcomponent'}
|
||||
<AppFileInput
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onFileChange={component.onFileChange}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 's3fileinputcomponent'}
|
||||
<AppS3FileInput
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onFileChange={component.onFileChange}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'imagecomponent'}
|
||||
<AppImage
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'drawercomponent'}
|
||||
<AppDrawer
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onOpenRecomputeIds={component.onOpenRecomputeIds}
|
||||
onCloseRecomputeIds={component.onCloseRecomputeIds}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'mapcomponent'}
|
||||
<AppMap
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'pdfcomponent'}
|
||||
<AppPdf
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'modalcomponent'}
|
||||
<AppModal
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onOpenRecomputeIds={component.onOpenRecomputeIds}
|
||||
onCloseRecomputeIds={component.onCloseRecomputeIds}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'schemaformcomponent'}
|
||||
<AppSchemaForm
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{initializing}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selecttabcomponent'}
|
||||
<AppSelectTab
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selectstepcomponent'}
|
||||
<AppSelectStep
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'chartjscomponent'}
|
||||
<AppChartJs
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'chartjscomponentv2'}
|
||||
<AppChartJsV2
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'carousellistcomponent'}
|
||||
<AppCarouselList
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'accordionlistcomponent'}
|
||||
<AppAccordionList
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'statcomponent'}
|
||||
<AppStatCard
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'menucomponent'}
|
||||
<AppMenu
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
menuItems={component.menuItems}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'decisiontreecomponent' && component.nodes}
|
||||
<AppDecisionTree
|
||||
id={component.id}
|
||||
nodes={component.nodes}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'alertcomponent'}
|
||||
<AppAlert
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'navbarcomponent'}
|
||||
<AppNavbar
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
navbarItems={component.navbarItems}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dateselectcomponent'}
|
||||
<AppDateSelect
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'jobiddisplaycomponent'}
|
||||
<AppDisplayComponentByJobId
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'recomputeallcomponent'}
|
||||
<AppRecomputeAll
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if initializing}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
if (component.id !== $hoverStore) {
|
||||
$hoverStore = component.id
|
||||
}
|
||||
}}
|
||||
on:mouseout|stopPropagation={() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
$hoverStore = undefined
|
||||
}
|
||||
}}
|
||||
class="absolute inset-0 center-center flex-col bg- border animate-skeleton"
|
||||
{#if everRender}
|
||||
<ComponentRendered
|
||||
{moveMode}
|
||||
{componentDraggedId}
|
||||
{overlapped}
|
||||
{render}
|
||||
{component}
|
||||
{selected}
|
||||
{locked}
|
||||
{fullHeight}
|
||||
/>
|
||||
{:else}
|
||||
<ComponentInner
|
||||
{component}
|
||||
render={false}
|
||||
componentContainerHeight={0}
|
||||
errorHandledByComponent={false}
|
||||
inlineEditorOpened={false}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,778 @@
|
||||
<script lang="ts">
|
||||
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
|
||||
import AppMultiSelectV2 from '../../components/inputs/AppMultiSelectV2.svelte'
|
||||
import AppModal from '../../components/layout/AppModal.svelte'
|
||||
import AppSchemaForm from '../../components/buttons/AppSchemaForm.svelte'
|
||||
import AppStepper from '../../components/layout/AppStepper.svelte'
|
||||
import AppSelectTab from '../../components/inputs/AppSelectTab.svelte'
|
||||
import AppConditionalWrapper from '../../components/layout/AppConditionalWrapper.svelte'
|
||||
import AppSelectStep from '../../components/inputs/AppSelectStep.svelte'
|
||||
import AppDownload from '../../components/display/AppDownload.svelte'
|
||||
import AppLogsComponent from '../../components/display/AppLogsComponent.svelte'
|
||||
import AppFlowStatusComponent from '../../components/display/AppFlowStatusComponent.svelte'
|
||||
import AppChartJs from '../../components/display/AppChartJs.svelte'
|
||||
import AppChartJsV2 from '../../components/display/AppChartJsV2.svelte'
|
||||
import AppQuillEditor from '../../components/inputs/AppQuillEditor.svelte'
|
||||
import AppList from '../../components/layout/AppList.svelte'
|
||||
import AppJobIdLogComponent from '../../components/display/AppJobIdLogComponent.svelte'
|
||||
import AppJobIdFlowStatus from '../../components/display/AppJobIdFlowStatus.svelte'
|
||||
import AppCarouselList from '../../components/display/AppCarouselList.svelte'
|
||||
import AppAccordionList from '../../components/display/AppAccordionList.svelte'
|
||||
import AppAggridTableEe from '../../components/display/table/AppAggridTableEe.svelte'
|
||||
import AppCustomComponent from '../../components/display/AppCustomComponent.svelte'
|
||||
import AppStatCard from '../../components/display/AppStatCard.svelte'
|
||||
import AppMenu from '../../components/display/AppMenu.svelte'
|
||||
import AppDecisionTree from '../../components/layout/AppDecisionTree.svelte'
|
||||
import AppAgCharts from '../../components/display/charts/AppAgCharts.svelte'
|
||||
import AppDbExplorer from '../../components/display/dbtable/AppDbExplorer.svelte'
|
||||
import AppS3FileInput from '../../components/inputs/AppS3FileInput.svelte'
|
||||
import AppAlert from '../../components/display/AppAlert.svelte'
|
||||
import AppDateSliderInput from '../../components/inputs/AppDateSliderInput.svelte'
|
||||
import AppTimeInput from '../../components/inputs/AppTimeInput.svelte'
|
||||
import AppDateTimeInput from '../../components/inputs/AppDateTimeInput.svelte'
|
||||
import AppAggridInfiniteTable from '../../components/display/table/AppAggridInfiniteTable.svelte'
|
||||
import AppAggridInfiniteTableEe from '../../components/display/table/AppAggridInfiniteTableEe.svelte'
|
||||
import AppDisplayComponent from '../../components/display/AppDisplayComponent.svelte'
|
||||
import AppTimeseries from '../../components/display/AppTimeseries.svelte'
|
||||
import AppHtml from '../../components/display/AppHtml.svelte'
|
||||
import AppMarkdown from '../../components/display/AppMarkdown.svelte'
|
||||
import VegaLiteHtml from '../../components/display/VegaLiteHtml.svelte'
|
||||
import PlotlyHtml from '../../components/display/PlotlyHtml.svelte'
|
||||
import PlotlyHtmlV2 from '../../components/display/PlotlyHtmlV2.svelte'
|
||||
import AppScatterChart from '../../components/display/AppScatterChart.svelte'
|
||||
import AppPieChart from '../../components/display/AppPieChart.svelte'
|
||||
import AppTable from '../../components/display/table/AppTable.svelte'
|
||||
import AppAggridTable from '../../components/display/table/AppAggridTable.svelte'
|
||||
import AppText from '../../components/display/AppText.svelte'
|
||||
import AppButton from '../../components/buttons/AppButton.svelte'
|
||||
import AppForm from '../../components/buttons/AppForm.svelte'
|
||||
import AppFormButton from '../../components/buttons/AppFormButton.svelte'
|
||||
import AppCheckbox from '../../components/inputs/AppCheckbox.svelte'
|
||||
import AppTextInput from '../../components/inputs/AppTextInput.svelte'
|
||||
import AppDateInput from '../../components/inputs/AppDateInput.svelte'
|
||||
import AppSelect from '../../components/inputs/AppSelect.svelte'
|
||||
import AppBarChart from '../../components/display/AppBarChart.svelte'
|
||||
import AppDivider from '../../components/layout/AppDivider.svelte'
|
||||
import AppRangeInput from '../../components/inputs/AppRangeInput.svelte'
|
||||
import AppTabs from '../../components/layout/AppTabs.svelte'
|
||||
import AppContainer from '../../components/layout/AppContainer.svelte'
|
||||
import AppSplitpanes from '../../components/layout/AppSplitpanes.svelte'
|
||||
import AppIcon from '../../components/display/AppIcon.svelte'
|
||||
import AppFileInput from '../../components/inputs/AppFileInput.svelte'
|
||||
import AppImage from '../../components/display/AppImage.svelte'
|
||||
import AppDrawer from '../../components/layout/AppDrawer.svelte'
|
||||
import AppMap from '../../components/display/AppMap.svelte'
|
||||
import AppPdf from '../../components/display/AppPdf.svelte'
|
||||
import AppCurrencyInput from '../../components/inputs/currency/AppCurrencyInput.svelte'
|
||||
import AppSliderInputs from '../../components/inputs/AppSliderInputs.svelte'
|
||||
import AppNumberInput from '../../components/inputs/AppNumberInput.svelte'
|
||||
import AppNavbar from '../../components/display/AppNavbar.svelte'
|
||||
import AppDateSelect from '../../components/inputs/AppDateSelect.svelte'
|
||||
import AppDisplayComponentByJobId from '../../components/display/AppRecomputeAll.svelte'
|
||||
import AppRecomputeAll from '../../components/display/AppRecomputeAll.svelte'
|
||||
import AppUserResource from '../../components/inputs/AppUserResource.svelte'
|
||||
import type { AppComponent } from './components'
|
||||
|
||||
export let component: AppComponent
|
||||
export let render: boolean
|
||||
export let componentContainerHeight: number
|
||||
export let initializing: boolean | undefined = undefined
|
||||
export let errorHandledByComponent: boolean
|
||||
export let inlineEditorOpened: boolean
|
||||
</script>
|
||||
|
||||
{#if component.type === 'displaycomponent'}
|
||||
<AppDisplayComponent
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'logcomponent'}
|
||||
<AppLogsComponent />
|
||||
{:else if component.type === 'jobidlogcomponent'}
|
||||
<AppJobIdLogComponent
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'flowstatuscomponent'}
|
||||
<AppFlowStatusComponent />
|
||||
{:else if component.type === 'jobidflowstatuscomponent'}
|
||||
<AppJobIdFlowStatus
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'barchartcomponent'}
|
||||
<AppBarChart
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'timeseriescomponent'}
|
||||
<AppTimeseries
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'htmlcomponent'}
|
||||
<AppHtml
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'customcomponent'}
|
||||
<AppCustomComponent
|
||||
customComponent={component.customComponent}
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'mardowncomponent'}
|
||||
<AppMarkdown
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'vegalitecomponent'}
|
||||
<VegaLiteHtml
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'plotlycomponent'}
|
||||
<PlotlyHtml
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'plotlycomponentv2'}
|
||||
<PlotlyHtmlV2
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'scatterchartcomponent'}
|
||||
<AppScatterChart
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'piechartcomponent'}
|
||||
<AppPieChart
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'agchartscomponent'}
|
||||
<AppAgCharts
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'agchartscomponentee'}
|
||||
<AppAgCharts
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
license={component.license}
|
||||
ee={true}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'tablecomponent'}
|
||||
<AppTable
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
actionButtons={component.actionButtons}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dbexplorercomponent'}
|
||||
<AppDbExplorer
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
bind:initializing
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridcomponent'}
|
||||
<AppAggridTable
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
actionsOrder={component.actionsOrder ?? undefined}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridcomponentee'}
|
||||
<AppAggridTableEe
|
||||
license={component.license}
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
actionsOrder={component.actionsOrder ?? undefined}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridinfinitecomponent'}
|
||||
<AppAggridInfiniteTable
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'aggridinfinitecomponentee'}
|
||||
<AppAggridInfiniteTableEe
|
||||
license={component.license}
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
actions={component.actions ?? []}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'textcomponent'}
|
||||
<AppText
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
bind:editorMode={inlineEditorOpened}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'buttoncomponent'}
|
||||
<AppButton
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'downloadcomponent'}
|
||||
<AppDownload
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
|
||||
<AppSelect
|
||||
recomputeIds={component.recomputeIds}
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
onSelect={component.onSelect}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'userresourcecomponent'}
|
||||
<AppUserResource
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'multiselectcomponent'}
|
||||
<AppMultiSelect
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'multiselectcomponentv2'}
|
||||
<AppMultiSelectV2
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'formcomponent'}
|
||||
<AppForm
|
||||
id={component.id}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'formbuttoncomponent'}
|
||||
<AppFormButton
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
componentInput={component.componentInput}
|
||||
recomputeIds={component.recomputeIds}
|
||||
bind:errorHandledByComponent
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
recomputeIds={component.recomputeIds}
|
||||
onToggle={component.onToggle}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'textinputcomponent'}
|
||||
<AppTextInput
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'quillcomponent'}
|
||||
<AppQuillEditor id={component.id} configuration={component.configuration} {render} />
|
||||
{:else if component.type === 'textareainputcomponent'}
|
||||
<AppTextInput
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
inputType="textarea"
|
||||
appCssKey="textareainputcomponent"
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'emailinputcomponent'}
|
||||
<AppTextInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="email"
|
||||
appCssKey="emailinputcomponent"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'passwordinputcomponent'}
|
||||
<AppTextInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="password"
|
||||
appCssKey="passwordinputcomponent"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dateinputcomponent'}
|
||||
<AppDateInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="date"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'timeinputcomponent'}
|
||||
<AppTimeInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'datetimeinputcomponent'}
|
||||
<AppDateTimeInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
inputType="date"
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'numberinputcomponent'}
|
||||
<AppNumberInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'currencycomponent'}
|
||||
<AppCurrencyInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'slidercomponent'}
|
||||
<AppSliderInputs
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dateslidercomponent'}
|
||||
<AppDateSliderInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'horizontaldividercomponent'}
|
||||
<AppDivider
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
position="horizontal"
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'verticaldividercomponent'}
|
||||
<AppDivider
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
position="vertical"
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'rangecomponent'}
|
||||
<AppRangeInput
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'tabscomponent' && component.tabs}
|
||||
<AppTabs
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
tabs={component.tabs}
|
||||
disabledTabs={component.disabledTabs}
|
||||
onTabChange={component.onTabChange}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'steppercomponent' && component.tabs}
|
||||
<AppStepper
|
||||
id={component.id}
|
||||
tabs={component.tabs}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
componentInput={component.componentInput}
|
||||
onNext={component.onNext}
|
||||
onPrevious={component.onPrevious}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'conditionalwrapper' && component.conditions}
|
||||
<AppConditionalWrapper
|
||||
id={component.id}
|
||||
conditions={component.conditions}
|
||||
customCss={component.customCss}
|
||||
onTabChange={component.onTabChange}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'containercomponent'}
|
||||
<AppContainer
|
||||
groupFields={component.groupFields}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'listcomponent'}
|
||||
<AppList
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
configuration={component.configuration}
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'verticalsplitpanescomponent'}
|
||||
<AppSplitpanes
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
panes={component.panes}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'horizontalsplitpanescomponent'}
|
||||
<AppSplitpanes
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
panes={component.panes}
|
||||
{componentContainerHeight}
|
||||
horizontal={true}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'iconcomponent'}
|
||||
<AppIcon
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'fileinputcomponent'}
|
||||
<AppFileInput
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onFileChange={component.onFileChange}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 's3fileinputcomponent'}
|
||||
<AppS3FileInput
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onFileChange={component.onFileChange}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'imagecomponent'}
|
||||
<AppImage
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'drawercomponent'}
|
||||
<AppDrawer
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onOpenRecomputeIds={component.onOpenRecomputeIds}
|
||||
onCloseRecomputeIds={component.onCloseRecomputeIds}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'mapcomponent'}
|
||||
<AppMap
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'pdfcomponent'}
|
||||
<AppPdf
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'modalcomponent'}
|
||||
<AppModal
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
onOpenRecomputeIds={component.onOpenRecomputeIds}
|
||||
onCloseRecomputeIds={component.onCloseRecomputeIds}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'schemaformcomponent'}
|
||||
<AppSchemaForm
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{initializing}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selecttabcomponent'}
|
||||
<AppSelectTab
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selectstepcomponent'}
|
||||
<AppSelectStep
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'chartjscomponent'}
|
||||
<AppChartJs
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'chartjscomponentv2'}
|
||||
<AppChartJsV2
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
componentInput={component.componentInput}
|
||||
datasets={component.datasets}
|
||||
xData={component.xData}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'carousellistcomponent'}
|
||||
<AppCarouselList
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'accordionlistcomponent'}
|
||||
<AppAccordionList
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'statcomponent'}
|
||||
<AppStatCard
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'menucomponent'}
|
||||
<AppMenu
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
menuItems={component.menuItems}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'decisiontreecomponent' && component.nodes}
|
||||
<AppDecisionTree
|
||||
id={component.id}
|
||||
nodes={component.nodes}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'alertcomponent'}
|
||||
<AppAlert
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'navbarcomponent'}
|
||||
<AppNavbar
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
navbarItems={component.navbarItems}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'dateselectcomponent'}
|
||||
<AppDateSelect
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
customCss={component.customCss}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'jobiddisplaycomponent'}
|
||||
<AppDisplayComponentByJobId
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'recomputeallcomponent'}
|
||||
<AppRecomputeAll
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:initializing
|
||||
configuration={component.configuration}
|
||||
horizontalAlignment={component.horizontalAlignment}
|
||||
{render}
|
||||
/>
|
||||
{/if}
|
||||
@@ -0,0 +1,219 @@
|
||||
<script lang="ts" context="module">
|
||||
let outTimeout: NodeJS.Timeout | undefined = undefined
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppEditorContext, AppViewerContext } from '../../types'
|
||||
import ComponentHeader from '../ComponentHeader.svelte'
|
||||
import type { AppComponent } from './components'
|
||||
|
||||
import { Anchor } from 'lucide-svelte'
|
||||
import { findGridItemParentGrid, isContainer } from '../appUtils'
|
||||
import ComponentInner from './ComponentInner.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
export let locked: boolean = false
|
||||
export let fullHeight: boolean
|
||||
export let overlapped: string | undefined = undefined
|
||||
export let moveMode: string | undefined = undefined
|
||||
export let componentDraggedId: string | undefined = undefined
|
||||
export let render: boolean = false
|
||||
const { mode, app, hoverStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const editorContext = getContext<AppEditorContext>('AppEditorContext')
|
||||
const componentActive = editorContext?.componentActive
|
||||
|
||||
const movingcomponents = editorContext?.movingcomponents
|
||||
$: ismoving =
|
||||
movingcomponents != undefined && $mode == 'dnd' && $movingcomponents?.includes(component.id)
|
||||
|
||||
let initializing: boolean | undefined = undefined
|
||||
|
||||
let errorHandledByComponent: boolean = false
|
||||
let componentContainerHeight: number = 0
|
||||
let componentContainerWidth: number = 0
|
||||
|
||||
let inlineEditorOpened: boolean = false
|
||||
let showSkeleton = false
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
showSkeleton = true
|
||||
}, 100)
|
||||
})
|
||||
|
||||
function mouseOut() {
|
||||
outTimeout && clearTimeout(outTimeout)
|
||||
outTimeout = setTimeout(() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
// In order to avoid flickering when hovering over table actions,
|
||||
// we leave the actions to manage the hover state
|
||||
if ($hoverStore.startsWith(`${component.id}_`)) {
|
||||
return
|
||||
}
|
||||
|
||||
$hoverStore = undefined
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
function componentDraggedIsNotChild(componentDraggedId: string, componentId: string) {
|
||||
let parentGrid = findGridItemParentGrid($app, componentDraggedId)
|
||||
|
||||
return !parentGrid?.startsWith(`${componentId}-`)
|
||||
}
|
||||
|
||||
function areOnTheSameSubgrid(componentDraggedId: string, componentId: string) {
|
||||
return (
|
||||
findGridItemParentGrid($app, componentDraggedId) === findGridItemParentGrid($app, componentId)
|
||||
)
|
||||
}
|
||||
|
||||
let cachedComponentDraggedIsNotChild: boolean | undefined
|
||||
let cachedAreOnTheSameSubgrid: boolean | undefined
|
||||
|
||||
function updateCache(componentDraggedId: string | undefined) {
|
||||
if (componentDraggedId) {
|
||||
cachedComponentDraggedIsNotChild = componentDraggedIsNotChild(
|
||||
componentDraggedId,
|
||||
component.id
|
||||
)
|
||||
cachedAreOnTheSameSubgrid = areOnTheSameSubgrid(componentDraggedId, component.id)
|
||||
} else {
|
||||
cachedComponentDraggedIsNotChild = undefined
|
||||
cachedAreOnTheSameSubgrid = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$: updateCache(componentDraggedId)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
outTimeout && clearTimeout(outTimeout)
|
||||
if (component.id !== $hoverStore) {
|
||||
$hoverStore = component.id
|
||||
}
|
||||
}}
|
||||
on:mouseout|stopPropagation={mouseOut}
|
||||
class={twMerge(
|
||||
'h-full flex flex-col w-full component relative',
|
||||
initializing ? 'overflow-hidden h-0' : ''
|
||||
)}
|
||||
data-connection-button
|
||||
>
|
||||
{#if render}
|
||||
{#if locked && componentActive && $componentActive && moveMode === 'move' && componentDraggedId && componentDraggedId !== component.id && cachedAreOnTheSameSubgrid}
|
||||
<div
|
||||
class={twMerge('absolute inset-0 bg-locked center-center flex-col z-50', 'bg-locked-hover')}
|
||||
>
|
||||
<div class="bg-surface p-2 shadow-sm rounded-md flex center-center flex-col gap-2">
|
||||
<Anchor size={24} class="text-primary " />
|
||||
<div class="text-xs"> Anchored: The component cannot be moved. </div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if moveMode === 'insert' && isContainer(component.type) && componentDraggedId && componentDraggedId !== component.id && cachedComponentDraggedIsNotChild}
|
||||
<div
|
||||
class={twMerge(
|
||||
'absolute inset-0 flex-col rounded-md bg-blue-100 dark:bg-gray-800 bg-opacity-50',
|
||||
'outline-dashed outline-offset-2 outline-2 outline-blue-300 dark:outline-blue-700',
|
||||
overlapped === component?.id ? 'bg-draggedover dark:bg-draggedover-dark' : ''
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
{#if $mode !== 'preview'}
|
||||
<ComponentHeader
|
||||
on:mouseover={() => {
|
||||
outTimeout && clearTimeout(outTimeout)
|
||||
|
||||
if (component.id !== $hoverStore) {
|
||||
$hoverStore = component.id
|
||||
}
|
||||
}}
|
||||
hover={$hoverStore === component.id}
|
||||
{component}
|
||||
{selected}
|
||||
{fullHeight}
|
||||
connecting={$connectingInput.opened}
|
||||
on:lock
|
||||
on:expand
|
||||
on:fillHeight
|
||||
{locked}
|
||||
{inlineEditorOpened}
|
||||
hasInlineEditor={component.type === 'textcomponent' &&
|
||||
component.componentInput &&
|
||||
component.componentInput.type !== 'connected'}
|
||||
on:triggerInlineEditor={() => {
|
||||
inlineEditorOpened = !inlineEditorOpened
|
||||
}}
|
||||
{errorHandledByComponent}
|
||||
{componentContainerWidth}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if ismoving}
|
||||
<div class="absolute -top-8 w-40">
|
||||
<button
|
||||
class="border p-0.5 text-xs"
|
||||
on:click={() => {
|
||||
$movingcomponents = undefined
|
||||
}}
|
||||
>
|
||||
Cancel move
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<div
|
||||
class={twMerge(
|
||||
render ? 'h-full outline-1' : 'h-0 overflow-hidden',
|
||||
$mode === 'dnd' ? 'bg-surface/40' : '',
|
||||
$hoverStore === component.id && $mode !== 'preview'
|
||||
? $connectingInput.opened
|
||||
? 'outline outline-[#f8aa4b]'
|
||||
: 'outline outline-blue-400'
|
||||
: '',
|
||||
selected && $mode !== 'preview' ? 'outline outline-blue-600' : '',
|
||||
$mode != 'preview' ? 'cursor-pointer' : '',
|
||||
'relative z-auto',
|
||||
$app.css?.['app']?.['component']?.class,
|
||||
'wm-app-component',
|
||||
ismoving ? 'animate-pulse' : ''
|
||||
)}
|
||||
style={$app.css?.['app']?.['component']?.style}
|
||||
bind:clientHeight={componentContainerHeight}
|
||||
bind:clientWidth={componentContainerWidth}
|
||||
>
|
||||
<ComponentInner
|
||||
{component}
|
||||
{render}
|
||||
{componentContainerHeight}
|
||||
bind:initializing
|
||||
bind:errorHandledByComponent
|
||||
{inlineEditorOpened}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if initializing && render && showSkeleton}
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
if (component.id !== $hoverStore) {
|
||||
$hoverStore = component.id
|
||||
}
|
||||
}}
|
||||
on:mouseout|stopPropagation={() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
$hoverStore = undefined
|
||||
}
|
||||
}}
|
||||
class="absolute inset-0 center-center flex-col border animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms]"
|
||||
/>
|
||||
{/if}
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import JsonEditor from '$lib/components/JsonEditor.svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let code = JSON.stringify($app.lazyInitRequire)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-8" style="all:none;">
|
||||
<Alert type="info" title="Lazy mode for large apps performance">
|
||||
Lazy mode is a feature that allows you to lazy render components which is ideal for apps with
|
||||
many components where most are not directly visible to the user.
|
||||
<br />
|
||||
When lazy mode is enabled, components are not rendered until they are needed. This can significantly
|
||||
improve the performance of your app, especially on mobile devices.
|
||||
<br />
|
||||
You can enable lazy mode below, but you will need to declare the list of components whose initialization
|
||||
is expected to see initialized before the initial refresh of the app happens.
|
||||
</Alert>
|
||||
|
||||
<Toggle
|
||||
label="Lazy mode"
|
||||
checked={Boolean($app.lazyInitRequire != undefined)}
|
||||
on:change={(e) => {
|
||||
$app.lazyInitRequire = e.detail ? [] : undefined
|
||||
code = JSON.stringify($app.lazyInitRequire)
|
||||
}}
|
||||
options={{
|
||||
right: 'Lazy mode'
|
||||
}}
|
||||
/>
|
||||
|
||||
<Section label="Component ids to wait the initialization of before the initial refresh">
|
||||
{#if $app.lazyInitRequire != undefined}
|
||||
<JsonEditor bind:value={$app.lazyInitRequire} {code} />
|
||||
<span class="text-tertiary text-xs">
|
||||
{'e.g: ["a", "b"]'}, no need to put background runnables ids
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-tertiary"
|
||||
>Without lazy mode, all components' initialization will be waited on the initial refresh.</span
|
||||
>
|
||||
{/if}
|
||||
</Section>
|
||||
</div>
|
||||
@@ -9,6 +9,8 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { columnConfiguration, WIDE_GRID_COLUMNS } from '../gridUtils'
|
||||
|
||||
import gridHelp from './utils/helper'
|
||||
import type { AppViewerContext, GridItem } from '../types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -41,7 +43,7 @@
|
||||
|
||||
export let items: FilledItem<T>[]
|
||||
export let rowHeight: number = ROW_HEIGHT
|
||||
export let cols: [number, number][]
|
||||
|
||||
export let gap = [ROW_GAP_X, ROW_GAP_Y]
|
||||
export let throttleUpdate = 100
|
||||
export let throttleResize = 100
|
||||
@@ -53,7 +55,10 @@
|
||||
export let root: boolean = false
|
||||
export let parentWidth: number | undefined = undefined
|
||||
export let disableMove: boolean = false
|
||||
let getComputedCols
|
||||
const cols = columnConfiguration
|
||||
|
||||
let getComputedCols: 3 | 12 | undefined =
|
||||
$app.mobileViewOnSmallerScreens == false ? WIDE_GRID_COLUMNS : undefined
|
||||
let container
|
||||
|
||||
$: [gapX, gapY] = gap
|
||||
@@ -73,16 +78,22 @@
|
||||
})
|
||||
}, throttleUpdate)
|
||||
|
||||
let mounted = false
|
||||
|
||||
onMount(() => {
|
||||
const sizeObserver = new ResizeObserver((entries) => {
|
||||
requestAnimationFrame(() => {
|
||||
let width = entries[0].contentRect.width
|
||||
|
||||
if (width === containerWidth) return
|
||||
if (width === containerWidth) {
|
||||
mounted = true
|
||||
return
|
||||
}
|
||||
|
||||
getComputedCols = getColumn(parentWidth ?? width, cols)
|
||||
|
||||
xPerPx = width / getComputedCols
|
||||
if ($app.mobileViewOnSmallerScreens != false || !getComputedCols) {
|
||||
getComputedCols = getColumn(parentWidth ?? width, cols)
|
||||
}
|
||||
xPerPx = width / getComputedCols!
|
||||
|
||||
if (!containerWidth) {
|
||||
items = specifyUndefinedColumns(items, getComputedCols, cols)
|
||||
@@ -97,6 +108,7 @@
|
||||
}
|
||||
|
||||
containerWidth = width
|
||||
mounted = true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -106,7 +118,7 @@
|
||||
})
|
||||
|
||||
let sortedItems: FilledItem<T>[] = []
|
||||
$: sortedItems = JSON.parse(JSON.stringify(items)).sort((a, b) => a.id.localeCompare(b.id))
|
||||
$: sortedItems = smartCopy(items).sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
let resizing: boolean = false
|
||||
|
||||
@@ -123,21 +135,31 @@
|
||||
|
||||
let initItems: FilledItem<T>[] | undefined = undefined
|
||||
|
||||
function smartCopy(items: FilledItem<T>[]) {
|
||||
return getComputedCols != undefined
|
||||
? items.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
[getComputedCols as number]: structuredClone(item[getComputedCols as number])
|
||||
}
|
||||
})
|
||||
: []
|
||||
}
|
||||
const updateMatrix = ({ detail }) => {
|
||||
let isPointerUp = detail.isPointerUp
|
||||
let citems: FilledItem<T>[]
|
||||
if (isPointerUp) {
|
||||
try {
|
||||
citems = JSON.parse(JSON.stringify(initItems))
|
||||
} catch (e) {
|
||||
citems = JSON.parse(JSON.stringify(sortedItems))
|
||||
if (initItems == undefined) {
|
||||
citems = smartCopy(sortedItems)
|
||||
} else {
|
||||
citems = smartCopy(initItems)
|
||||
}
|
||||
initItems = undefined
|
||||
} else {
|
||||
if (initItems == undefined) {
|
||||
initItems = JSON.parse(JSON.stringify(sortedItems))
|
||||
initItems = smartCopy(sortedItems)
|
||||
}
|
||||
citems = JSON.parse(JSON.stringify(initItems))
|
||||
citems = smartCopy(initItems)
|
||||
}
|
||||
let nselectedIds = selectedIds ?? []
|
||||
if (detail.id && !selectedIds?.includes(detail.id)) {
|
||||
@@ -147,7 +169,7 @@
|
||||
for (let id of nselectedIds) {
|
||||
let activeItem = getItemById(id, sortedItems)
|
||||
|
||||
if (activeItem) {
|
||||
if (activeItem && getComputedCols) {
|
||||
activeItem = {
|
||||
...activeItem,
|
||||
[getComputedCols]: {
|
||||
@@ -202,11 +224,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (isPointerUp) {
|
||||
dispatch(
|
||||
'redraw',
|
||||
sortGridItemsPosition(JSON.parse(JSON.stringify(sortedItems)), getComputedCols)
|
||||
)
|
||||
if (isPointerUp && getComputedCols) {
|
||||
dispatch('redraw', sortGridItemsPosition(smartCopy(sortedItems), getComputedCols))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +273,7 @@
|
||||
export function handleMove({ detail }) {
|
||||
Object.entries(moveResizes).forEach(([id, moveResize]) => {
|
||||
if (selectedIds?.includes(id)) {
|
||||
moveResize?.updateMove(JSON.parse(JSON.stringify(detail.cordDiff)), detail.eventY)
|
||||
moveResize?.updateMove(structuredClone(detail.cordDiff), detail.eventY)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -275,7 +294,7 @@
|
||||
) {
|
||||
const draggedItem = sortedItems.find((item) => item.id === $componentDraggedIdStore)
|
||||
|
||||
if (draggedItem) {
|
||||
if (draggedItem && getComputedCols) {
|
||||
draggedItem[getComputedCols].x = detail.shadow.x
|
||||
draggedItem[getComputedCols].y = detail.shadow.y
|
||||
}
|
||||
@@ -391,33 +410,33 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if xPerPx > 0 && getComputedCols}
|
||||
{#each sortedItems as item (item.id)}
|
||||
{#if item[getComputedCols] != undefined}
|
||||
{#if $isCtrlOrMetaPressedStore && item.id === $overlappedStore && $componentDraggedIdStore && $componentDraggedParentIdStore !== item.id && $fakeShadowStore}
|
||||
{@const columnGap = gapX}
|
||||
<!-- gap between the columns in px -->
|
||||
{@const containerBorder = 0.5 * 16}
|
||||
<!-- 0.5rem converted to px (1rem = 16px) -->
|
||||
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
|
||||
<!-- subtract borders -->
|
||||
{@const availableWidth = gridTotalWidth - 11 * columnGap}
|
||||
<!-- subtract gaps between the 12 columns (11 gaps) -->
|
||||
{@const columnWidthPx = availableWidth / 12}
|
||||
<!-- divide by the number of columns -->
|
||||
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
|
||||
|
||||
{#each sortedItems as item (item.id)}
|
||||
{#if item[getComputedCols] != undefined}
|
||||
{#if $isCtrlOrMetaPressedStore && item.id === $overlappedStore && $componentDraggedIdStore && $componentDraggedParentIdStore !== item.id && $fakeShadowStore}
|
||||
{@const columnGap = gapX}
|
||||
<!-- gap between the columns in px -->
|
||||
{@const containerBorder = 0.5 * 16}
|
||||
<!-- 0.5rem converted to px (1rem = 16px) -->
|
||||
{@const gridTotalWidth = containerWidth ? containerWidth - 2 * containerBorder : 0}
|
||||
<!-- subtract borders -->
|
||||
{@const availableWidth = gridTotalWidth - 11 * columnGap}
|
||||
<!-- subtract gaps between the 12 columns (11 gaps) -->
|
||||
{@const columnWidthPx = availableWidth / 12}
|
||||
<!-- divide by the number of columns -->
|
||||
{@const maxX = Math.floor(availableWidth / columnWidthPx) - $fakeShadowStore.w}
|
||||
|
||||
<div
|
||||
class="absolute"
|
||||
style={`
|
||||
<div
|
||||
class="absolute"
|
||||
style={`
|
||||
left: ${item[getComputedCols].x * xPerPx + gapX}px;
|
||||
top: ${item[getComputedCols].y * yPerPx + gapY}px;
|
||||
`}
|
||||
>
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
class={twMerge('absolute transition-all duration-[50ms] bg-blue-300')}
|
||||
style={`
|
||||
>
|
||||
<div class="relative h-full w-full">
|
||||
<div
|
||||
class={twMerge('absolute transition-all duration-[50ms] bg-blue-300')}
|
||||
style={`
|
||||
left: calc(${
|
||||
Math.min($fakeShadowStore.x, maxX) * $fakeShadowStore.xPerPx + gapX
|
||||
}px + 0.5rem + ${getDeltaXByComponent(item.data['type'])});
|
||||
@@ -427,68 +446,73 @@
|
||||
width: ${$fakeShadowStore.w * $fakeShadowStore.xPerPx - gapX * 2}px;
|
||||
height: ${$fakeShadowStore.h * $fakeShadowStore.yPerPx - gapY * 2}px;
|
||||
`}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<MoveResize
|
||||
on:initmove={() => handleInitMove(item.id)}
|
||||
on:move={handleMove}
|
||||
bind:shadow={shadows[item.id]}
|
||||
bind:this={moveResizes[item.id]}
|
||||
on:repaint={handleRepaint}
|
||||
on:resizeStart={() => (resizing = true)}
|
||||
on:resizeEnd={() => (resizing = false)}
|
||||
onTop={Boolean(allIdsInPath?.includes(item.id))}
|
||||
id={item.id}
|
||||
{xPerPx}
|
||||
{yPerPx}
|
||||
fakeShadow={$fakeShadowStore}
|
||||
on:dropped={(e) => {
|
||||
$componentDraggedIdStore = undefined
|
||||
$componentDraggedParentIdStore = undefined
|
||||
$overlappedStore = undefined
|
||||
$fakeShadowStore = undefined
|
||||
lastDetail = undefined
|
||||
|
||||
if (!$isCtrlOrMetaPressedStore) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('dropped', e.detail)
|
||||
}}
|
||||
width={xPerPx == 0
|
||||
? 0
|
||||
: Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx -
|
||||
gapX * 2}
|
||||
height={(item[getComputedCols] && item[getComputedCols].h) * yPerPx - gapY * 2}
|
||||
top={(item[getComputedCols] && item[getComputedCols].y) * yPerPx + gapY}
|
||||
left={(item[getComputedCols] && item[getComputedCols].x) * xPerPx + gapX}
|
||||
item={item[getComputedCols]}
|
||||
cols={getComputedCols}
|
||||
{gapX}
|
||||
{gapY}
|
||||
{sensor}
|
||||
container={scroller}
|
||||
nativeContainer={container}
|
||||
overlapped={$overlappedStore}
|
||||
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
|
||||
type={item.data['type']}
|
||||
{disableMove}
|
||||
>
|
||||
{#if item[getComputedCols]}
|
||||
<slot
|
||||
dataItem={item}
|
||||
hidden={false}
|
||||
overlapped={$overlappedStore}
|
||||
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
|
||||
componentDraggedId={$componentDraggedIdStore}
|
||||
/>
|
||||
{/if}
|
||||
</MoveResize>
|
||||
{/if}
|
||||
{/each}
|
||||
<MoveResize
|
||||
{mounted}
|
||||
on:initmove={() => handleInitMove(item.id)}
|
||||
on:move={handleMove}
|
||||
bind:shadow={shadows[item.id]}
|
||||
bind:this={moveResizes[item.id]}
|
||||
on:repaint={handleRepaint}
|
||||
on:resizeStart={() => (resizing = true)}
|
||||
on:resizeEnd={() => (resizing = false)}
|
||||
onTop={Boolean(allIdsInPath?.includes(item.id))}
|
||||
id={item.id}
|
||||
{xPerPx}
|
||||
{yPerPx}
|
||||
fakeShadow={$fakeShadowStore}
|
||||
on:dropped={(e) => {
|
||||
$componentDraggedIdStore = undefined
|
||||
$componentDraggedParentIdStore = undefined
|
||||
$overlappedStore = undefined
|
||||
$fakeShadowStore = undefined
|
||||
lastDetail = undefined
|
||||
|
||||
if (!$isCtrlOrMetaPressedStore) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('dropped', e.detail)
|
||||
}}
|
||||
width={xPerPx == 0
|
||||
? 0
|
||||
: Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx -
|
||||
gapX * 2}
|
||||
height={(item[getComputedCols] && item[getComputedCols].h) * yPerPx - gapY * 2}
|
||||
top={(item[getComputedCols] && item[getComputedCols].y) * yPerPx + gapY}
|
||||
left={(item[getComputedCols] && item[getComputedCols].x) * xPerPx + gapX}
|
||||
item={item[getComputedCols]}
|
||||
cols={getComputedCols}
|
||||
{gapX}
|
||||
{gapY}
|
||||
{sensor}
|
||||
container={scroller}
|
||||
nativeContainer={container}
|
||||
overlapped={$overlappedStore}
|
||||
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
|
||||
type={item.data['type']}
|
||||
{disableMove}
|
||||
>
|
||||
{#if item[getComputedCols]}
|
||||
<slot
|
||||
dataItem={item}
|
||||
hidden={false}
|
||||
overlapped={$overlappedStore}
|
||||
moveMode={$isCtrlOrMetaPressedStore ? 'insert' : 'move'}
|
||||
componentDraggedId={$componentDraggedIdStore}
|
||||
/>
|
||||
{/if}
|
||||
</MoveResize>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if root}
|
||||
<div
|
||||
class="h-full w-full flex-col animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms]"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
export let type: string | undefined = undefined
|
||||
export let fakeShadow: GridShadow | undefined = undefined
|
||||
export let disableMove: boolean = true
|
||||
export let mounted: boolean = false
|
||||
|
||||
const ctx = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { mode, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
@@ -504,7 +505,7 @@
|
||||
: trans
|
||||
? `transform: translate(${cordDiff.x}px, ${cordDiff.y}px); position:absolute; transition: width 0.2s, height 0.2s;`
|
||||
: `${
|
||||
xPerPx > 0 ? 'transition: transform 0.1s, opacity 0.1s;' : ''
|
||||
xPerPx > 0 && mounted ? 'transition: transform 0.1s, opacity 0.1s;' : ''
|
||||
} transform: translate(${left}px, ${top}px); `} "
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -139,13 +139,13 @@ export type HiddenRunnable = {
|
||||
|
||||
export type AppTheme =
|
||||
| {
|
||||
type: 'path'
|
||||
path: string
|
||||
}
|
||||
type: 'path'
|
||||
path: string
|
||||
}
|
||||
| {
|
||||
type: 'inlined'
|
||||
css: string
|
||||
}
|
||||
type: 'inlined'
|
||||
css: string
|
||||
}
|
||||
|
||||
export type App = {
|
||||
grid: GridItem[]
|
||||
@@ -162,6 +162,7 @@ export type App = {
|
||||
css?: Partial<Record<AppCssItemName, Record<string, ComponentCssProperty>>>
|
||||
subgrids?: Record<string, GridItem[]>
|
||||
theme: AppTheme | undefined
|
||||
lazyInitRequire?: string[] | undefined
|
||||
hideLegacyTopBar?: boolean | undefined
|
||||
mobileViewOnSmallerScreens?: boolean | undefined
|
||||
version?: number
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
Terminal,
|
||||
Webhook,
|
||||
Unplug,
|
||||
PlugZap
|
||||
PlugZap,
|
||||
|
||||
Database
|
||||
|
||||
} from 'lucide-svelte'
|
||||
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
@@ -69,7 +72,7 @@
|
||||
</Tab>
|
||||
<Tab value="postgres">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Unplug size={12} />
|
||||
<Database size={12} />
|
||||
Postgres
|
||||
</span>
|
||||
</Tab>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
|
||||
let name: string = ''
|
||||
export let customName: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
<div class="flex flex-row gap-2 p-2 rounded-md">
|
||||
<input
|
||||
bind:value={name}
|
||||
placeholder="Field name"
|
||||
placeholder={`${customName ?? 'Field'} name`}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
addField()
|
||||
@@ -44,7 +45,7 @@
|
||||
disabled={!name}
|
||||
shortCut={{ Icon: CornerDownLeft, withoutModifier: true }}
|
||||
>
|
||||
Add field
|
||||
Add {customName ? customName.toLowerCase() : 'field'}
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -65,6 +65,8 @@ export function captureTriggerKindToTriggerKind(kind: CaptureTriggerKind): Trigg
|
||||
return 'kafka'
|
||||
case 'nats':
|
||||
return 'nats'
|
||||
case 'postgres':
|
||||
return 'postgres'
|
||||
default:
|
||||
throw new Error(`Unknown CaptureTriggerKind: ${kind}`)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Webhook, Route, Unplug, Mail, Plus } from 'lucide-svelte'
|
||||
import { Webhook, Route, Unplug, Mail, Plus, Database } from 'lucide-svelte'
|
||||
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { type CaptureTriggerKind } from '$lib/gen'
|
||||
@@ -82,6 +82,15 @@
|
||||
<p class="text-xs text-secondary">Websocket</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="hover:bg-surface-hover p-2 transition-colors duration-150"
|
||||
on:click={() => handleClick('postgres')}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<Database size={16} />
|
||||
<p class="text-xs text-secondary">Postgres</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="hover:bg-surface-hover p-2 transition-colors duration-150"
|
||||
on:click={() => handleClick('email')}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
import type { CaptureInfo } from './CaptureSection.svelte'
|
||||
import CaptureTable from './CaptureTable.svelte'
|
||||
import NatsTriggersConfigSection from './nats/NatsTriggersConfigSection.svelte'
|
||||
import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte'
|
||||
import { invalidRelations } from './postgres/utils'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
@@ -26,16 +28,37 @@
|
||||
export let args: Record<string, any> = {}
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
|
||||
export async function setConfig() {
|
||||
await CaptureService.setCaptureConfig({
|
||||
requestBody: {
|
||||
trigger_kind: captureType,
|
||||
path,
|
||||
is_flow: isFlow,
|
||||
trigger_config: args && Object.keys(args).length > 0 ? args : undefined
|
||||
},
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
export async function setConfig(): Promise<boolean> {
|
||||
if (captureType === 'postgres') {
|
||||
if (!args?.publication?.table_to_track) {
|
||||
sendUserToast('Table to track must be set', true)
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
invalidRelations(args.publication.table_to_track, {
|
||||
showError: true,
|
||||
trackSchemaTableError: true
|
||||
}) === true
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
await CaptureService.setCaptureConfig({
|
||||
requestBody: {
|
||||
trigger_kind: captureType,
|
||||
path,
|
||||
is_flow: isFlow,
|
||||
trigger_config: args && Object.keys(args).length > 0 ? args : undefined
|
||||
},
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
sendUserToast(error.body, true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let captureActive = false
|
||||
@@ -55,7 +78,10 @@
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
if ((captureType === 'websocket' || captureType === 'kafka') && captureActive) {
|
||||
if (
|
||||
(captureType === 'postgres' || captureType === 'websocket' || captureType === 'kafka') &&
|
||||
captureActive
|
||||
) {
|
||||
const config = captureConfigs[captureType]
|
||||
if (config && config.error) {
|
||||
const serverEnabled = getServerEnabled(config)
|
||||
@@ -114,24 +140,22 @@
|
||||
if (captureActive || e.detail.disableOnly) {
|
||||
captureActive = false
|
||||
} else {
|
||||
await setConfig()
|
||||
capture()
|
||||
const configSet = await setConfig()
|
||||
|
||||
if (configSet) {
|
||||
capture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let config: CaptureConfig | undefined
|
||||
$: config = captureConfigs[captureType]
|
||||
|
||||
let cloudDisabled =
|
||||
(captureType === 'websocket' || captureType === 'kafka' || captureType === 'nats') &&
|
||||
isCloudHosted()
|
||||
const streamingTrigger = ['postgres', 'websocket', 'kafka', 'nats']
|
||||
let cloudDisabled = streamingTrigger.includes(captureType) && isCloudHosted()
|
||||
|
||||
function updateConnectionInfo(config: CaptureConfig | undefined, captureActive: boolean) {
|
||||
if (
|
||||
(captureType === 'websocket' || captureType === 'kafka' || captureType === 'nats') &&
|
||||
config &&
|
||||
captureActive
|
||||
) {
|
||||
if (streamingTrigger.includes(captureType) && config && captureActive) {
|
||||
const serverEnabled = getServerEnabled(config)
|
||||
const connected = serverEnabled && !config.error
|
||||
const message = connected
|
||||
@@ -181,6 +205,21 @@
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'postgres'}
|
||||
<PostgresEditorConfigSection
|
||||
bind:postgres_resource_path={args.postgres_resource_path}
|
||||
bind:publication={args.publication}
|
||||
{showCapture}
|
||||
{captureInfo}
|
||||
can_write={true}
|
||||
headless={true}
|
||||
bind:captureTable
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'webhook'}
|
||||
<WebhooksConfigSection
|
||||
{isFlow}
|
||||
|
||||
@@ -3,24 +3,27 @@
|
||||
CancelablePromise,
|
||||
KafkaTriggerService,
|
||||
NatsTriggerService,
|
||||
PostgresTriggerService,
|
||||
WebsocketTriggerService
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
|
||||
export let kind: 'websocket' | 'nats' | 'kafka'
|
||||
export let kind: 'websocket' | 'nats' | 'kafka' | 'postgres'
|
||||
export let args: Record<string, any>
|
||||
export let noButton = false
|
||||
export let testLoading: boolean = false
|
||||
|
||||
const kindToName: { [key: string]: string } = {
|
||||
websocket: 'WebSocket',
|
||||
nats: 'NATS server(s)',
|
||||
kafka: 'Kafka broker(s)'
|
||||
kafka: 'Kafka broker(s)',
|
||||
postgres: 'Postgres'
|
||||
}
|
||||
|
||||
let testLoading: boolean = false
|
||||
let promise: CancelablePromise<any> | null = null
|
||||
async function testTriggerConnection() {
|
||||
export async function testTriggerConnection() {
|
||||
if (testLoading) {
|
||||
promise?.cancel()
|
||||
return
|
||||
@@ -43,6 +46,11 @@
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: args as any
|
||||
})
|
||||
} else if (kind === 'postgres') {
|
||||
promise = PostgresTriggerService.testPostgresConnection({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: args as any
|
||||
})
|
||||
}
|
||||
await promise
|
||||
sendUserToast(`Successfully connected to ${kindToName[kind]}`)
|
||||
@@ -56,16 +64,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row justify-end mt-1">
|
||||
<Button
|
||||
spacingSize="sm"
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={testTriggerConnection}
|
||||
loading={testLoading}
|
||||
clickableWhileLoading
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
</div>
|
||||
{#if !noButton}
|
||||
<div class="flex flex-row justify-end mt-1">
|
||||
<Button
|
||||
spacingSize="sm"
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={testTriggerConnection}
|
||||
loading={testLoading}
|
||||
clickableWhileLoading
|
||||
>
|
||||
Test connection
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -140,7 +140,18 @@
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'postgres'}
|
||||
<div class="p-4">
|
||||
<PostgresTriggersPanel {newItem} path={currentPath} {isFlow} />
|
||||
<PostgresTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
isEditor={true}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats'}
|
||||
<div class="p-4 flex flex-col gap-2">
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
webhook: 'Webhook',
|
||||
kafka: '+ New Kafka trigger',
|
||||
email: 'Email trigger',
|
||||
nats: '+ New NATS trigger'
|
||||
nats: '+ New NATS trigger',
|
||||
postgres: '+ New Postgres trigger'
|
||||
}
|
||||
|
||||
const { captureOn } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import EmailTriggerConfigSection from '../details/EmailTriggerConfigSection.svelte'
|
||||
import KafkaTriggersConfigSection from './kafka/KafkaTriggersConfigSection.svelte'
|
||||
import NatsTriggersConfigSection from './nats/NatsTriggersConfigSection.svelte'
|
||||
import PostgresEditorConfigSection from './postgres/PostgresEditorConfigSection.svelte'
|
||||
|
||||
export let triggerType: CaptureTriggerKind = 'webhook'
|
||||
export let cloudDisabled: boolean = false
|
||||
@@ -30,6 +31,14 @@
|
||||
bind:url_runnable_args={args.url_runnable_args}
|
||||
showCapture={false}
|
||||
/>
|
||||
{:else if triggerType === 'postgres'}
|
||||
<PostgresEditorConfigSection
|
||||
can_write={true}
|
||||
headless={true}
|
||||
showCapture={false}
|
||||
bind:publication={args.publication}
|
||||
bind:postgres_resource_path={args.postgres_resource_path}
|
||||
/>
|
||||
{:else if triggerType === 'webhook'}
|
||||
<WebhooksConfigSection
|
||||
{isFlow}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { PostgresTriggerService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { emptyString } from '$lib/utils'
|
||||
|
||||
let loadingConfiguration = false
|
||||
|
||||
const checkDatabaseConfiguration = async () => {
|
||||
if (emptyString(postgres_resource_path)) {
|
||||
sendUserToast('You must first pick a database resource', true)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const invalidConfig = !(await PostgresTriggerService.isValidPostgresConfiguration({
|
||||
workspace: $workspaceStore!,
|
||||
path: postgres_resource_path
|
||||
}))
|
||||
|
||||
let msg = 'Database is in logical mode. Triggers can be used.'
|
||||
|
||||
if (invalidConfig) {
|
||||
msg =
|
||||
'Database is NOT in logical mode. Triggers cannot be used. Refer to the PostgreSQL documentation for configuration requirements.'
|
||||
}
|
||||
|
||||
sendUserToast(msg, invalidConfig)
|
||||
} catch (error) {
|
||||
sendUserToast(error.body, true)
|
||||
}
|
||||
|
||||
loadingConfiguration = false
|
||||
}
|
||||
|
||||
const checkConnectionAndDatabaseConfiguration = async () => {
|
||||
try {
|
||||
loadingConfiguration = true
|
||||
if (checkConnection) {
|
||||
await checkConnection()
|
||||
}
|
||||
await checkDatabaseConfiguration()
|
||||
} catch (error) {
|
||||
sendUserToast(error.body, true)
|
||||
}
|
||||
loadingConfiguration = false
|
||||
}
|
||||
|
||||
export let can_write: boolean
|
||||
export let postgres_resource_path: string
|
||||
export let checkConnection: any | undefined = undefined
|
||||
|
||||
console.log('dbg check connection', checkConnection)
|
||||
</script>
|
||||
|
||||
{#if postgres_resource_path}
|
||||
<div class="flex flex-col justify-end mt-1 gap-2">
|
||||
<Button
|
||||
disabled={!can_write}
|
||||
loading={loadingConfiguration}
|
||||
on:click={checkConnectionAndDatabaseConfiguration}
|
||||
size="xs"
|
||||
color="light"
|
||||
spacingSize="sm"
|
||||
variant="border"
|
||||
>
|
||||
{`Check database configuration ${checkConnection ? 'and connection' : ''}`}
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/postgres_triggers#requirements"
|
||||
>
|
||||
<p class="text-sm">
|
||||
Verifies whether the database is configured with the required <strong>settings</strong>.
|
||||
{checkConnection && 'Also checks whether the connection to the database is working.'}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user