Compare commits
1 Commits
v1.517.0
...
win-1465-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cb5660057 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,7 +7,7 @@ CaddyfileRemoteMalo
|
||||
*.swp
|
||||
**/.idea/
|
||||
.direnv
|
||||
/.vscode
|
||||
.vscode
|
||||
.dev-docker-wrapper*
|
||||
backend/.minio-data
|
||||
.aider*
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,32 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.517.0](https://github.com/windmill-labs/windmill/compare/v1.516.0...v1.517.0) (2025-07-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** wmill-lock.yaml v2 for easier git merge diffs ([ef3e235](https://github.com/windmill-labs/windmill/commit/ef3e2353a76d096847b3f10b1daa6767fc4baa0d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use with_capacity back presusre for tantivy directory multipart writes ([#6313](https://github.com/windmill-labs/windmill/issues/6313)) ([8887707](https://github.com/windmill-labs/windmill/commit/8887707d41456889371e471c996773e605088a88))
|
||||
|
||||
## [1.516.0](https://github.com/windmill-labs/windmill/compare/v1.515.1...v1.516.0) (2025-07-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add CA certificate update at startup via environment variable ([#6280](https://github.com/windmill-labs/windmill/issues/6280)) ([a460e13](https://github.com/windmill-labs/windmill/commit/a460e131c71a0105fb14812ea2fabaa6bea1e0df))
|
||||
* prevent too large results (>500Mb) from OOMing database ([4b9683f](https://github.com/windmill-labs/windmill/commit/4b9683f1462e9c8a577cc9e65a79fcdcd3894da0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* indexer collection of job logs before indexing ([#6300](https://github.com/windmill-labs/windmill/issues/6300)) ([77c8f17](https://github.com/windmill-labs/windmill/commit/77c8f17fdf88821951af36786e28aed9a270d476))
|
||||
* no process relative imports for scripts with codebase ([576156b](https://github.com/windmill-labs/windmill/commit/576156b0cc89c8a6ccb94234c59307ab8c41fed4))
|
||||
* sqs oidc authentication disconnect [#6307](https://github.com/windmill-labs/windmill/issues/6307) ([993e809](https://github.com/windmill-labs/windmill/commit/993e80955b23098d7075ed5279e3f18cd8a633b9))
|
||||
|
||||
## [1.515.1](https://github.com/windmill-labs/windmill/compare/v1.515.0...v1.515.1) (2025-07-29)
|
||||
|
||||
|
||||
|
||||
@@ -364,8 +364,6 @@ you to have it being synced automatically everyday.
|
||||
| DISABLE_RESPONSE_LOGS | false | Disable response logs | Server |
|
||||
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmins can create new workspaces | Server |
|
||||
| MIN_FREE_DISK_SPACE_MB | 15000 | Minimum amount of free space on worker. Sends critical alert if worker has less free space. | Worker |
|
||||
| RUN_UPDATE_CA_CERTIFICATE_AT_START | false | If true, runs CA certificate update command at startup before other initialization | All |
|
||||
| RUN_UPDATE_CA_CERTIFICATE_PATH | /usr/sbin/update-ca-certificates | Path to the CA certificate update command/script to run when RUN_UPDATE_CA_CERTIFICATE_AT_START is true | All |
|
||||
|
||||
## Run a local dev setup
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO deployment_metadata (workspace_id, path, flow_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path) DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
|
||||
"query": "INSERT INTO deployment_metadata (workspace_id, path, flow_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -14,5 +14,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "434d8dfbc25cf7e92de51d763d3a2904ccc2e95ecc3d90b43a6394a7bb4d26ab"
|
||||
"hash": "4968e9edac534657c808b891cbf93c8c0a57f93b7b445171b1cc3f4428ee6e53"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5) \n ON CONFLICT (workspace_id, path) DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
|
||||
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -14,5 +14,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e77fcf4e0d58855542605d13177df61671334418820ca942b442adfab413cbae"
|
||||
"hash": "6bb4fab9e74c421fb30cfd856a1a9b44c76f1cf1485ceba50aeede9daec94c01"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH job_sizes AS (\n SELECT \n cj.id,\n cj.workspace_id,\n cj.parent_job,\n cj.created_by,\n cj.duration_ms,\n cj.success,\n cj.script_hash,\n cj.script_path,\n cj.args,\n cj.result,\n cj.deleted,\n cj.canceled,\n cj.canceled_by,\n cj.canceled_reason,\n cj.job_kind,\n cj.schedule_path,\n cj.permissioned_as,\n cj.is_flow_step,\n cj.language,\n cj.is_skipped,\n cj.email,\n cj.visible_to_owner,\n cj.mem_peak,\n cj.tag,\n cj.created_at,\n cj.started_at,\n job_logs.logs,\n job_logs.log_offset,\n job_logs.log_file_index,\n -- Estimate size in bytes based on actual data characteristics\n (36 + -- UUID\n COALESCE(LENGTH(cj.workspace_id), 0) + \n COALESCE(LENGTH(cj.created_by), 0) + \n COALESCE(LENGTH(cj.script_path), 0) + \n COALESCE(LENGTH(cj.args::text), 0) + \n COALESCE(LENGTH(cj.result::text), 0) + \n COALESCE(LENGTH(job_logs.logs), 0) + \n 200) AS estimated_size_bytes -- Other fields overhead\n FROM v2_as_completed_job AS cj\n LEFT JOIN job_logs ON cj.id = job_logs.job_id\n WHERE cj.created_at < $1\n ORDER BY created_at ASC\n LIMIT 5000\n ),\n cumulative_sizes AS (\n SELECT \n *,\n SUM(estimated_size_bytes) OVER (\n ORDER BY created_at ASC \n ROWS UNBOUNDED PRECEDING\n ) AS cumulative_size_bytes,\n ROW_NUMBER() OVER (ORDER BY created_at ASC) AS row_num\n FROM job_sizes\n )\n SELECT\n id AS \"id!\",\n workspace_id AS \"workspace_id!\",\n parent_job,\n created_by AS \"created_by!\",\n duration_ms AS \"duration_ms!\",\n success AS \"success!\",\n script_hash AS \"script_hash!: Option<ScriptHash>\",\n script_path,\n args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n result AS \"result: sqlx::types::Json<Box<RawValue>>\",\n deleted AS \"deleted!\",\n canceled AS \"canceled!\",\n canceled_by,\n canceled_reason,\n job_kind AS \"job_kind!: JobKind\",\n schedule_path,\n permissioned_as AS \"permissioned_as!\",\n is_flow_step AS \"is_flow_step!\",\n language AS \"language: ScriptLang\",\n is_skipped AS \"is_skipped!\",\n email AS \"email!\",\n visible_to_owner AS \"visible_to_owner!\",\n mem_peak,\n tag AS \"tag!\",\n created_at AS \"created_at!\",\n started_at,\n logs,\n log_offset AS \"log_offset?\",\n log_file_index\n FROM cumulative_sizes\n WHERE cumulative_size_bytes <= $2 OR row_num = 1\n ORDER BY created_at ASC",
|
||||
"query": "SELECT\n cj.id AS \"id!\",\n cj.workspace_id AS \"workspace_id!\",\n cj.parent_job,\n cj.created_by AS \"created_by!\",\n cj.duration_ms AS \"duration_ms!\",\n cj.success AS \"success!\",\n cj.script_hash AS \"script_hash!: Option<ScriptHash>\",\n cj.script_path,\n cj.args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n cj.result AS \"result: sqlx::types::Json<Box<RawValue>>\",\n cj.deleted AS \"deleted!\",\n cj.canceled AS \"canceled!\",\n cj.canceled_by,\n cj.canceled_reason,\n cj.job_kind AS \"job_kind!: JobKind\",\n cj.schedule_path,\n cj.permissioned_as AS \"permissioned_as!\",\n cj.is_flow_step AS \"is_flow_step!\",\n cj.language AS \"language: ScriptLang\",\n cj.is_skipped AS \"is_skipped!\",\n cj.email AS \"email!\",\n cj.visible_to_owner AS \"visible_to_owner!\",\n cj.mem_peak,\n cj.tag AS \"tag!\",\n cj.created_at AS \"created_at!\",\n cj.started_at,\n job_logs.logs,\n job_logs.log_offset AS \"log_offset?\",\n job_logs.log_file_index\n\n FROM v2_as_completed_job AS cj\n LEFT JOIN job_logs ON cj.id = job_logs.job_id\n WHERE (cj.created_at > $1 AND cj.created_at < $3)\n OR cj.id = ANY($2)\n ORDER BY cj.created_at ASC LIMIT $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -207,6 +207,8 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"UuidArray",
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
@@ -243,5 +245,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a6608d47b96d851eb7b04d2e4b472889ff257db0f0e3e9252adda2e4ef2039d6"
|
||||
"hash": "6e2564dc37ee967c634deade67ceabc4c516418e595dee9cd7d651acc1f4af6e"
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH job_sizes AS (\n SELECT \n cj.id,\n cj.workspace_id,\n cj.parent_job,\n cj.created_by,\n cj.duration_ms,\n cj.success,\n cj.script_hash,\n cj.script_path,\n cj.args,\n cj.result,\n cj.deleted,\n cj.canceled,\n cj.canceled_by,\n cj.canceled_reason,\n cj.job_kind,\n cj.schedule_path,\n cj.permissioned_as,\n cj.is_flow_step,\n cj.language,\n cj.is_skipped,\n cj.email,\n cj.visible_to_owner,\n cj.mem_peak,\n cj.tag,\n cj.created_at,\n cj.started_at,\n job_logs.logs,\n job_logs.log_offset,\n job_logs.log_file_index,\n -- Estimate size in bytes based on actual data characteristics\n (36 + -- UUID\n COALESCE(LENGTH(cj.workspace_id), 0) + \n COALESCE(LENGTH(cj.created_by), 0) + \n COALESCE(LENGTH(cj.script_path), 0) + \n COALESCE(LENGTH(cj.args::text), 0) + \n COALESCE(LENGTH(cj.result::text), 0) + \n COALESCE(LENGTH(job_logs.logs), 0) + \n 200) AS estimated_size_bytes -- Other fields overhead\n FROM v2_as_completed_job AS cj\n LEFT JOIN job_logs ON cj.id = job_logs.job_id\n WHERE (cj.created_at > $1 AND cj.created_at < $3)\n OR cj.id = ANY($2)\n ORDER BY created_at ASC\n LIMIT 5000\n ),\n cumulative_sizes AS (\n SELECT \n *,\n SUM(estimated_size_bytes) OVER (\n ORDER BY created_at ASC \n ROWS UNBOUNDED PRECEDING\n ) AS cumulative_size_bytes,\n ROW_NUMBER() OVER (ORDER BY created_at ASC) AS row_num\n FROM job_sizes\n )\n SELECT\n id AS \"id!\",\n workspace_id AS \"workspace_id!\",\n parent_job,\n created_by AS \"created_by!\",\n duration_ms AS \"duration_ms!\",\n success AS \"success!\",\n script_hash AS \"script_hash!: Option<ScriptHash>\",\n script_path,\n args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n result AS \"result: sqlx::types::Json<Box<RawValue>>\",\n deleted AS \"deleted!\",\n canceled AS \"canceled!\",\n canceled_by,\n canceled_reason,\n job_kind AS \"job_kind!: JobKind\",\n schedule_path,\n permissioned_as AS \"permissioned_as!\",\n is_flow_step AS \"is_flow_step!\",\n language AS \"language: ScriptLang\",\n is_skipped AS \"is_skipped!\",\n email AS \"email!\",\n visible_to_owner AS \"visible_to_owner!\",\n mem_peak,\n tag AS \"tag!\",\n created_at AS \"created_at!\",\n started_at,\n logs,\n log_offset AS \"log_offset?\",\n log_file_index\n FROM cumulative_sizes\n WHERE cumulative_size_bytes <= $4 OR row_num = 1\n ORDER BY created_at ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_by!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "duration_ms!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "success!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_hash!: Option<ScriptHash>",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "args: sqlx::types::Json<HashMap<String, Box<RawValue>>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "deleted!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "canceled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "canceled_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "canceled_reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "job_kind!: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "schedule_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "permissioned_as!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "is_flow_step!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "language: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "is_skipped!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "email!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "visible_to_owner!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "mem_peak",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "tag!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "logs",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "log_offset?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "log_file_index",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"UuidArray",
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7274e9489b18d7ab82bad1fbff89ffe41d162b482adb04b7b898a19576af2a5e"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path) DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
|
||||
"query": "INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -14,5 +14,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f04632c3a8e0d7c5b48cdd26a99bb1dc5bd12df221f82405d663b8f15f5c0c3a"
|
||||
"hash": "b83e58972d8f8285eb604de94c3d363102f5f6705d6debb442a85126ed59d863"
|
||||
}
|
||||
247
backend/.sqlx/query-f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7.json
generated
Normal file
247
backend/.sqlx/query-f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7.json
generated
Normal file
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n cj.id AS \"id!\",\n cj.workspace_id AS \"workspace_id!\",\n cj.parent_job,\n cj.created_by AS \"created_by!\",\n cj.duration_ms AS \"duration_ms!\",\n cj.success AS \"success!\",\n cj.script_hash AS \"script_hash!: Option<ScriptHash>\",\n cj.script_path,\n cj.args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n cj.result AS \"result: sqlx::types::Json<Box<RawValue>>\",\n cj.deleted AS \"deleted!\",\n cj.canceled AS \"canceled!\",\n cj.canceled_by,\n cj.canceled_reason,\n cj.job_kind AS \"job_kind!: JobKind\",\n cj.schedule_path,\n cj.permissioned_as AS \"permissioned_as!\",\n cj.is_flow_step AS \"is_flow_step!\",\n cj.language AS \"language: ScriptLang\",\n cj.is_skipped AS \"is_skipped!\",\n cj.email AS \"email!\",\n cj.visible_to_owner AS \"visible_to_owner!\",\n cj.mem_peak,\n cj.tag AS \"tag!\",\n cj.created_at AS \"created_at!\",\n cj.started_at,\n job_logs.logs,\n job_logs.log_offset AS \"log_offset?\",\n job_logs.log_file_index\n\n FROM v2_as_completed_job AS cj\n LEFT JOIN job_logs ON cj.id = job_logs.job_id\n WHERE cj.created_at < $1\n ORDER BY cj.created_at ASC LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_by!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "duration_ms!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "success!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_hash!: Option<ScriptHash>",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "args: sqlx::types::Json<HashMap<String, Box<RawValue>>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "deleted!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "canceled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "canceled_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "canceled_reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "job_kind!: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "schedule_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "permissioned_as!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "is_flow_step!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "language: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "is_skipped!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "email!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "visible_to_owner!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "mem_peak",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "tag!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "logs",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "log_offset?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "log_file_index",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7"
|
||||
}
|
||||
231
backend/Cargo.lock
generated
231
backend/Cargo.lock
generated
@@ -1196,9 +1196,9 @@ checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973"
|
||||
|
||||
[[package]]
|
||||
name = "backon"
|
||||
version = "1.5.2"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d"
|
||||
checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"gloo-timers",
|
||||
@@ -1531,31 +1531,6 @@ dependencies = [
|
||||
"serde_with",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bon"
|
||||
version = "3.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d9ef19ae5263a138da9a86871eca537478ab0332a7770bac7e3f08b801f89f"
|
||||
dependencies = [
|
||||
"bon-macros",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bon-macros"
|
||||
version = "3.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "577ae008f2ca11ca7641bd44601002ee5ab49ef0af64846ce1ab6057218a5cc1"
|
||||
dependencies = [
|
||||
"darling 0.21.0",
|
||||
"ident_case",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.5.7"
|
||||
@@ -1992,9 +1967,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.42"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882"
|
||||
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2002,9 +1977,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.42"
|
||||
version = "4.5.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966"
|
||||
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -2502,16 +2477,6 @@ dependencies = [
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9"
|
||||
dependencies = [
|
||||
"darling_core 0.21.0",
|
||||
"darling_macro 0.21.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.13.4"
|
||||
@@ -2554,20 +2519,6 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.13.4"
|
||||
@@ -2601,17 +2552,6 @@ dependencies = [
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146"
|
||||
dependencies = [
|
||||
"darling_core 0.21.0",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
@@ -4600,7 +4540,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"redox_users 0.5.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
@@ -4684,9 +4624,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "2.0.1"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dprint-swc-ext"
|
||||
@@ -5308,12 +5248,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fs4"
|
||||
version = "0.13.1"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
|
||||
checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8"
|
||||
dependencies = [
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.59.0",
|
||||
"rustix 0.38.44",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6597,15 +6537,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyperloglogplus"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
@@ -7361,9 +7292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.9"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
@@ -7750,10 +7681,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "measure_time"
|
||||
version = "0.9.0"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e"
|
||||
checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc"
|
||||
dependencies = [
|
||||
"instant",
|
||||
"log",
|
||||
]
|
||||
|
||||
@@ -8039,9 +7971,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mysql_common"
|
||||
version = "0.35.5"
|
||||
version = "0.35.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbb9f371618ce723f095c61fbcdc36e8936956d2b62832f9c7648689b338e052"
|
||||
checksum = "6e0ec195e788c95f36b7cf88127d538465fc2f7773e6e47af01834738eab0aee"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.1",
|
||||
@@ -9012,8 +8944,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "ownedbytes"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
@@ -10243,9 +10176,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
@@ -11690,9 +11623,9 @@ checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
|
||||
|
||||
[[package]]
|
||||
name = "sketches-ddsketch"
|
||||
version = "0.3.0"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a"
|
||||
checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -12817,14 +12750,14 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tantivy"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96599ea6fccd844fc833fed21d2eecac2e6a7c1afd9e044057391d78b1feb141"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"arc-swap",
|
||||
"base64 0.22.1",
|
||||
"bitpacking",
|
||||
"bon",
|
||||
"byteorder",
|
||||
"census",
|
||||
"crc32fast",
|
||||
@@ -12834,20 +12767,20 @@ dependencies = [
|
||||
"fnv",
|
||||
"fs4",
|
||||
"htmlescape",
|
||||
"hyperloglogplus",
|
||||
"itertools 0.14.0",
|
||||
"itertools 0.12.1",
|
||||
"levenshtein_automata",
|
||||
"log",
|
||||
"lru 0.12.5",
|
||||
"lz4_flex",
|
||||
"measure_time",
|
||||
"memmap2 0.9.7",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"oneshot",
|
||||
"rayon",
|
||||
"regex",
|
||||
"rust-stemmers",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustc-hash 1.1.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sketches-ddsketch",
|
||||
@@ -12860,7 +12793,7 @@ dependencies = [
|
||||
"tantivy-stacker",
|
||||
"tantivy-tokenizer-api",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"uuid",
|
||||
"winapi",
|
||||
@@ -12868,20 +12801,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-bitpacker"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df"
|
||||
dependencies = [
|
||||
"bitpacking",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-columnar"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e"
|
||||
dependencies = [
|
||||
"downcast-rs",
|
||||
"fastdivide",
|
||||
"itertools 0.14.0",
|
||||
"itertools 0.12.1",
|
||||
"serde",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
@@ -12891,8 +12826,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-common"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
@@ -12914,21 +12850,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-query-grammar"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-sstable"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"itertools 0.14.0",
|
||||
"tantivy-bitpacker",
|
||||
"tantivy-common",
|
||||
"tantivy-fst",
|
||||
@@ -12937,8 +12871,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-stacker"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8"
|
||||
dependencies = [
|
||||
"murmurhash32",
|
||||
"rand_distr 0.4.3",
|
||||
@@ -12947,8 +12882,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tantivy-tokenizer-api"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/windmill-labs/tantivy?rev=6a24621231202ccd77bec90d8787e2281fb94e4e#6a24621231202ccd77bec90d8787e2281fb94e4e"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -14749,7 +14685,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -14801,7 +14737,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14916,7 +14852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -14931,7 +14867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -14944,7 +14880,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -14958,7 +14894,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -15037,13 +14973,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"windmill-common",
|
||||
@@ -15052,7 +14987,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -15076,7 +15011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15088,7 +15023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15097,7 +15032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15109,7 +15044,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15121,7 +15056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15133,7 +15068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15145,7 +15080,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15157,7 +15092,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15168,7 +15103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15179,7 +15114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15191,7 +15126,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15214,7 +15149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15231,7 +15166,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15245,7 +15180,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15263,7 +15198,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.16",
|
||||
@@ -15287,7 +15222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15297,7 +15232,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15330,7 +15265,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15340,7 +15275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -386,7 +386,7 @@ tikv-jemalloc-ctl = { version = "^0.5" }
|
||||
triomphe = "^0"
|
||||
pin-project-lite = "^0"
|
||||
|
||||
tantivy = { git="https://github.com/windmill-labs/tantivy", rev="6a24621231202ccd77bec90d8787e2281fb94e4e" }
|
||||
tantivy = "0.22.0"
|
||||
|
||||
backon = "1.3.0"
|
||||
systemstat = "0.2.4"
|
||||
|
||||
@@ -1 +1 @@
|
||||
525af5409971b0347e7b2770268abdbe643f2b24
|
||||
cef7486dfa765f1cdfaddba59564c4bb92fe864e
|
||||
@@ -135,36 +135,6 @@ pub fn setup_deno_runtime() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_ca_certificates_if_requested() {
|
||||
if std::env::var("RUN_UPDATE_CA_CERTIFICATE_AT_START")
|
||||
.ok()
|
||||
.map(|v| v.to_lowercase() == "true")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let ca_cert_path = std::env::var("RUN_UPDATE_CA_CERTIFICATE_PATH")
|
||||
.unwrap_or_else(|_| "/usr/sbin/update-ca-certificates".to_string());
|
||||
|
||||
println!("RUN_UPDATE_CA_CERTIFICATE_AT_START=true, running: {}", ca_cert_path);
|
||||
|
||||
let output = std::process::Command::new(&ca_cert_path)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
if result.status.success() {
|
||||
println!("Successfully updated CA certificates");
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
println!("Failed to update CA certificates, but continuing startup: {}", stderr.trim());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Could not run update-ca-certificates command, but continuing startup: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn create_and_run_current_thread_inner<F, R>(future: F) -> R
|
||||
where
|
||||
@@ -293,14 +263,12 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
|
||||
async fn windmill_main() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
update_ca_certificates_if_requested();
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info")
|
||||
}
|
||||
|
||||
if let Err(_e) = rustls::crypto::ring::default_provider().install_default() {
|
||||
println!("Failed to install rustls crypto provider");
|
||||
tracing::error!("Failed to install rustls crypto provider");
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -312,9 +280,9 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
.arg("iptables -A OUTPUT -d 169.254.169.254 -j DROP && iptables -A FORWARD -d 169.254.169.254 -j DROP")
|
||||
.status()
|
||||
{
|
||||
println!("Failed to run iptables to block metadata endpoint: {e}");
|
||||
tracing::warn!("Failed to run iptables to block metadata endpoint: {e}");
|
||||
} else {
|
||||
println!("Successfully blocked metadata endpoint using iptables");
|
||||
tracing::info!("Successfully blocked metadata endpoint using iptables");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +377,7 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
|
||||
let num_version = sqlx::query_scalar!("SELECT version()").fetch_one(&db).await;
|
||||
|
||||
println!(
|
||||
tracing::info!(
|
||||
"PostgreSQL version: {} (windmill require PG >= 14)",
|
||||
num_version
|
||||
.ok()
|
||||
@@ -418,7 +386,7 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
);
|
||||
load_otel(&db).await;
|
||||
|
||||
println!("Database connected");
|
||||
tracing::info!("Database connected");
|
||||
(Connection::Sql(db), None)
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.517.0
|
||||
version: 1.515.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -2455,65 +2455,6 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/workspaces/edit_git_sync_repository:
|
||||
post:
|
||||
summary: add or update individual git sync repository
|
||||
operationId: editGitSyncRepository
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: Git sync repository settings to add or update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
git_repo_resource_path:
|
||||
type: string
|
||||
description: The resource path of the git repository to update
|
||||
repository:
|
||||
$ref: "#/components/schemas/GitRepositorySettings"
|
||||
required:
|
||||
- git_repo_resource_path
|
||||
- repository
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/workspaces/delete_git_sync_repository:
|
||||
delete:
|
||||
summary: delete individual git sync repository
|
||||
operationId: deleteGitSyncRepository
|
||||
tags:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: Git sync repository to delete
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
git_repo_resource_path:
|
||||
type: string
|
||||
description: The resource path of the git repository to delete
|
||||
required:
|
||||
- git_repo_resource_path
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/workspaces/edit_deploy_ui_config:
|
||||
post:
|
||||
summary: edit workspace deploy ui settings
|
||||
@@ -13708,24 +13649,6 @@ components:
|
||||
customai,
|
||||
]
|
||||
|
||||
GitSyncObjectType:
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
- app
|
||||
- folder
|
||||
- resource
|
||||
- variable
|
||||
- secret
|
||||
- resourcetype
|
||||
- schedule
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
- settings
|
||||
- key
|
||||
|
||||
AIProviderModel:
|
||||
type: object
|
||||
properties:
|
||||
@@ -17211,7 +17134,15 @@ components:
|
||||
include_type:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/GitSyncObjectType"
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
- app
|
||||
- resource
|
||||
- variable
|
||||
- secret
|
||||
- trigger
|
||||
|
||||
WorkspaceDefaultScripts:
|
||||
type: object
|
||||
@@ -17251,7 +17182,22 @@ components:
|
||||
include_type:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/GitSyncObjectType"
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
- app
|
||||
- folder
|
||||
- resource
|
||||
- variable
|
||||
- secret
|
||||
- resourcetype
|
||||
- schedule
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
- settings
|
||||
- key
|
||||
exclude_path:
|
||||
type: array
|
||||
items:
|
||||
@@ -17263,7 +17209,22 @@ components:
|
||||
exclude_types_override:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/GitSyncObjectType"
|
||||
type: string
|
||||
enum:
|
||||
- script
|
||||
- flow
|
||||
- app
|
||||
- folder
|
||||
- resource
|
||||
- variable
|
||||
- secret
|
||||
- resourcetype
|
||||
- schedule
|
||||
- user
|
||||
- group
|
||||
- trigger
|
||||
- settings
|
||||
- key
|
||||
required:
|
||||
- script_path
|
||||
- git_repo_resource_path
|
||||
|
||||
@@ -216,18 +216,6 @@ async fn create_folder(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"folder.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&ng.name.to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
@@ -239,6 +227,17 @@ async fn create_folder(
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"folder.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&ng.name.to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::CreateFolder { workspace: w_id, name: ng.name.clone() },
|
||||
@@ -368,18 +367,6 @@ async fn update_folder(
|
||||
}
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"folder.update",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&name.to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
@@ -391,6 +378,17 @@ async fn update_folder(
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"folder.update",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&name.to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
webhook.send_message(
|
||||
w_id.clone().clone(),
|
||||
WebhookMessage::UpdateFolder { workspace: w_id, name: name.to_owned() },
|
||||
|
||||
@@ -1069,18 +1069,6 @@ async fn create_resource_type(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"resource_types.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&resource_type.name),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
@@ -1095,6 +1083,18 @@ async fn create_resource_type(
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"resource_types.create",
|
||||
ActionKind::Create,
|
||||
&w_id,
|
||||
Some(&resource_type.name),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::CreateResourceType { name: resource_type.name.clone() },
|
||||
|
||||
@@ -25,11 +25,7 @@ use std::str::FromStr;
|
||||
use windmill_audit::audit_oss::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult, Result},
|
||||
schedule::Schedule,
|
||||
utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath},
|
||||
worker::to_raw_value,
|
||||
db::UserDB, error::{Error, JsonResult, Result}, schedule::Schedule, utils::{not_found_if_none, paginate, Pagination, ScheduleType, StripPath}, worker::to_raw_value
|
||||
};
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::schedule::push_scheduled_job;
|
||||
@@ -257,6 +253,17 @@ async fn create_schedule(
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("inserting schedule in {w_id}: {e:#}")))?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: ns.path.clone() },
|
||||
Some(format!("Schedule '{}' created", ns.path.clone())),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -281,17 +288,6 @@ async fn create_schedule(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: ns.path.clone() },
|
||||
Some(format!("Schedule '{}' created", ns.path.clone())),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ns.path.to_string())
|
||||
}
|
||||
|
||||
@@ -402,6 +398,17 @@ async fn edit_schedule(
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("updating schedule in {w_id}: {e:#}")))?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: path.to_string() },
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -423,17 +430,6 @@ async fn edit_schedule(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: path.to_string() },
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(path.to_string())
|
||||
}
|
||||
|
||||
@@ -671,6 +667,17 @@ pub async fn set_enabled(
|
||||
|
||||
clear_schedule(&mut tx, path, &w_id).await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: path.to_string() },
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -687,17 +694,6 @@ pub async fn set_enabled(
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: path.to_string() },
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!(
|
||||
"succesfully updated schedule at path {} to status {}",
|
||||
path, payload.enabled
|
||||
@@ -794,6 +790,17 @@ async fn delete_schedule(
|
||||
)));
|
||||
}
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: path.to_string() },
|
||||
Some(format!("Schedule '{}' deleted", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -807,17 +814,6 @@ async fn delete_schedule(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Schedule { path: path.to_string() },
|
||||
Some(format!("Schedule '{}' deleted", path)),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("schedule {} deleted", path))
|
||||
}
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ async fn create_snapshot_script(
|
||||
let mut script_hash = None;
|
||||
let mut tx = None;
|
||||
let mut uploaded = false;
|
||||
let mut handle_deployment_metadata = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
let name = field.name().unwrap().to_string();
|
||||
let data = field.bytes().await.unwrap();
|
||||
@@ -403,7 +403,7 @@ async fn create_snapshot_script(
|
||||
let ns: NewScript = Some(serde_json::from_slice(&data).map_err(to_anyhow)?).unwrap();
|
||||
let is_tar = ns.codebase.as_ref().is_some_and(|x| x.ends_with(".tar"));
|
||||
|
||||
let (new_hash, ntx, hdm) = create_script_internal(
|
||||
let (new_hash, ntx) = create_script_internal(
|
||||
ns,
|
||||
w_id.clone(),
|
||||
authed.clone(),
|
||||
@@ -415,7 +415,6 @@ async fn create_snapshot_script(
|
||||
let nh = new_hash.to_string();
|
||||
script_hash = Some(if is_tar { format!("{nh}.tar") } else { nh });
|
||||
tx = Some(ntx);
|
||||
handle_deployment_metadata = hdm;
|
||||
}
|
||||
if name == "file" {
|
||||
let hash = script_hash.as_ref().ok_or_else(|| {
|
||||
@@ -478,9 +477,6 @@ async fn create_snapshot_script(
|
||||
}
|
||||
|
||||
tx.unwrap().commit().await?;
|
||||
if let Some(hdm) = handle_deployment_metadata {
|
||||
hdm.handle(&db).await?;
|
||||
}
|
||||
return Ok((StatusCode::CREATED, format!("{}", script_hash.unwrap())));
|
||||
}
|
||||
|
||||
@@ -510,38 +506,11 @@ async fn create_script(
|
||||
Path(w_id): Path<String>,
|
||||
Json(ns): Json<NewScript>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let (hash, tx, hdm) =
|
||||
create_script_internal(ns, w_id, authed, db.clone(), user_db, webhook).await?;
|
||||
let (hash, tx) = create_script_internal(ns, w_id, authed, db, user_db, webhook).await?;
|
||||
tx.commit().await?;
|
||||
if let Some(hdm) = hdm {
|
||||
hdm.handle(&db).await?;
|
||||
}
|
||||
Ok((StatusCode::CREATED, format!("{}", hash)))
|
||||
}
|
||||
|
||||
struct HandleDeploymentMetadata {
|
||||
email: String,
|
||||
created_by: String,
|
||||
w_id: String,
|
||||
obj: DeployedObject,
|
||||
deployment_message: Option<String>,
|
||||
}
|
||||
|
||||
impl HandleDeploymentMetadata {
|
||||
async fn handle(self, db: &DB) -> Result<()> {
|
||||
handle_deployment_metadata(
|
||||
&self.email,
|
||||
&self.created_by,
|
||||
&db,
|
||||
&self.w_id,
|
||||
self.obj,
|
||||
self.deployment_message,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_script_internal<'c>(
|
||||
ns: NewScript,
|
||||
w_id: String,
|
||||
@@ -549,11 +518,7 @@ async fn create_script_internal<'c>(
|
||||
db: sqlx::Pool<Postgres>,
|
||||
user_db: UserDB,
|
||||
webhook: WebhookShared,
|
||||
) -> Result<(
|
||||
ScriptHash,
|
||||
Transaction<'c, Postgres>,
|
||||
Option<HandleDeploymentMetadata>,
|
||||
)> {
|
||||
) -> Result<(ScriptHash, Transaction<'c, Postgres>)> {
|
||||
check_scopes(&authed, || format!("scripts:write:{}", ns.path))?;
|
||||
|
||||
let codebase = ns.codebase.as_ref();
|
||||
@@ -1024,74 +989,57 @@ async fn create_script_internal<'c>(
|
||||
Some(&authed.clone().into()),
|
||||
)
|
||||
.await?;
|
||||
Ok((hash, new_tx, None))
|
||||
Ok((hash, new_tx))
|
||||
} else {
|
||||
if codebase.is_none() {
|
||||
let db2 = db.clone();
|
||||
let w_id2 = w_id.clone();
|
||||
let authed2 = authed.clone();
|
||||
let permissioned_as2 = permissioned_as.clone();
|
||||
let script_path2 = script_path.clone();
|
||||
let parent_path = p_path_opt.clone();
|
||||
let lock = ns.lock.clone();
|
||||
let deployment_message = ns.deployment_message.clone();
|
||||
let content = ns.content.clone();
|
||||
let language = ns.language.clone();
|
||||
tokio::spawn(async move {
|
||||
// wait for 10 seconds to make sure the script is deployed and that the CLI sync that pushed it (f one) is complete
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
if let Err(e) = process_relative_imports(
|
||||
&db2,
|
||||
None,
|
||||
None,
|
||||
&w_id2,
|
||||
&script_path2,
|
||||
parent_path,
|
||||
deployment_message,
|
||||
&content,
|
||||
&Some(language),
|
||||
&authed2.email,
|
||||
&authed2.username,
|
||||
&permissioned_as2,
|
||||
lock,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%e, "error processing relative imports");
|
||||
}
|
||||
});
|
||||
}
|
||||
let db2 = db.clone();
|
||||
let w_id2 = w_id.clone();
|
||||
let authed2 = authed.clone();
|
||||
let permissioned_as2 = permissioned_as.clone();
|
||||
let script_path2 = script_path.clone();
|
||||
let parent_path = p_path_opt.clone();
|
||||
let lock = ns.lock.clone();
|
||||
let deployment_message = ns.deployment_message.clone();
|
||||
let content = ns.content.clone();
|
||||
let language = ns.language.clone();
|
||||
tokio::spawn(async move {
|
||||
// wait for 10 seconds to make sure the script is deployed and that the CLI sync that pushed it (f one) is complete
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
if let Err(e) = process_relative_imports(
|
||||
&db2,
|
||||
None,
|
||||
None,
|
||||
&w_id2,
|
||||
&script_path2,
|
||||
parent_path,
|
||||
deployment_message,
|
||||
&content,
|
||||
&Some(language),
|
||||
&authed2.email,
|
||||
&authed2.username,
|
||||
&permissioned_as2,
|
||||
lock,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%e, "error processing relative imports");
|
||||
}
|
||||
});
|
||||
|
||||
// handle_deployment_metadata(
|
||||
// &authed.email,
|
||||
// &authed.username,
|
||||
// &db,
|
||||
// &w_id,
|
||||
// DeployedObject::Script {
|
||||
// hash: hash.clone(),
|
||||
// path: script_path.clone(),
|
||||
// parent_path: p_path_opt,
|
||||
// },
|
||||
// ns.deployment_message,
|
||||
// false,
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
Ok((
|
||||
hash,
|
||||
tx,
|
||||
Some(HandleDeploymentMetadata {
|
||||
email: authed.email,
|
||||
created_by: authed.username,
|
||||
w_id,
|
||||
obj: DeployedObject::Script {
|
||||
hash: hash.clone(),
|
||||
path: script_path.clone(),
|
||||
parent_path: p_path_opt,
|
||||
},
|
||||
deployment_message: ns.deployment_message,
|
||||
}),
|
||||
))
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
DeployedObject::Script {
|
||||
hash: hash.clone(),
|
||||
path: script_path.clone(),
|
||||
parent_path: p_path_opt,
|
||||
},
|
||||
ns.deployment_message,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Ok((hash, tx))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,6 @@ use windmill_common::{
|
||||
oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH,
|
||||
utils::{paginate, rd_string, require_admin, Pagination},
|
||||
};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::workspaces::GitRepositorySettings;
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -115,8 +113,6 @@ pub fn workspaced_service() -> Router {
|
||||
post(edit_large_file_storage_config),
|
||||
)
|
||||
.route("/edit_git_sync_config", post(edit_git_sync_config))
|
||||
.route("/edit_git_sync_repository", post(edit_git_sync_repository))
|
||||
.route("/delete_git_sync_repository", delete(delete_git_sync_repository))
|
||||
.route("/edit_deploy_ui_config", post(edit_deploy_ui_config))
|
||||
.route("/edit_default_app", post(edit_default_app))
|
||||
.route("/default_app", get(get_default_app))
|
||||
@@ -873,76 +869,6 @@ pub struct EditGitSyncConfig {
|
||||
pub git_sync_settings: Option<WorkspaceGitSyncSettings>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EditGitSyncRepository {
|
||||
pub git_repo_resource_path: String,
|
||||
pub repository: GitRepositorySettings,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct DeleteGitSyncRepositoryRequest {
|
||||
pub git_repo_resource_path: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
fn validate_git_repo_resource_path(path: &str) -> Result<()> {
|
||||
// Resource paths should follow the pattern: $res:f/<folder>/<name> or $res:u/<username>/<name>
|
||||
if path.is_empty() {
|
||||
return Err(Error::BadRequest("Resource path cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
// Must start with $res: prefix
|
||||
if !path.starts_with("$res:") {
|
||||
return Err(Error::BadRequest("Resource path must start with '$res:'".to_string()));
|
||||
}
|
||||
|
||||
// Extract the actual path after $res:
|
||||
let actual_path = &path[5..]; // Remove "$res:" prefix
|
||||
|
||||
// Basic validation: must start with f/ or u/ and contain at least one slash
|
||||
if !actual_path.starts_with("f/") && !actual_path.starts_with("u/") {
|
||||
return Err(Error::BadRequest("Resource path must start with '$res:f/' or '$res:u/'".to_string()));
|
||||
}
|
||||
|
||||
// Must have at least 3 parts (type, folder/user, name)
|
||||
let parts: Vec<&str> = actual_path.split('/').collect();
|
||||
if parts.len() < 3 || parts.iter().any(|part| part.is_empty()) {
|
||||
return Err(Error::BadRequest("Invalid resource path format".to_string()));
|
||||
}
|
||||
|
||||
// Resource name validation (last part)
|
||||
let resource_name = parts.last().unwrap();
|
||||
if !resource_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
|
||||
return Err(Error::BadRequest("Resource name can only contain alphanumeric characters, underscores, and hyphens".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
fn cleanup_legacy_git_sync_settings_in_memory(
|
||||
git_sync_settings: &mut windmill_common::workspaces::WorkspaceGitSyncSettings,
|
||||
workspace_id: &str,
|
||||
) {
|
||||
// Check if all repositories are in new format (have settings field)
|
||||
let all_repos_migrated = git_sync_settings.repositories.iter()
|
||||
.all(|repo| repo.settings.is_some());
|
||||
|
||||
// If all repos are migrated and we still have legacy workspace-level settings
|
||||
if all_repos_migrated && (git_sync_settings.include_path.is_some() || git_sync_settings.include_type.is_some()) {
|
||||
tracing::info!(
|
||||
workspace_id = workspace_id,
|
||||
"All git sync repositories migrated to new format, cleaning up legacy workspace-level settings"
|
||||
);
|
||||
|
||||
// Remove workspace-level legacy fields
|
||||
git_sync_settings.include_path = None;
|
||||
git_sync_settings.include_type = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
async fn edit_git_sync_config(
|
||||
_authed: ApiAuthed,
|
||||
@@ -979,10 +905,7 @@ async fn edit_git_sync_config(
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(mut git_sync_settings) = new_config.git_sync_settings {
|
||||
// Clean up legacy workspace-level settings if all repos are migrated
|
||||
cleanup_legacy_git_sync_settings_in_memory(&mut git_sync_settings, &w_id);
|
||||
|
||||
if let Some(git_sync_settings) = new_config.git_sync_settings {
|
||||
let serialized_config = serde_json::to_value::<WorkspaceGitSyncSettings>(git_sync_settings)
|
||||
.map_err(|err| Error::internal_err(err.to_string()))?;
|
||||
|
||||
@@ -1001,7 +924,6 @@ async fn edit_git_sync_config(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for git sync settings changes
|
||||
@@ -1019,220 +941,6 @@ async fn edit_git_sync_config(
|
||||
Ok(format!("Edit git sync config for workspace {}", &w_id))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
async fn edit_git_sync_repository(
|
||||
_authed: ApiAuthed,
|
||||
Extension(_db): Extension<DB>,
|
||||
Path(_w_id): Path<String>,
|
||||
Json(_new_config): Json<serde_json::Value>,
|
||||
) -> Result<String> {
|
||||
return Err(Error::BadRequest(
|
||||
"Git sync is only available on Windmill Enterprise Edition".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn edit_git_sync_repository(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, .. }: ApiAuthed,
|
||||
Json(new_config): Json<EditGitSyncRepository>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
// Validate the resource path format
|
||||
validate_git_repo_resource_path(&new_config.git_repo_resource_path)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
// First, get the current git sync settings
|
||||
let current_settings = sqlx::query!(
|
||||
"SELECT git_sync FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let mut git_sync_settings = if let Some(row) = current_settings {
|
||||
if let Some(git_sync) = row.git_sync {
|
||||
serde_json::from_value::<WorkspaceGitSyncSettings>(git_sync)
|
||||
.map_err(|err| Error::internal_err(err.to_string()))?
|
||||
} else {
|
||||
WorkspaceGitSyncSettings::default()
|
||||
}
|
||||
} else {
|
||||
WorkspaceGitSyncSettings::default()
|
||||
};
|
||||
|
||||
// Audit log before we move the repository
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"workspaces.edit_git_sync_repository",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("repository_path", new_config.git_repo_resource_path.as_str()), ("repository_data", &format!("{:?}", new_config.repository))].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check if repository exists before modifying
|
||||
let repo_exists = git_sync_settings.repositories.iter()
|
||||
.any(|repo| repo.git_repo_resource_path == new_config.git_repo_resource_path);
|
||||
|
||||
// Find and update the specific repository, or add it if it doesn't exist
|
||||
let repo_found = git_sync_settings.repositories.iter_mut()
|
||||
.find(|repo| repo.git_repo_resource_path == new_config.git_repo_resource_path);
|
||||
|
||||
if let Some(existing_repo) = repo_found {
|
||||
// Update existing repository
|
||||
*existing_repo = new_config.repository;
|
||||
} else {
|
||||
// Repository doesn't exist, add it as a new repository
|
||||
git_sync_settings.repositories.push(new_config.repository);
|
||||
}
|
||||
|
||||
// Clean up legacy workspace-level settings if all repos are migrated
|
||||
cleanup_legacy_git_sync_settings_in_memory(&mut git_sync_settings, &w_id);
|
||||
|
||||
// Save the updated configuration
|
||||
let serialized_config = serde_json::to_value::<WorkspaceGitSyncSettings>(git_sync_settings)
|
||||
.map_err(|err| Error::internal_err(err.to_string()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET git_sync = $1 WHERE workspace_id = $2",
|
||||
serialized_config,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for individual repository update/add
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "git_sync".to_string() },
|
||||
Some(format!("Git sync repository '{}' {}",
|
||||
new_config.git_repo_resource_path,
|
||||
if repo_exists { "updated" } else { "added" }
|
||||
)),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("{} git sync repository '{}' for workspace {}",
|
||||
if repo_exists { "Updated" } else { "Added" },
|
||||
new_config.git_repo_resource_path,
|
||||
&w_id
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
async fn delete_git_sync_repository(
|
||||
_authed: ApiAuthed,
|
||||
Extension(_db): Extension<DB>,
|
||||
Path(_w_id): Path<String>,
|
||||
Json(_request): Json<serde_json::Value>,
|
||||
) -> Result<String> {
|
||||
return Err(Error::BadRequest(
|
||||
"Git sync is only available on Windmill Enterprise Edition".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn delete_git_sync_repository(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
ApiAuthed { is_admin, username, .. }: ApiAuthed,
|
||||
Json(request): Json<DeleteGitSyncRepositoryRequest>,
|
||||
) -> Result<String> {
|
||||
require_admin(is_admin, &username)?;
|
||||
|
||||
// Validate the resource path format
|
||||
validate_git_repo_resource_path(&request.git_repo_resource_path)?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
// First, get the current git sync settings
|
||||
let current_settings = sqlx::query!(
|
||||
"SELECT git_sync FROM workspace_settings WHERE workspace_id = $1",
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let mut git_sync_settings = if let Some(row) = current_settings {
|
||||
if let Some(git_sync) = row.git_sync {
|
||||
serde_json::from_value::<WorkspaceGitSyncSettings>(git_sync)
|
||||
.map_err(|err| Error::internal_err(err.to_string()))?
|
||||
} else {
|
||||
WorkspaceGitSyncSettings::default()
|
||||
}
|
||||
} else {
|
||||
WorkspaceGitSyncSettings::default()
|
||||
};
|
||||
|
||||
// Check if repository exists and remove it
|
||||
let original_count = git_sync_settings.repositories.len();
|
||||
git_sync_settings.repositories.retain(|repo| repo.git_repo_resource_path != request.git_repo_resource_path);
|
||||
|
||||
if git_sync_settings.repositories.len() == original_count {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Repository with path '{}' not found in git sync configuration",
|
||||
request.git_repo_resource_path
|
||||
)));
|
||||
}
|
||||
|
||||
// Audit log
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
"workspaces.delete_git_sync_repository",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("repository_path", request.git_repo_resource_path.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Clean up legacy workspace-level settings if all repos are migrated
|
||||
cleanup_legacy_git_sync_settings_in_memory(&mut git_sync_settings, &w_id);
|
||||
|
||||
// Save the updated configuration
|
||||
let serialized_config = serde_json::to_value::<WorkspaceGitSyncSettings>(git_sync_settings)
|
||||
.map_err(|err| Error::internal_err(err.to_string()))?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET git_sync = $1 WHERE workspace_id = $2",
|
||||
serialized_config,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Trigger git sync for repository deletion
|
||||
handle_deployment_metadata(
|
||||
&authed.email,
|
||||
&authed.username,
|
||||
&db,
|
||||
&w_id,
|
||||
windmill_git_sync::DeployedObject::Settings { setting_type: "git_sync".to_string() },
|
||||
Some(format!("Git sync repository '{}' deleted", request.git_repo_resource_path)),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(format!("Deleted git sync repository '{}' from workspace {}", request.git_repo_resource_path, &w_id))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EditDeployUIConfig {
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -2618,25 +2326,15 @@ pub async fn mute_critical_alerts() -> Error {
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct ChangeOperatorSettings {
|
||||
#[serde(default)]
|
||||
runs: bool,
|
||||
#[serde(default)]
|
||||
schedules: bool,
|
||||
#[serde(default)]
|
||||
resources: bool,
|
||||
#[serde(default)]
|
||||
variables: bool,
|
||||
#[serde(default)]
|
||||
assets: bool,
|
||||
#[serde(default)]
|
||||
triggers: bool,
|
||||
#[serde(default)]
|
||||
audit_logs: bool,
|
||||
#[serde(default)]
|
||||
groups: bool,
|
||||
#[serde(default)]
|
||||
folders: bool,
|
||||
#[serde(default)]
|
||||
workers: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ impl Default for TantivyIndexerSettings {
|
||||
fn default() -> Self {
|
||||
TantivyIndexerSettings {
|
||||
writer_memory_budget: 300_000_000,
|
||||
commit_job_max_batch_size: 50_000,
|
||||
commit_job_max_batch_size: 100_000,
|
||||
commit_log_max_batch_size: 10_000,
|
||||
refresh_index_period: 300,
|
||||
refresh_log_index_period: 300,
|
||||
|
||||
@@ -21,5 +21,4 @@ serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
windmill-queue.workspace = true
|
||||
regex = "1.10.3"
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
regex = "1.10.3"
|
||||
@@ -8,8 +8,12 @@ edition.workspace = true
|
||||
name = "windmill_indexer"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "windmill_indexer_debug"
|
||||
path = "./src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["enterprise", "private", "parquet"]
|
||||
parquet = ["dep:object_store"]
|
||||
private = []
|
||||
enterprise = []
|
||||
@@ -33,3 +37,4 @@ tokio-tar.workspace = true
|
||||
lazy_static.workspace = true
|
||||
const_format.workspace = true
|
||||
flume.workspace = true
|
||||
gethostname.workspace = true
|
||||
|
||||
131
backend/windmill-indexer/src/main.rs
Normal file
131
backend/windmill-indexer/src/main.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use anyhow::anyhow;
|
||||
use const_format::concatcp;
|
||||
use gethostname::gethostname;
|
||||
use std::{str::FromStr, sync::{Arc, Mutex}};
|
||||
use tantivy::{directory::MmapDirectory, schema::Schema};
|
||||
use windmill_common::{
|
||||
indexer::TantivyIndexerSettings, utils::{rd_string, Mode}, worker::TMP_DIR,
|
||||
};
|
||||
use windmill_indexer::{
|
||||
completed_runs_ee::{
|
||||
fill_schema, IndexWriter, IndexedJobTrackerData, INDEXED_JOB_TRACKER_FILENAME,
|
||||
},
|
||||
indexer_ee::{clean_dir_all_if_exists, IndexedDBTracker, S3BackedMMapDirectory},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref HOSTNAME :String = std::env::var("FORCE_HOSTNAME").unwrap_or_else(|_| {
|
||||
gethostname()
|
||||
.to_str()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| rd_string(5))
|
||||
});
|
||||
}
|
||||
|
||||
const S3_JOB_INDEX_PATH: &str = "search-dbg/completed_job_index";
|
||||
const JOB_INDEX_DIRECTORY_PATH: &str = concatcp!(TMP_DIR, "/search-dbg/completed_jobs_index");
|
||||
|
||||
fn init_index() -> anyhow::Result<IndexWriter> {
|
||||
let mut schema_builder = Schema::builder();
|
||||
let fields = fill_schema(&mut schema_builder);
|
||||
let schema = schema_builder.build();
|
||||
|
||||
std::fs::create_dir_all(JOB_INDEX_DIRECTORY_PATH)?;
|
||||
|
||||
let idx_tantivy_dir = MmapDirectory::open(JOB_INDEX_DIRECTORY_PATH)
|
||||
.map_err(|e| anyhow!("Failed to create MMapDirectory: {e}"))?;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
let idx_tantivy_dir = S3BackedMMapDirectory::new(
|
||||
std::path::PathBuf::from_str(JOB_INDEX_DIRECTORY_PATH).map_err(|e| anyhow!("{}", e))?,
|
||||
idx_tantivy_dir,
|
||||
S3_JOB_INDEX_PATH.to_string(),
|
||||
tokio::runtime::Handle::current(),
|
||||
);
|
||||
|
||||
clean_dir_all_if_exists(JOB_INDEX_DIRECTORY_PATH)?;
|
||||
let index = tantivy::Index::open_or_create(idx_tantivy_dir, schema)
|
||||
.map_err(|e| anyhow!("Failed to open index {e}"))?;
|
||||
|
||||
let job_tracker: IndexedDBTracker<IndexedJobTrackerData> = IndexedDBTracker::new(
|
||||
std::path::PathBuf::from_str(INDEXED_JOB_TRACKER_FILENAME)
|
||||
.map_err(|e| anyhow!("Failed to read {}: {e}", INDEXED_JOB_TRACKER_FILENAME))?,
|
||||
index.directory().clone(),
|
||||
);
|
||||
|
||||
Ok(IndexWriter { index, fields, job_tracker })
|
||||
}
|
||||
#[tokio::main]
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let hostname = HOSTNAME.to_owned();
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info")
|
||||
}
|
||||
|
||||
let (_guard, _jaja) = windmill_common::tracing_init::initialize_tracing(&hostname, &Mode::Indexer, "");
|
||||
|
||||
let db = windmill_common::initial_connection().await?;
|
||||
|
||||
let mut idx_writer = init_index()?;
|
||||
|
||||
let max_created_at = std::env::var("INDEX_STARTING_FROM")
|
||||
.ok()
|
||||
.map(|start_from| chrono::DateTime::parse_from_rfc3339(&start_from))
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!("Failed to parse date for INDEX_STARTING_FROM"))?;
|
||||
|
||||
if let Some(max_created_at) = max_created_at {
|
||||
tracing::info!("Date to start indexing aka INDEX_STARTING_FROM is set to {:?}", max_created_at);
|
||||
idx_writer.job_tracker.data =
|
||||
Some(IndexedJobTrackerData { max_created_at: max_created_at.into(), queued_uuids_remainder: vec![] });
|
||||
} else {
|
||||
tracing::info!("INDEX_STARTING_FROM env var not found, indexing from the very first job");
|
||||
}
|
||||
|
||||
let writer = idx_writer
|
||||
.index
|
||||
.writer(300 * 1024 * 1024)?;
|
||||
|
||||
let w = Arc::new(Mutex::new(writer));
|
||||
idx_writer.refresh_jobs(w.clone(), &db, &TantivyIndexerSettings::default()).await?;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Err(e) = Arc::try_unwrap(w)
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"There was more than 1 refrence to the writer. This should not be possible."
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.into_inner()
|
||||
.unwrap()
|
||||
.wait_merging_threads()
|
||||
{
|
||||
tracing::error!("Error while waiting for index writer merging threads: {e}");
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
tracing::info!("All merging threads completed, releasing lock");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// let (index_reader, index_writer) = {
|
||||
// let mut indexer_rx = killpill_rx.resubscribe();
|
||||
//
|
||||
// let (mut reader, mut writer) = (None, None);
|
||||
// tokio::select! {
|
||||
// _ = indexer_rx.recv() => {
|
||||
// tracing::info!("Received killpill, aborting index initialization");
|
||||
// },
|
||||
// res = windmill_indexer::completed_runs_oss::init_index(&db) => {
|
||||
// let res = res?;
|
||||
// reader = Some(res.0);
|
||||
// writer = Some(res.1);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// (reader, writer)
|
||||
// };
|
||||
@@ -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.517.0";
|
||||
export const VERSION = "v1.515.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
3
cli/.gitignore
vendored
3
cli/.gitignore
vendored
@@ -1,3 +1,2 @@
|
||||
npm/
|
||||
gen/
|
||||
node_modules/
|
||||
gen/
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Generate client files
|
||||
./gen_wm_client-mac.sh
|
||||
|
||||
# Generate utils client files
|
||||
./windmill-utils-internal/gen_wm_client-mac.sh
|
||||
|
||||
# Function to add .ts extensions to relative imports
|
||||
add_ts_extensions() {
|
||||
find windmill-utils-internal/src -name "*.ts" -type f | while read -r file; do
|
||||
# Create backup of original
|
||||
cp "$file" "$file.orig"
|
||||
|
||||
# Add .ts to relative imports that don't already have extensions
|
||||
gsed -E \
|
||||
-e 's/(from[[:space:]]+["'"'"'])(\.[^"'"'"']*[^./][^"'"'"']*)(["'"'"'])/\1\2.ts\3/g' \
|
||||
-e 's/(import[[:space:]]+["'"'"'])(\.[^"'"'"']*[^./][^"'"'"']*)(["'"'"'])/\1\2.ts\3/g' \
|
||||
"$file.orig" > "$file"
|
||||
done
|
||||
}
|
||||
|
||||
# Function to revert to original files
|
||||
revert_extensions() {
|
||||
find windmill-utils-internal/src -name "*.orig" -type f | while read -r backup_file; do
|
||||
original_file="${backup_file%.orig}"
|
||||
mv "$backup_file" "$original_file"
|
||||
done
|
||||
}
|
||||
|
||||
# Set up trap to ensure cleanup happens on exit, error, or interruption
|
||||
trap 'echo "Cleaning up..."; revert_extensions' EXIT ERR INT TERM
|
||||
|
||||
# Add .ts extensions for Deno
|
||||
echo "Adding .ts extensions for Deno build..."
|
||||
add_ts_extensions
|
||||
|
||||
# Run dnt
|
||||
echo "Running dnt..."
|
||||
deno run -A dnt.ts
|
||||
|
||||
echo "Build complete!"
|
||||
43
cli/build.sh
43
cli/build.sh
@@ -1,45 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Set script to exit on any error
|
||||
set -e
|
||||
|
||||
# Generate client files
|
||||
# Note for mac OS users: you need to install gnu-sed with `brew install gnu-sed` and use `gsed` instead of `sed`.
|
||||
./gen_wm_client.sh
|
||||
|
||||
# Generate utils client files
|
||||
./windmill-utils-internal/gen_wm_client.sh
|
||||
|
||||
# Function to add .ts extensions to relative imports
|
||||
add_ts_extensions() {
|
||||
find windmill-utils-internal/src -name "*.ts" -type f | while read -r file; do
|
||||
# Create backup of original
|
||||
cp "$file" "$file.orig"
|
||||
|
||||
# Add .ts to relative imports that don't already have extensions
|
||||
gsed -E \
|
||||
-e 's/(from[[:space:]]+["'"'"'])(\.[^"'"'"']*[^./][^"'"'"']*)(["'"'"'])/\1\2.ts\3/g' \
|
||||
-e 's/(import[[:space:]]+["'"'"'])(\.[^"'"'"']*[^./][^"'"'"']*)(["'"'"'])/\1\2.ts\3/g' \
|
||||
"$file.orig" > "$file"
|
||||
done
|
||||
}
|
||||
|
||||
# Function to revert to original files
|
||||
revert_extensions() {
|
||||
find windmill-utils-internal/src -name "*.orig" -type f | while read -r backup_file; do
|
||||
original_file="${backup_file%.orig}"
|
||||
mv "$backup_file" "$original_file"
|
||||
done
|
||||
}
|
||||
|
||||
# Set up trap to ensure cleanup happens on exit, error, or interruption
|
||||
trap 'echo "Cleaning up..."; revert_extensions' EXIT ERR INT TERM
|
||||
|
||||
# Add .ts extensions for Deno
|
||||
echo "Adding .ts extensions for Deno build..."
|
||||
add_ts_extensions
|
||||
|
||||
# Run dnt
|
||||
echo "Running dnt..."
|
||||
deno run -A dnt.ts
|
||||
|
||||
echo "Build complete!"
|
||||
1769
cli/deno.lock
generated
1769
cli/deno.lock
generated
File diff suppressed because it is too large
Load Diff
14
cli/dev.ts
14
cli/dev.ts
@@ -22,8 +22,7 @@ import {
|
||||
import { exts, findGlobalDeps, removeExtensionToPath } from "./script.ts";
|
||||
import { inferContentTypeFromFilePath } from "./script_common.ts";
|
||||
import { OpenFlow } from "./gen/types.gen.ts";
|
||||
import { FlowFile } from "./flow.ts";
|
||||
import { replaceInlineScripts } from "./windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import { FlowFile, replaceInlineScripts } from "./flow.ts";
|
||||
import { parseMetadataFile } from "./metadata.ts";
|
||||
|
||||
const PORT = 3001;
|
||||
@@ -75,16 +74,7 @@ async function dev(opts: GlobalOptions & SyncOptions) {
|
||||
const localFlow = (await yamlParseFile(
|
||||
localPath + "flow.yaml"
|
||||
)) as FlowFile;
|
||||
await replaceInlineScripts(
|
||||
localFlow.value.modules,
|
||||
async (path: string) => await Deno.readTextFile(localPath + path),
|
||||
log,
|
||||
localPath,
|
||||
SEP,
|
||||
undefined,
|
||||
(path: string, newPath: string) => Deno.renameSync(path, newPath),
|
||||
(path: string) => Deno.removeSync(path),
|
||||
);
|
||||
replaceInlineScripts(localFlow.value.modules, localPath, undefined);
|
||||
currentLastEdit = {
|
||||
type: "flow",
|
||||
flow: localFlow,
|
||||
|
||||
64
cli/flow.ts
64
cli/flow.ts
@@ -8,12 +8,11 @@ import { requireLogin } from "./auth.ts";
|
||||
import { resolveWorkspace, validatePath } from "./context.ts";
|
||||
import { resolve, track_job } from "./script.ts";
|
||||
import { defaultFlowDefinition } from "./bootstrap/flow_bootstrap.ts";
|
||||
import { generateFlowLockInternal } from "./metadata.ts";
|
||||
import { blueColor, generateFlowLockInternal } from "./metadata.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile } from "./conf.ts";
|
||||
import { FSFSElement, elementsToMap, ignoreF } from "./sync.ts";
|
||||
import { Flow } from "./gen/types.gen.ts";
|
||||
import { replaceInlineScripts } from "./windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
|
||||
import { readInlinePathSync } from "./utils.ts";
|
||||
import { Flow, FlowModule } from "./gen/types.gen.ts";
|
||||
|
||||
export interface FlowFile {
|
||||
summary: string;
|
||||
@@ -24,6 +23,55 @@ export interface FlowFile {
|
||||
|
||||
const alreadySynced: string[] = [];
|
||||
|
||||
export function replaceInlineScripts(
|
||||
modules: FlowModule[],
|
||||
localPath: string,
|
||||
removeLocks: string[] | undefined
|
||||
) {
|
||||
modules.forEach((m, i) => {
|
||||
if (!m.value) {
|
||||
throw Error(
|
||||
`Module value is undefined for flow module ${i} in ${localPath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (m.value.type == "rawscript") {
|
||||
if (m.value.content.startsWith("!inline")) {
|
||||
const path = m.value.content.split(" ")[1];
|
||||
m.value.content = Deno.readTextFileSync(localPath + path);
|
||||
const lock = m.value.lock;
|
||||
if (removeLocks && removeLocks.includes(path)) {
|
||||
m.value.lock = undefined;
|
||||
} else if (
|
||||
lock &&
|
||||
typeof lock == "string" &&
|
||||
lock.trimStart().startsWith("!inline ")
|
||||
) {
|
||||
const path = lock.split(" ")[1];
|
||||
try {
|
||||
m.value.lock = readInlinePathSync(localPath + path);
|
||||
} catch {
|
||||
log.error(`Lock file ${path} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (m.value.type == "forloopflow") {
|
||||
replaceInlineScripts(m.value.modules, localPath, removeLocks);
|
||||
} else if (m.value.type == "whileloopflow") {
|
||||
replaceInlineScripts(m.value.modules, localPath, removeLocks);
|
||||
} else if (m.value.type == "branchall") {
|
||||
m.value.branches.forEach((b) =>
|
||||
replaceInlineScripts(b.modules, localPath, removeLocks)
|
||||
);
|
||||
} else if (m.value.type == "branchone") {
|
||||
m.value.branches.forEach((b) =>
|
||||
replaceInlineScripts(b.modules, localPath, removeLocks)
|
||||
);
|
||||
replaceInlineScripts(m.value.default, localPath, removeLocks);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function pushFlow(
|
||||
workspace: string,
|
||||
remotePath: string,
|
||||
@@ -50,13 +98,7 @@ export async function pushFlow(
|
||||
}
|
||||
const localFlow = (await yamlParseFile(localPath + "flow.yaml")) as FlowFile;
|
||||
|
||||
await replaceInlineScripts(
|
||||
localFlow.value.modules,
|
||||
async (path: string) => await Deno.readTextFile(localPath + path),
|
||||
log,
|
||||
localPath,
|
||||
SEP,
|
||||
);
|
||||
replaceInlineScripts(localFlow.value.modules, localPath, undefined);
|
||||
|
||||
if (flow) {
|
||||
if (isSuperset(localFlow, flow)) {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eou pipefail
|
||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# only install gnu-sed if not already installed
|
||||
if ! command -v gsed &> /dev/null; then
|
||||
brew install gnu-sed
|
||||
fi
|
||||
|
||||
rm -rf "${script_dirpath}/gen"
|
||||
|
||||
npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../backend/windmill-api/openapi.yaml" --output "${script_dirpath}/gen" --useOptions --client legacy/fetch --schemas false
|
||||
cat <<EOF - gen/core/OpenAPI.ts > temp_file && mv temp_file gen/core/OpenAPI.ts
|
||||
const getEnv = (key: string) => {
|
||||
return Deno.env.get(key)
|
||||
};
|
||||
|
||||
const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000";
|
||||
const baseUrlApi = (baseUrl ?? '') + "/api";
|
||||
|
||||
EOF
|
||||
gsed -i 's/WITH_CREDENTIALS: false/WITH_CREDENTIALS: true/g' gen/core/OpenAPI.ts
|
||||
gsed -i 's/TOKEN: undefined/TOKEN: getEnv("WM_TOKEN")/g' gen/core/OpenAPI.ts
|
||||
gsed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" gen/core/OpenAPI.ts
|
||||
|
||||
find gen/ -name "*.ts" -exec gsed -i -E "s/(import.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
|
||||
|
||||
find gen/ -name "*.ts" -exec gsed -i -E "s/(export.*from[[:space:]]*['\"][^'\"]+)(['\"])/\1.ts\2/g" {} \;
|
||||
738
cli/main.ts
738
cli/main.ts
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
Command,
|
||||
CompletionsCommand,
|
||||
UpgradeCommand,
|
||||
colors,
|
||||
esMain,
|
||||
log,
|
||||
yamlStringify,
|
||||
Command,
|
||||
CompletionsCommand,
|
||||
UpgradeCommand,
|
||||
colors,
|
||||
esMain,
|
||||
log,
|
||||
yamlStringify,
|
||||
} from "./deps.ts";
|
||||
import flow from "./flow.ts";
|
||||
import app from "./apps.ts";
|
||||
@@ -40,26 +40,26 @@ import { readLockfile } from "./metadata.ts";
|
||||
import { FLOW_GUIDANCE } from "./flow_guidance.ts";
|
||||
|
||||
export {
|
||||
flow,
|
||||
app,
|
||||
script,
|
||||
workspace,
|
||||
resource,
|
||||
resourceType,
|
||||
user,
|
||||
variable,
|
||||
hub,
|
||||
folder,
|
||||
schedule,
|
||||
trigger,
|
||||
sync,
|
||||
gitsyncSettings,
|
||||
instance,
|
||||
dev,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
workspaceAdd,
|
||||
flow,
|
||||
app,
|
||||
script,
|
||||
workspace,
|
||||
resource,
|
||||
resourceType,
|
||||
user,
|
||||
variable,
|
||||
hub,
|
||||
folder,
|
||||
schedule,
|
||||
trigger,
|
||||
sync,
|
||||
gitsyncSettings,
|
||||
instance,
|
||||
dev,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
workspaceAdd,
|
||||
};
|
||||
|
||||
// addEventListener("error", (event) => {
|
||||
@@ -69,229 +69,232 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.517.0";
|
||||
export const VERSION = "1.515.1";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
.action(() =>
|
||||
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`)
|
||||
)
|
||||
.description("Windmill CLI")
|
||||
.name("wmill")
|
||||
.action(() =>
|
||||
log.info(`Welcome to Windmill CLI ${VERSION}. Use -h for help.`),
|
||||
)
|
||||
.description("Windmill CLI")
|
||||
|
||||
.globalOption(
|
||||
"--workspace <workspace:string>",
|
||||
"Specify the target workspace. This overrides the default workspace."
|
||||
)
|
||||
.globalOption("--debug --verbose", "Show debug/verbose logs")
|
||||
.globalOption(
|
||||
"--show-diffs",
|
||||
"Show diff informations when syncing (may show sensitive informations)"
|
||||
)
|
||||
.globalOption(
|
||||
"--token <token:string>",
|
||||
"Specify an API token. This will override any stored token."
|
||||
)
|
||||
.globalOption(
|
||||
"--base-url <baseUrl:string>",
|
||||
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used."
|
||||
)
|
||||
.globalOption(
|
||||
"--config-dir <configDir:string>",
|
||||
"Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location."
|
||||
)
|
||||
.env(
|
||||
"HEADERS <headers:string>",
|
||||
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\""
|
||||
)
|
||||
.version(VERSION)
|
||||
.versionOption(false)
|
||||
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
|
||||
.option("--use-default", "Use default settings without checking backend")
|
||||
.option("--use-backend", "Use backend git-sync settings if available")
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when using backend settings"
|
||||
)
|
||||
.action(
|
||||
async (opts: {
|
||||
useDefault?: boolean;
|
||||
useBackend?: boolean;
|
||||
repository?: string;
|
||||
workspace?: string;
|
||||
debug?: unknown;
|
||||
showDiffs?: boolean;
|
||||
token?: string;
|
||||
baseUrl?: string;
|
||||
configDir?: string;
|
||||
}) => {
|
||||
if (await Deno.stat("wmill.yaml").catch(() => null)) {
|
||||
log.error(colors.red("wmill.yaml already exists"));
|
||||
} else {
|
||||
// Import DEFAULT_SYNC_OPTIONS from conf.ts
|
||||
const { DEFAULT_SYNC_OPTIONS } = await import("./conf.ts");
|
||||
.globalOption(
|
||||
"--workspace <workspace:string>",
|
||||
"Specify the target workspace. This overrides the default workspace.",
|
||||
)
|
||||
.globalOption("--debug --verbose", "Show debug/verbose logs")
|
||||
.globalOption(
|
||||
"--show-diffs",
|
||||
"Show diff informations when syncing (may show sensitive informations)",
|
||||
)
|
||||
.globalOption(
|
||||
"--token <token:string>",
|
||||
"Specify an API token. This will override any stored token.",
|
||||
)
|
||||
.globalOption(
|
||||
"--base-url <baseUrl:string>",
|
||||
"Specify the base URL of the API. If used, --token and --workspace are required and no local remote/workspace already set will be used.",
|
||||
)
|
||||
.globalOption(
|
||||
"--config-dir <configDir:string>",
|
||||
"Specify a custom config directory. Overrides WMILL_CONFIG_DIR environment variable and default ~/.config location.",
|
||||
)
|
||||
.env(
|
||||
"HEADERS <headers:string>",
|
||||
"Specify headers to use for all requests. e.g: \"HEADERS='h1: v1, h2: v2'\"",
|
||||
)
|
||||
.version(VERSION)
|
||||
.versionOption(false)
|
||||
.command("init", "Bootstrap a windmill project with a wmill.yaml file")
|
||||
.option("--use-default", "Use default settings without checking backend")
|
||||
.option("--use-backend", "Use backend git-sync settings if available")
|
||||
.option(
|
||||
"--repository <repo:string>",
|
||||
"Specify repository path (e.g., u/user/repo) when using backend settings",
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
opts: {
|
||||
useDefault?: boolean;
|
||||
useBackend?: boolean;
|
||||
repository?: string;
|
||||
workspace?: string;
|
||||
debug?: unknown;
|
||||
showDiffs?: boolean;
|
||||
token?: string;
|
||||
baseUrl?: string;
|
||||
configDir?: string;
|
||||
},
|
||||
) => {
|
||||
if (await Deno.stat("wmill.yaml").catch(() => null)) {
|
||||
log.error(colors.red("wmill.yaml already exists"));
|
||||
} else {
|
||||
// Import DEFAULT_SYNC_OPTIONS from conf.ts
|
||||
const { DEFAULT_SYNC_OPTIONS } = await import("./conf.ts");
|
||||
|
||||
// Create initial config with defaults
|
||||
const initialConfig = {
|
||||
defaultTs: DEFAULT_SYNC_OPTIONS.defaultTs,
|
||||
includes: DEFAULT_SYNC_OPTIONS.includes,
|
||||
excludes: DEFAULT_SYNC_OPTIONS.excludes,
|
||||
codebases: DEFAULT_SYNC_OPTIONS.codebases,
|
||||
skipVariables: DEFAULT_SYNC_OPTIONS.skipVariables,
|
||||
skipResources: DEFAULT_SYNC_OPTIONS.skipResources,
|
||||
skipSecrets: DEFAULT_SYNC_OPTIONS.skipSecrets,
|
||||
skipScripts: DEFAULT_SYNC_OPTIONS.skipScripts,
|
||||
skipFlows: DEFAULT_SYNC_OPTIONS.skipFlows,
|
||||
skipApps: DEFAULT_SYNC_OPTIONS.skipApps,
|
||||
skipFolders: DEFAULT_SYNC_OPTIONS.skipFolders,
|
||||
includeSchedules: DEFAULT_SYNC_OPTIONS.includeSchedules,
|
||||
includeTriggers: DEFAULT_SYNC_OPTIONS.includeTriggers,
|
||||
overrides: {},
|
||||
};
|
||||
// Create initial config with defaults
|
||||
const initialConfig = {
|
||||
defaultTs: DEFAULT_SYNC_OPTIONS.defaultTs,
|
||||
includes: DEFAULT_SYNC_OPTIONS.includes,
|
||||
excludes: DEFAULT_SYNC_OPTIONS.excludes,
|
||||
codebases: DEFAULT_SYNC_OPTIONS.codebases,
|
||||
skipVariables: DEFAULT_SYNC_OPTIONS.skipVariables,
|
||||
skipResources: DEFAULT_SYNC_OPTIONS.skipResources,
|
||||
skipSecrets: DEFAULT_SYNC_OPTIONS.skipSecrets,
|
||||
skipScripts: DEFAULT_SYNC_OPTIONS.skipScripts,
|
||||
skipFlows: DEFAULT_SYNC_OPTIONS.skipFlows,
|
||||
skipApps: DEFAULT_SYNC_OPTIONS.skipApps,
|
||||
skipFolders: DEFAULT_SYNC_OPTIONS.skipFolders,
|
||||
includeSchedules: DEFAULT_SYNC_OPTIONS.includeSchedules,
|
||||
includeTriggers: DEFAULT_SYNC_OPTIONS.includeTriggers,
|
||||
overrides: {},
|
||||
};
|
||||
|
||||
await Deno.writeTextFile("wmill.yaml", yamlStringify(initialConfig));
|
||||
log.info(colors.green("wmill.yaml created with default settings"));
|
||||
await Deno.writeTextFile(
|
||||
"wmill.yaml",
|
||||
yamlStringify(initialConfig),
|
||||
);
|
||||
log.info(
|
||||
colors.green("wmill.yaml created with default settings"),
|
||||
);
|
||||
|
||||
// Create lock file
|
||||
await readLockfile();
|
||||
// Create lock file
|
||||
await readLockfile();
|
||||
|
||||
// Check for backend git-sync settings unless --use-default is specified
|
||||
if (!opts.useDefault) {
|
||||
try {
|
||||
const { requireLogin } = await import("./auth.ts");
|
||||
const { resolveWorkspace } = await import("./context.ts");
|
||||
// Check for backend git-sync settings unless --use-default is specified
|
||||
if (!opts.useDefault) {
|
||||
try {
|
||||
const { requireLogin } = await import("./auth.ts");
|
||||
const { resolveWorkspace } = await import("./context.ts");
|
||||
|
||||
// Check if user has workspace configured
|
||||
const { getActiveWorkspace } = await import("./workspace.ts");
|
||||
const activeWorkspace = await getActiveWorkspace(
|
||||
opts as GlobalOptions
|
||||
);
|
||||
// Check if user has workspace configured
|
||||
const { getActiveWorkspace } = await import(
|
||||
"./workspace.ts"
|
||||
);
|
||||
const activeWorkspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
|
||||
if (!activeWorkspace) {
|
||||
log.info("No workspace configured. Using default settings.");
|
||||
log.info(
|
||||
"You can configure a workspace later with 'wmill workspace add'"
|
||||
);
|
||||
return;
|
||||
if (!activeWorkspace) {
|
||||
log.info(
|
||||
"No workspace configured. Using default settings.",
|
||||
);
|
||||
log.info(
|
||||
"You can configure a workspace later with 'wmill workspace add'",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await requireLogin(opts as GlobalOptions);
|
||||
const workspace = await resolveWorkspace(opts as GlobalOptions);
|
||||
|
||||
const wmill = await import("./gen/services.gen.ts");
|
||||
const settings = await wmill.getSettings({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
settings.git_sync?.repositories &&
|
||||
settings.git_sync.repositories.length > 0
|
||||
) {
|
||||
let useBackendSettings = opts.useBackend;
|
||||
|
||||
// If repository is specified, implicitly use backend settings
|
||||
if (opts.repository && !opts.useDefault) {
|
||||
useBackendSettings = true;
|
||||
}
|
||||
|
||||
if (useBackendSettings === undefined) {
|
||||
// Interactive prompt
|
||||
const { Select } = await import("./deps.ts");
|
||||
const choice = await Select.prompt({
|
||||
message:
|
||||
"Git-sync settings found on backend. What would you like to do?",
|
||||
options: [
|
||||
{
|
||||
name: "Use backend git-sync settings",
|
||||
value: "backend",
|
||||
},
|
||||
{
|
||||
name: "Use default settings",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Cancel",
|
||||
value: "cancel",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (choice === "cancel") {
|
||||
// Clean up the created files
|
||||
try {
|
||||
await Deno.remove("wmill.yaml");
|
||||
await Deno.remove("wmill-lock.yaml");
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
log.info("Init cancelled");
|
||||
Deno.exit(0);
|
||||
}
|
||||
|
||||
useBackendSettings = choice === "backend";
|
||||
}
|
||||
|
||||
if (useBackendSettings) {
|
||||
log.info(
|
||||
"Applying git-sync settings from backend...",
|
||||
);
|
||||
|
||||
// Import and run the pull git-sync settings logic
|
||||
const { pullGitSyncSettings } = await import(
|
||||
"./gitsync-settings.ts"
|
||||
);
|
||||
await pullGitSyncSettings({
|
||||
...(opts as GlobalOptions),
|
||||
repository: opts.repository,
|
||||
jsonOutput: false,
|
||||
diff: false,
|
||||
replace: true, // Auto-replace when using backend settings during init
|
||||
});
|
||||
|
||||
log.info(
|
||||
colors.green(
|
||||
"Git-sync settings applied from backend",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error checking backend settings, just continue with defaults
|
||||
log.warn(
|
||||
`Could not check backend for git-sync settings: ${error.message}`,
|
||||
);
|
||||
log.info("Continuing with default settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await requireLogin(opts as GlobalOptions);
|
||||
const workspace = await resolveWorkspace(opts as GlobalOptions);
|
||||
|
||||
const wmill = await import("./gen/services.gen.ts");
|
||||
const settings = await wmill.getSettings({
|
||||
workspace: workspace.workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
settings.git_sync?.repositories &&
|
||||
settings.git_sync.repositories.length > 0
|
||||
) {
|
||||
let useBackendSettings = opts.useBackend;
|
||||
|
||||
// If repository is specified, implicitly use backend settings
|
||||
if (opts.repository && !opts.useDefault) {
|
||||
useBackendSettings = true;
|
||||
}
|
||||
|
||||
if (useBackendSettings === undefined) {
|
||||
// Interactive prompt
|
||||
const { Select } = await import("./deps.ts");
|
||||
const choice = await Select.prompt({
|
||||
message:
|
||||
"Git-sync settings found on backend. What would you like to do?",
|
||||
options: [
|
||||
{
|
||||
name: "Use backend git-sync settings",
|
||||
value: "backend",
|
||||
},
|
||||
{
|
||||
name: "Use default settings",
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
name: "Cancel",
|
||||
value: "cancel",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (choice === "cancel") {
|
||||
// Clean up the created files
|
||||
try {
|
||||
await Deno.remove("wmill.yaml");
|
||||
await Deno.remove("wmill-lock.yaml");
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
log.info("Init cancelled");
|
||||
Deno.exit(0);
|
||||
// Create .cursor/rules directory and files with SCRIPT_GUIDANCE content
|
||||
try {
|
||||
const scriptGuidanceContent = SCRIPT_GUIDANCE;
|
||||
const flowGuidanceContent = FLOW_GUIDANCE;
|
||||
|
||||
// Create .cursor/rules directory
|
||||
await Deno.mkdir(".cursor/rules", { recursive: true });
|
||||
|
||||
// Create windmill.mdc file
|
||||
if (!await Deno.stat(".cursor/rules/script.mdc").catch(() => null)) {
|
||||
await Deno.writeTextFile(".cursor/rules/script.mdc", scriptGuidanceContent);
|
||||
log.info(colors.green("Created .cursor/rules/script.mdc"));
|
||||
}
|
||||
|
||||
useBackendSettings = choice === "backend";
|
||||
}
|
||||
|
||||
if (useBackendSettings) {
|
||||
log.info("Applying git-sync settings from backend...");
|
||||
|
||||
// Import and run the pull git-sync settings logic
|
||||
const { pullGitSyncSettings } = await import(
|
||||
"./gitsync-settings.ts"
|
||||
);
|
||||
await pullGitSyncSettings({
|
||||
...(opts as GlobalOptions),
|
||||
repository: opts.repository,
|
||||
jsonOutput: false,
|
||||
diff: false,
|
||||
replace: true, // Auto-replace when using backend settings during init
|
||||
});
|
||||
|
||||
log.info(
|
||||
colors.green("Git-sync settings applied from backend")
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error checking backend settings, just continue with defaults
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
log.warn(
|
||||
`Could not check backend for git-sync settings: ${errorMessage}`
|
||||
);
|
||||
log.info("Continuing with default settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create .cursor/rules directory and files with SCRIPT_GUIDANCE content
|
||||
try {
|
||||
const scriptGuidanceContent = SCRIPT_GUIDANCE;
|
||||
const flowGuidanceContent = FLOW_GUIDANCE;
|
||||
|
||||
// Create .cursor/rules directory
|
||||
await Deno.mkdir(".cursor/rules", { recursive: true });
|
||||
|
||||
// Create windmill.mdc file
|
||||
if (!(await Deno.stat(".cursor/rules/script.mdc").catch(() => null))) {
|
||||
await Deno.writeTextFile(
|
||||
".cursor/rules/script.mdc",
|
||||
scriptGuidanceContent
|
||||
);
|
||||
log.info(colors.green("Created .cursor/rules/script.mdc"));
|
||||
}
|
||||
|
||||
if (!(await Deno.stat(".cursor/rules/flow.mdc").catch(() => null))) {
|
||||
await Deno.writeTextFile(
|
||||
".cursor/rules/flow.mdc",
|
||||
flowGuidanceContent
|
||||
);
|
||||
log.info(colors.green("Created .cursor/rules/flow.mdc"));
|
||||
}
|
||||
|
||||
// Create CLAUDE.md file
|
||||
if (!(await Deno.stat("CLAUDE.md").catch(() => null))) {
|
||||
await Deno.writeTextFile(
|
||||
"CLAUDE.md",
|
||||
`
|
||||
if (!await Deno.stat(".cursor/rules/flow.mdc").catch(() => null)) {
|
||||
await Deno.writeTextFile(".cursor/rules/flow.mdc", flowGuidanceContent);
|
||||
log.info(colors.green("Created .cursor/rules/flow.mdc"));
|
||||
}
|
||||
|
||||
// Create CLAUDE.md file
|
||||
if (!await Deno.stat("CLAUDE.md").catch(() => null)) {
|
||||
await Deno.writeTextFile("CLAUDE.md", `
|
||||
# Claude
|
||||
|
||||
You are a helpful assistant that can help with Windmill scripts and flows creation.
|
||||
@@ -301,161 +304,162 @@ const command = new Command()
|
||||
|
||||
## Flow Guidance
|
||||
${flowGuidanceContent}
|
||||
`
|
||||
);
|
||||
log.info(colors.green("Created CLAUDE.md"));
|
||||
`);
|
||||
log.info(colors.green("Created CLAUDE.md"));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
log.warn(`Could not create guidance files: ${error.message}`);
|
||||
} else {
|
||||
log.warn(`Could not create guidance files: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
)
|
||||
.command("app", app)
|
||||
.command("flow", flow)
|
||||
.command("script", script)
|
||||
.command("workspace", workspace)
|
||||
.command("resource", resource)
|
||||
.command("resource-type", resourceType)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("hub", hub)
|
||||
.command("folder", folder)
|
||||
.command("schedule", schedule)
|
||||
.command("trigger", trigger)
|
||||
.command("dev", dev)
|
||||
.command("sync", sync)
|
||||
.command("gitsync-settings", gitsyncSettings)
|
||||
.command("instance", instance)
|
||||
.command("worker-groups", workerGroups)
|
||||
.command("workers", workers)
|
||||
.command("queues", queues)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
try {
|
||||
const provider = new NpmProvider({ package: "windmill-cli" });
|
||||
const versions = await provider.getVersions("windmill-cli");
|
||||
if (versions.latest !== VERSION) {
|
||||
console.log(
|
||||
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`,
|
||||
);
|
||||
} else {
|
||||
console.log("CLI is up to date");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
log.warn(`Could not create guidance files: ${error.message}`);
|
||||
const workspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
if (workspace) {
|
||||
try {
|
||||
const backendVersion = await fetchVersion(workspace.remote);
|
||||
console.log("Backend Version: " + backendVersion);
|
||||
} catch (e) {
|
||||
console.warn("Cannot fetch backend version: " + e);
|
||||
}
|
||||
} else {
|
||||
log.warn(`Could not create guidance files: ${error}`);
|
||||
console.warn(
|
||||
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.command("app", app)
|
||||
.command("flow", flow)
|
||||
.command("script", script)
|
||||
.command("workspace", workspace)
|
||||
.command("resource", resource)
|
||||
.command("resource-type", resourceType)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("hub", hub)
|
||||
.command("folder", folder)
|
||||
.command("schedule", schedule)
|
||||
.command("trigger", trigger)
|
||||
.command("dev", dev)
|
||||
.command("sync", sync)
|
||||
.command("gitsync-settings", gitsyncSettings)
|
||||
.command("instance", instance)
|
||||
.command("worker-groups", workerGroups)
|
||||
.command("workers", workers)
|
||||
.command("queues", queues)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
try {
|
||||
const provider = new NpmProvider({ package: "windmill-cli" });
|
||||
const versions = await provider.getVersions("windmill-cli");
|
||||
if (versions.latest !== VERSION) {
|
||||
console.log(
|
||||
`CLI is outdated. Latest version ${versions.latest} is available. Run \`wmill upgrade\` to update.`
|
||||
);
|
||||
} else {
|
||||
console.log("CLI is up to date");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cannot fetch latest CLI version on npmjs to check if up-to-date: ${e}`
|
||||
);
|
||||
}
|
||||
const workspace = await getActiveWorkspace(opts as GlobalOptions);
|
||||
if (workspace) {
|
||||
try {
|
||||
const backendVersion = await fetchVersion(workspace.remote);
|
||||
console.log("Backend Version: " + backendVersion);
|
||||
} catch (e) {
|
||||
console.warn("Cannot fetch backend version: " + e);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Cannot fetch backend version: no active workspace selected, choose one to pick a remote to fetch version of"
|
||||
);
|
||||
}
|
||||
})
|
||||
.command(
|
||||
"upgrade",
|
||||
new UpgradeCommand({
|
||||
provider: new NpmProvider({ package: "windmill-cli" }),
|
||||
}).error((e: any) => {
|
||||
log.error(e);
|
||||
log.info(
|
||||
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli"
|
||||
);
|
||||
})
|
||||
)
|
||||
.command("completions", new CompletionsCommand());
|
||||
.command(
|
||||
"upgrade",
|
||||
new UpgradeCommand({
|
||||
provider: new NpmProvider({ package: "windmill-cli" }),
|
||||
}).error((e) => {
|
||||
log.error(e);
|
||||
log.info(
|
||||
"Try running with sudo and otherwise check the result of the command: npm uninstall windmill-cli && npm install -g windmill-cli",
|
||||
);
|
||||
}),
|
||||
)
|
||||
.command("completions", new CompletionsCommand());
|
||||
|
||||
export let showDiffs = false;
|
||||
|
||||
let isWin: boolean | undefined = undefined;
|
||||
|
||||
export async function getIsWin() {
|
||||
if (isWin === undefined) {
|
||||
const os = await import("node:os");
|
||||
isWin = os.platform() === "win32";
|
||||
}
|
||||
return isWin;
|
||||
if (isWin === undefined) {
|
||||
const os = await import("node:os");
|
||||
isWin = os.platform() === "win32";
|
||||
}
|
||||
return isWin;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
if (Deno.args.length === 0) {
|
||||
command.showHelp();
|
||||
}
|
||||
const LOG_LEVEL =
|
||||
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
|
||||
? "DEBUG"
|
||||
: "INFO";
|
||||
// const NO_COLORS = Deno.args.includes("--no-colors");
|
||||
showDiffs = Deno.args.includes("--show-diffs");
|
||||
try {
|
||||
if (Deno.args.length === 0) {
|
||||
command.showHelp();
|
||||
}
|
||||
const LOG_LEVEL =
|
||||
Deno.args.includes("--verbose") || Deno.args.includes("--debug")
|
||||
? "DEBUG"
|
||||
: "INFO";
|
||||
// const NO_COLORS = Deno.args.includes("--no-colors");
|
||||
showDiffs = Deno.args.includes("--show-diffs");
|
||||
|
||||
log.setup({
|
||||
handlers: {
|
||||
console: new log.ConsoleHandler(LOG_LEVEL, {
|
||||
formatter: ({ msg }) => `${msg}`,
|
||||
useColors: isWin ? false : true,
|
||||
}),
|
||||
},
|
||||
loggers: {
|
||||
default: {
|
||||
level: LOG_LEVEL,
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
log.debug("Debug logging enabled. CLI build against " + VERSION);
|
||||
log.setup({
|
||||
handlers: {
|
||||
console: new log.ConsoleHandler(LOG_LEVEL, {
|
||||
formatter: ({ msg }) => `${msg}`,
|
||||
useColors: isWin ? false : true,
|
||||
}),
|
||||
},
|
||||
loggers: {
|
||||
default: {
|
||||
level: LOG_LEVEL,
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
log.debug("Debug logging enabled. CLI build against " + VERSION);
|
||||
|
||||
const extraHeaders = getHeaders();
|
||||
if (extraHeaders) {
|
||||
OpenAPI.HEADERS = extraHeaders;
|
||||
const extraHeaders = getHeaders();
|
||||
if (extraHeaders) {
|
||||
OpenAPI.HEADERS = extraHeaders;
|
||||
}
|
||||
await command.parse(Deno.args);
|
||||
} catch (e) {
|
||||
if (e.name === "ApiError") {
|
||||
console.log("Server failed. " + e.statusText + ": " + e.body);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
await command.parse(Deno.args);
|
||||
} catch (e) {
|
||||
if (e.name === "ApiError") {
|
||||
console.log("Server failed. " + e.statusText + ": " + e.body);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function isMain() {
|
||||
// dnt-shim-ignore
|
||||
const { Deno } = globalThis as any;
|
||||
// dnt-shim-ignore
|
||||
const { Deno } = globalThis as any;
|
||||
|
||||
const isDeno = Deno != undefined;
|
||||
const isDeno = Deno != undefined;
|
||||
|
||||
if (isDeno) {
|
||||
const isMain = import.meta.main;
|
||||
if (isMain) {
|
||||
if (!Deno.args.includes("completions")) {
|
||||
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
|
||||
log.warn(
|
||||
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true"
|
||||
);
|
||||
if (isDeno) {
|
||||
const isMain = import.meta.main;
|
||||
if (isMain) {
|
||||
if (!Deno.args.includes("completions")) {
|
||||
if (Deno.env.get("SKIP_DENO_DEPRECATION_WARNING") !== "true") {
|
||||
log.warn(
|
||||
"Using the deno runtime for the Windmill CLI is deprecated, you can now use node: deno uninstall wmill && npm install -g windmill-cli. To skip this warning set SKIP_DENO_DEPRECATION_WARNING=true",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return isMain;
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return esMain.default(import.meta);
|
||||
}
|
||||
return isMain;
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return esMain.default(import.meta);
|
||||
}
|
||||
}
|
||||
if (isMain()) {
|
||||
main();
|
||||
main();
|
||||
}
|
||||
|
||||
export default command;
|
||||
|
||||
306
cli/metadata.ts
306
cli/metadata.ts
@@ -13,6 +13,7 @@ import {
|
||||
defaultScriptMetadata,
|
||||
} from "./bootstrap/script_bootstrap.ts";
|
||||
import { Workspace } from "./workspace.ts";
|
||||
import { SchemaProperty } from "./bootstrap/common.ts";
|
||||
import {
|
||||
languagesWithRawReqsSupport,
|
||||
LanguageWithRawReqsSupport,
|
||||
@@ -20,13 +21,16 @@ import {
|
||||
} from "./script_common.ts";
|
||||
import { inferContentTypeFromFilePath } from "./script_common.ts";
|
||||
import { GlobalDeps, exts, findGlobalDeps } from "./script.ts";
|
||||
import { FSFSElement, findCodebase, yamlOptions } from "./sync.ts";
|
||||
import {
|
||||
FSFSElement,
|
||||
extractInlineScriptsForFlows,
|
||||
findCodebase,
|
||||
newPathAssigner,
|
||||
yamlOptions,
|
||||
} from "./sync.ts";
|
||||
import { generateHash, readInlinePathSync } from "./utils.ts";
|
||||
import { SyncCodebase } from "./codebase.ts";
|
||||
import { FlowFile } from "./flow.ts";
|
||||
import { replaceInlineScripts } from "./windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
import { extractInlineScripts as extractInlineScriptsForFlows } from "./windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
import { argSigToJsonSchemaType } from "./windmill-utils-internal/src/parse/parse-schema.ts";
|
||||
import { FlowFile, replaceInlineScripts } from "./flow.ts";
|
||||
import { getIsWin } from "./main.ts";
|
||||
import { FlowValue } from "./gen/types.gen.ts";
|
||||
|
||||
@@ -168,15 +172,10 @@ export async function generateFlowLockInternal(
|
||||
}
|
||||
|
||||
log.info(`Recomputing locks of ${changedScripts.join(", ")} in ${folder}`);
|
||||
await replaceInlineScripts(
|
||||
replaceInlineScripts(
|
||||
flowValue.value.modules,
|
||||
async (path: string) => await Deno.readTextFile(folder + SEP + path),
|
||||
log,
|
||||
folder + SEP!,
|
||||
SEP,
|
||||
changedScripts,
|
||||
(path: string, newPath: string) => Deno.renameSync(path, newPath),
|
||||
(path: string) => Deno.removeSync(path)
|
||||
changedScripts
|
||||
);
|
||||
|
||||
//removeChangedLocks
|
||||
@@ -189,8 +188,7 @@ export async function generateFlowLockInternal(
|
||||
|
||||
const inlineScripts = extractInlineScriptsForFlows(
|
||||
flowValue.value.modules,
|
||||
{},
|
||||
SEP
|
||||
newPathAssigner(opts.defaultTs ?? "bun")
|
||||
);
|
||||
inlineScripts
|
||||
.filter((s) => s.path.endsWith(".lock"))
|
||||
@@ -531,7 +529,9 @@ export async function inferSchema(
|
||||
}> {
|
||||
let inferedSchema: any;
|
||||
if (language === "python3") {
|
||||
const { parse_python } = await import("./wasm/py/windmill_parser_wasm.js");
|
||||
const { parse_python } = await import(
|
||||
"./wasm/py/windmill_parser_wasm.js"
|
||||
);
|
||||
inferedSchema = JSON.parse(parse_python(content));
|
||||
} else if (language === "nativets") {
|
||||
const { parse_deno } = await import("./wasm/ts/windmill_parser_wasm.js");
|
||||
@@ -599,9 +599,7 @@ export async function inferSchema(
|
||||
...inferedSchema.args,
|
||||
];
|
||||
} else if (language === "duckdb") {
|
||||
const { parse_duckdb } = await import(
|
||||
"./wasm/regex/windmill_parser_wasm.js"
|
||||
);
|
||||
const { parse_duckdb } = await import("./wasm/regex/windmill_parser_wasm.js");
|
||||
inferedSchema = JSON.parse(parse_duckdb(content));
|
||||
} else if (language === "graphql") {
|
||||
const { parse_graphql } = await import(
|
||||
@@ -706,6 +704,234 @@ function sortObject(obj: any): any {
|
||||
);
|
||||
}
|
||||
|
||||
//copied straight fron frontend /src/utils/inferArgs.ts
|
||||
export function argSigToJsonSchemaType(
|
||||
t:
|
||||
| string
|
||||
| { resource: string | null }
|
||||
| {
|
||||
list:
|
||||
| (
|
||||
| string
|
||||
| {
|
||||
object: {
|
||||
name?: string;
|
||||
props?: { key: string; typ: any }[];
|
||||
};
|
||||
}
|
||||
)
|
||||
| { str: any }
|
||||
| { object: { name?: string; props?: { key: string; typ: any }[] } }
|
||||
| null;
|
||||
}
|
||||
| { dynselect: string }
|
||||
| { str: string[] | null }
|
||||
| { object: { name?: string; props?: { key: string; typ: any }[] } }
|
||||
| {
|
||||
oneof: {
|
||||
label: string;
|
||||
properties: { key: string; typ: any }[];
|
||||
}[];
|
||||
},
|
||||
oldS: SchemaProperty
|
||||
): void {
|
||||
const newS: SchemaProperty = { type: "" };
|
||||
if (t === "int") {
|
||||
newS.type = "integer";
|
||||
} else if (t === "float") {
|
||||
newS.type = "number";
|
||||
} else if (t === "bool") {
|
||||
newS.type = "boolean";
|
||||
} else if (t === "email") {
|
||||
newS.type = "string";
|
||||
newS.format = "email";
|
||||
} else if (t === "sql") {
|
||||
newS.type = "string";
|
||||
newS.format = "sql";
|
||||
} else if (t === "yaml") {
|
||||
newS.type = "string";
|
||||
newS.format = "yaml";
|
||||
} else if (t === "bytes") {
|
||||
newS.type = "string";
|
||||
newS.contentEncoding = "base64";
|
||||
newS.originalType = "bytes";
|
||||
} else if (t === "datetime") {
|
||||
newS.type = "string";
|
||||
newS.format = "date-time";
|
||||
} else if (typeof t !== "string" && "oneof" in t) {
|
||||
newS.type = "object";
|
||||
if (t.oneof) {
|
||||
newS.oneOf = t.oneof.map((obj) => {
|
||||
const oldObjS =
|
||||
oldS.oneOf?.find((o) => o?.title === obj.label) ?? undefined;
|
||||
const properties: Record<string, any> = {};
|
||||
for (const prop of obj.properties) {
|
||||
if (oldObjS?.properties && prop.key in oldObjS?.properties) {
|
||||
properties[prop.key] = oldObjS?.properties[prop.key];
|
||||
} else {
|
||||
properties[prop.key] = { description: "", type: "" };
|
||||
}
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
||||
}
|
||||
return {
|
||||
type: "object",
|
||||
title: obj.label,
|
||||
properties,
|
||||
order: oldObjS?.order ?? undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
} else if (typeof t !== "string" && `object` in t) {
|
||||
newS.type = "object";
|
||||
if (t.object.name) {
|
||||
newS.format = `resource-${t.object.name}`;
|
||||
}
|
||||
if (t.object.props) {
|
||||
const properties: Record<string, any> = {};
|
||||
for (const prop of t.object.props) {
|
||||
if (oldS.properties && prop.key in oldS.properties) {
|
||||
properties[prop.key] = oldS.properties[prop.key];
|
||||
} else {
|
||||
properties[prop.key] = { description: "", type: "" };
|
||||
}
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
||||
}
|
||||
newS.properties = properties;
|
||||
}
|
||||
} else if (typeof t !== "string" && `str` in t) {
|
||||
newS.type = "string";
|
||||
if (t.str) {
|
||||
newS.originalType = "enum";
|
||||
newS.enum = t.str;
|
||||
} else if (oldS.originalType == "string" && oldS.enum) {
|
||||
newS.originalType = "string";
|
||||
newS.enum = oldS.enum;
|
||||
} else {
|
||||
newS.originalType = "string";
|
||||
newS.enum = undefined;
|
||||
}
|
||||
} else if (typeof t !== "string" && `resource` in t) {
|
||||
newS.type = "object";
|
||||
newS.format = `resource-${t.resource}`;
|
||||
} else if (typeof t !== "string" && `dynselect` in t) {
|
||||
newS.type = "object";
|
||||
newS.format = `dynselect-${t.dynselect}`;
|
||||
} else if (typeof t !== "string" && `list` in t) {
|
||||
newS.type = "array";
|
||||
if (t.list === "int" || t.list === "float") {
|
||||
newS.items = { type: "number" };
|
||||
newS.originalType = "number[]";
|
||||
} else if (t.list === "bytes") {
|
||||
newS.items = { type: "string", contentEncoding: "base64" };
|
||||
newS.originalType = "bytes[]";
|
||||
} else if (
|
||||
t.list &&
|
||||
typeof t.list == "object" &&
|
||||
"str" in t.list &&
|
||||
t.list.str
|
||||
) {
|
||||
newS.items = { type: "string", enum: t.list.str };
|
||||
newS.originalType = "enum[]";
|
||||
} else if (
|
||||
t.list == "string" ||
|
||||
(t.list && typeof t.list == "object" && "str" in t.list)
|
||||
) {
|
||||
newS.items = { type: "string", enum: oldS.items?.enum };
|
||||
newS.originalType = "string[]";
|
||||
} else if (
|
||||
t.list &&
|
||||
typeof t.list == "object" &&
|
||||
"resource" in t.list &&
|
||||
t.list.resource
|
||||
) {
|
||||
newS.items = {
|
||||
type: "resource",
|
||||
resourceType: t.list.resource as string,
|
||||
};
|
||||
newS.originalType = "resource[]";
|
||||
} else if (
|
||||
t.list &&
|
||||
typeof t.list == "object" &&
|
||||
"object" in t.list &&
|
||||
t.list.object
|
||||
) {
|
||||
if (t.list.object.name) {
|
||||
newS.format = `resource-${t.list.object.name}`;
|
||||
}
|
||||
if (t.list.object.props && t.list.object.props.length > 0) {
|
||||
const properties: Record<string, any> = {};
|
||||
for (const prop of t.list.object.props) {
|
||||
properties[prop.key] = { description: "", type: "" };
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
|
||||
}
|
||||
newS.items = { type: "object", properties: properties };
|
||||
} else {
|
||||
newS.items = { type: "object" };
|
||||
}
|
||||
newS.originalType = "record[]";
|
||||
} else {
|
||||
newS.items = { type: "object" };
|
||||
newS.originalType = "object[]";
|
||||
}
|
||||
} else {
|
||||
newS.type = "object";
|
||||
}
|
||||
|
||||
const preservedFields = [
|
||||
"description",
|
||||
"pattern",
|
||||
"min",
|
||||
"max",
|
||||
"currency",
|
||||
"currencyLocale",
|
||||
"multiselect",
|
||||
"customErrorMessage",
|
||||
"required",
|
||||
"showExpr",
|
||||
"password",
|
||||
"order",
|
||||
"dateFormat",
|
||||
"title",
|
||||
"placeholder",
|
||||
];
|
||||
|
||||
preservedFields.forEach((field) => {
|
||||
// @ts-ignore
|
||||
if (oldS[field] !== undefined) {
|
||||
// @ts-ignore
|
||||
newS[field] = oldS[field];
|
||||
}
|
||||
});
|
||||
|
||||
if (oldS.type != newS.type) {
|
||||
for (const prop of Object.getOwnPropertyNames(newS)) {
|
||||
if (prop != "description") {
|
||||
// @ts-ignore
|
||||
delete oldS[prop];
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
(oldS.format == "date" || oldS.format === "date-time") &&
|
||||
newS.format == "string"
|
||||
) {
|
||||
newS.format = oldS.format;
|
||||
} else if (newS.format == "date-time" && oldS.format == "date") {
|
||||
newS.format = "date";
|
||||
} else if (oldS.items?.type != newS.items?.type) {
|
||||
delete oldS.items;
|
||||
}
|
||||
|
||||
if (oldS.format && !newS.format) {
|
||||
oldS.format = undefined
|
||||
}
|
||||
|
||||
Object.assign(oldS, newS);
|
||||
// if (sameItems && savedItems != undefined && savedItems.enum != undefined) {
|
||||
// sendUserToast(JSON.stringify(savedItems))
|
||||
// oldS.items = savedItems
|
||||
// }
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// end of refactoring TODO //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -816,7 +1042,6 @@ export async function parseMetadataFile(
|
||||
}
|
||||
|
||||
interface Lock {
|
||||
version?: "v2";
|
||||
locks?: { [path: string]: string | { [subpath: string]: string } };
|
||||
}
|
||||
|
||||
@@ -830,7 +1055,7 @@ export async function readLockfile(): Promise<Lock> {
|
||||
throw new Error("Invalid lockfile");
|
||||
}
|
||||
} catch {
|
||||
const lock = { locks: {}, version: "v2" as const };
|
||||
const lock = { locks: {} };
|
||||
await Deno.writeTextFile(WMILL_LOCKFILE, yamlStringify(lock, yamlOptions));
|
||||
log.info(colors.green("wmill-lock.yaml created"));
|
||||
|
||||
@@ -838,13 +1063,6 @@ export async function readLockfile(): Promise<Lock> {
|
||||
}
|
||||
}
|
||||
|
||||
function v2LockPath(path: string, subpath?: string) {
|
||||
if (subpath) {
|
||||
return `${path}+${subpath}`;
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
export async function checkifMetadataUptodate(
|
||||
path: string,
|
||||
hash: string,
|
||||
@@ -857,16 +1075,9 @@ export async function checkifMetadataUptodate(
|
||||
if (!conf.locks) {
|
||||
return false;
|
||||
}
|
||||
const isV2 = conf?.version == "v2";
|
||||
|
||||
if (isV2) {
|
||||
const current = conf.locks?.[v2LockPath(path, subpath)];
|
||||
return current == hash;
|
||||
} else {
|
||||
const obj = conf.locks?.[path];
|
||||
const current = subpath && typeof obj == "object" ? obj?.[subpath] : obj;
|
||||
return current == hash;
|
||||
}
|
||||
const obj = conf.locks?.[path];
|
||||
const current = subpath && typeof obj == "object" ? obj?.[subpath] : obj;
|
||||
return current == hash;
|
||||
}
|
||||
|
||||
export async function generateScriptHash(
|
||||
@@ -888,21 +1099,16 @@ export async function updateMetadataGlobalLock(
|
||||
if (!conf?.locks) {
|
||||
conf.locks = {};
|
||||
}
|
||||
const isV2 = conf?.version == "v2";
|
||||
|
||||
if (isV2) {
|
||||
conf.locks[v2LockPath(path, hash)] = hash;
|
||||
} else {
|
||||
if (subpath) {
|
||||
let prev: any = conf.locks[path];
|
||||
if (!prev || typeof prev != "object") {
|
||||
prev = {};
|
||||
conf.locks[path] = prev;
|
||||
}
|
||||
prev[subpath] = hash;
|
||||
} else {
|
||||
conf.locks[path] = hash;
|
||||
if (subpath) {
|
||||
let prev: any = conf.locks[path];
|
||||
if (!prev || typeof prev != "object") {
|
||||
prev = {};
|
||||
conf.locks[path] = prev;
|
||||
}
|
||||
prev[subpath] = hash;
|
||||
} else {
|
||||
conf.locks[path] = hash;
|
||||
}
|
||||
await Deno.writeTextFile(
|
||||
WMILL_LOCKFILE,
|
||||
|
||||
240
cli/sync.ts
240
cli/sync.ts
@@ -4,6 +4,7 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
Confirm,
|
||||
Input,
|
||||
Select,
|
||||
ensureDir,
|
||||
minimatch,
|
||||
@@ -36,8 +37,8 @@ import {
|
||||
} from "./script.ts";
|
||||
|
||||
import { handleFile } from "./script.ts";
|
||||
import { deepEqual, isFileResource } from "./utils.ts";
|
||||
import { SyncOptions, readConfigFile, getEffectiveSettings } from "./conf.ts";
|
||||
import { deepEqual, isFileResource, Repository, selectRepository } from "./utils.ts";
|
||||
import { SyncOptions, mergeConfigWithConfigFile, readConfigFile, getEffectiveSettings } from "./conf.ts";
|
||||
import { Workspace } from "./workspace.ts";
|
||||
import { removePathPrefix } from "./types.ts";
|
||||
import { SyncCodebase, listSyncCodebases } from "./codebase.ts";
|
||||
@@ -46,15 +47,15 @@ import {
|
||||
generateScriptMetadataInternal,
|
||||
readLockfile,
|
||||
} from "./metadata.ts";
|
||||
import { OpenFlow } from "./gen/types.gen.ts";
|
||||
import { FlowModule, OpenFlow, RawScript } from "./gen/types.gen.ts";
|
||||
import { pushResource } from "./resource.ts";
|
||||
import { assignPath } from "./windmill-utils-internal/src/path-utils/path-assigner.ts";
|
||||
import { extractInlineScripts as extractInlineScriptsForFlows } from "./windmill-utils-internal/src/inline-scripts/extractor.ts";
|
||||
|
||||
|
||||
// Merge CLI options with effective settings, preserving CLI flags as overrides
|
||||
function mergeCliWithEffectiveOptions<
|
||||
T extends GlobalOptions & SyncOptions & { repository?: string }
|
||||
>(cliOpts: T, effectiveOpts: SyncOptions): T {
|
||||
function mergeCliWithEffectiveOptions<T extends GlobalOptions & SyncOptions & { repository?: string }>(
|
||||
cliOpts: T,
|
||||
effectiveOpts: SyncOptions
|
||||
): T {
|
||||
// overlay CLI options on top (undefined cliOpts won't override effectiveOpts)
|
||||
return Object.assign({}, effectiveOpts, cliOpts) as T;
|
||||
}
|
||||
@@ -83,7 +84,7 @@ async function resolveEffectiveSyncOptions(
|
||||
|
||||
// Find all repository-specific overrides for this workspace
|
||||
for (const key of Object.keys(localConfig.overrides)) {
|
||||
if (key.startsWith(prefix) && !key.endsWith(":*")) {
|
||||
if (key.startsWith(prefix) && !key.endsWith(':*')) {
|
||||
const repo = key.substring(prefix.length);
|
||||
if (repo) {
|
||||
applicableRepos.push(repo);
|
||||
@@ -106,16 +107,13 @@ async function resolveEffectiveSyncOptions(
|
||||
|
||||
if (isInteractive) {
|
||||
const choices = [
|
||||
{
|
||||
name: "Use top-level settings (no repository-specific override)",
|
||||
value: "",
|
||||
},
|
||||
...applicableRepos.map((repo) => ({ name: repo, value: repo })),
|
||||
{ name: "Use top-level settings (no repository-specific override)", value: "" },
|
||||
...applicableRepos.map(repo => ({ name: repo, value: repo }))
|
||||
];
|
||||
|
||||
const selectedRepo = await Select.prompt({
|
||||
message: "Multiple repository overrides found. Select which to use:",
|
||||
options: choices,
|
||||
options: choices
|
||||
});
|
||||
|
||||
if (selectedRepo) {
|
||||
@@ -130,15 +128,9 @@ async function resolveEffectiveSyncOptions(
|
||||
);
|
||||
} else {
|
||||
// Non-interactive mode - list options and use top-level
|
||||
log.warn(
|
||||
`Multiple repository overrides found: ${applicableRepos.join(", ")}`
|
||||
);
|
||||
log.warn(
|
||||
`Running in non-interactive mode. Use --repository flag to specify which one to use.`
|
||||
);
|
||||
log.info(
|
||||
`Falling back to top-level settings (no repository-specific overrides applied)`
|
||||
);
|
||||
log.warn(`Multiple repository overrides found: ${applicableRepos.join(', ')}`);
|
||||
log.warn(`Running in non-interactive mode. Use --repository flag to specify which one to use.`);
|
||||
log.info(`Falling back to top-level settings (no repository-specific overrides applied)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,7 +280,6 @@ export async function FSFSElement(
|
||||
}
|
||||
|
||||
function prioritizeName(name: string): string {
|
||||
if (name == "version") return "aaa";
|
||||
if (name == "id") return "aa";
|
||||
if (name == "type") return "ab";
|
||||
if (name == "summary") return "ad";
|
||||
@@ -301,7 +292,6 @@ function prioritizeName(name: string): string {
|
||||
if (name == "failure_module") return "ak";
|
||||
if (name == "input_transforms") return "al";
|
||||
if (name == "lock") return "az";
|
||||
if (name == "locks") return "azz";
|
||||
|
||||
return name;
|
||||
}
|
||||
@@ -320,7 +310,57 @@ export interface InlineScript {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function extractInlineScriptsForApps(rec: any): InlineScript[] {
|
||||
export function extractInlineScriptsForFlows(
|
||||
modules: FlowModule[],
|
||||
pathAssigner: PathAssigner
|
||||
): InlineScript[] {
|
||||
return modules.flatMap((m) => {
|
||||
if (m.value.type == "rawscript") {
|
||||
const [basePath, ext] = pathAssigner.assignPath(
|
||||
m.summary,
|
||||
m.value.language
|
||||
);
|
||||
const path = basePath + ext;
|
||||
const content = m.value.content;
|
||||
const r = [{ path: path, content: content }];
|
||||
m.value.content = "!inline " + path.replaceAll(SEP, "/");
|
||||
const lock = m.value.lock;
|
||||
if (lock && lock != "") {
|
||||
const lockPath = basePath + "lock";
|
||||
m.value.lock = "!inline " + lockPath.replaceAll(SEP, "/");
|
||||
r.push({ path: lockPath, content: lock });
|
||||
}
|
||||
return r;
|
||||
} else if (m.value.type == "forloopflow") {
|
||||
return extractInlineScriptsForFlows(m.value.modules, pathAssigner);
|
||||
} else if (m.value.type == "branchall") {
|
||||
return m.value.branches.flatMap((b) =>
|
||||
extractInlineScriptsForFlows(b.modules, pathAssigner)
|
||||
);
|
||||
} else if (m.value.type == "whileloopflow") {
|
||||
return extractInlineScriptsForFlows(m.value.modules, pathAssigner);
|
||||
} else if (m.value.type == "branchone") {
|
||||
return [
|
||||
...m.value.branches.flatMap((b) =>
|
||||
extractInlineScriptsForFlows(b.modules, pathAssigner)
|
||||
),
|
||||
...extractInlineScriptsForFlows(m.value.default, pathAssigner),
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface PathAssigner {
|
||||
assignPath(summary: string | undefined, language: string): [string, string];
|
||||
}
|
||||
const INLINE_SCRIPT = "inline_script";
|
||||
|
||||
export function extractInlineScriptsForApps(
|
||||
rec: any,
|
||||
pathAssigner: PathAssigner
|
||||
): InlineScript[] {
|
||||
if (!rec) {
|
||||
return [];
|
||||
}
|
||||
@@ -328,7 +368,8 @@ export function extractInlineScriptsForApps(rec: any): InlineScript[] {
|
||||
return Object.entries(rec).flatMap(([k, v]) => {
|
||||
if (k == "inlineScript" && typeof v == "object") {
|
||||
const o: Record<string, any> = v as any;
|
||||
const [basePath, ext] = assignPath(rec["id"], o["language"]);
|
||||
const name = rec["name"];
|
||||
const [basePath, ext] = pathAssigner.assignPath(name, o["language"]);
|
||||
const r = [];
|
||||
if (o["content"]) {
|
||||
const content = o["content"];
|
||||
@@ -348,13 +389,69 @@ export function extractInlineScriptsForApps(rec: any): InlineScript[] {
|
||||
}
|
||||
return r;
|
||||
} else {
|
||||
return extractInlineScriptsForApps(v);
|
||||
return extractInlineScriptsForApps(v, pathAssigner);
|
||||
}
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function newPathAssigner(defaultTs: "bun" | "deno"): PathAssigner {
|
||||
let counter = 0;
|
||||
const seen_names = new Set<string>();
|
||||
function assignPath(
|
||||
summary: string | undefined,
|
||||
language: RawScript["language"] | "frontend" | "bunnative"
|
||||
): [string, string] {
|
||||
let name;
|
||||
|
||||
name = summary?.toLowerCase()?.replaceAll(" ", "_") ?? "";
|
||||
|
||||
let original_name = name;
|
||||
|
||||
if (name == "") {
|
||||
original_name = INLINE_SCRIPT;
|
||||
name = `${INLINE_SCRIPT}_0`;
|
||||
}
|
||||
|
||||
while (seen_names.has(name)) {
|
||||
counter++;
|
||||
name = `${original_name}_${counter}`;
|
||||
}
|
||||
seen_names.add(name);
|
||||
|
||||
let ext;
|
||||
if (language == "python3") ext = "py";
|
||||
else if (language == defaultTs || language == "bunnative") ext = "ts";
|
||||
else if (language == "bun") ext = "bun.ts";
|
||||
else if (language == "deno") ext = "deno.ts";
|
||||
else if (language == "go") ext = "go";
|
||||
else if (language == "bash") ext = "sh";
|
||||
else if (language == "powershell") ext = "ps1";
|
||||
else if (language == "postgresql") ext = "pg.sql";
|
||||
else if (language == "mysql") ext = "my.sql";
|
||||
else if (language == "bigquery") ext = "bq.sql";
|
||||
else if (language == "oracledb") ext = "odb.sql";
|
||||
else if (language == "snowflake") ext = "sf.sql";
|
||||
else if (language == "mssql") ext = "ms.sql";
|
||||
else if (language == "graphql") ext = "gql";
|
||||
else if (language == "nativets") ext = "native.ts";
|
||||
else if (language == "frontend") ext = "frontend.js";
|
||||
else if (language == "php") ext = "php";
|
||||
else if (language == "rust") ext = "rs";
|
||||
else if (language == "csharp") ext = "cs";
|
||||
else if (language == "nu") ext = "nu";
|
||||
else if (language == "ansible") ext = "playbook.yml";
|
||||
else if (language == "java") ext = "java";
|
||||
else if (language == "duckdb") ext = "duckdb.sql";
|
||||
// for related places search: ADD_NEW_LANG
|
||||
else ext = "no_ext";
|
||||
|
||||
return [`${name}.inline_script.`, ext];
|
||||
}
|
||||
|
||||
return { assignPath };
|
||||
}
|
||||
function ZipFSElement(
|
||||
zip: JSZip,
|
||||
useYaml: boolean,
|
||||
@@ -400,8 +497,7 @@ function ZipFSElement(
|
||||
const flow: OpenFlow = JSON.parse(await f.async("text"));
|
||||
const inlineScripts = extractInlineScriptsForFlows(
|
||||
flow.value.modules,
|
||||
{},
|
||||
SEP
|
||||
newPathAssigner(defaultTs)
|
||||
);
|
||||
for (const s of inlineScripts) {
|
||||
yield {
|
||||
@@ -426,7 +522,10 @@ function ZipFSElement(
|
||||
};
|
||||
} else if (kind == "app") {
|
||||
const app = JSON.parse(await f.async("text"));
|
||||
const inlineScripts = extractInlineScriptsForApps(app?.["value"]);
|
||||
const inlineScripts = extractInlineScriptsForApps(
|
||||
app?.["value"],
|
||||
newPathAssigner(defaultTs)
|
||||
);
|
||||
for (const s of inlineScripts) {
|
||||
yield {
|
||||
isDirectory: false,
|
||||
@@ -1032,8 +1131,7 @@ export async function ignoreF(wmillconf: {
|
||||
wmillconf.includes?.some((i) => minimatch(file, i))) &&
|
||||
(!wmillconf?.excludes ||
|
||||
wmillconf.excludes!.every((i) => !minimatch(file, i))) &&
|
||||
(!wmillconf.extraIncludes ||
|
||||
wmillconf.extraIncludes.length === 0 ||
|
||||
(!wmillconf.extraIncludes || wmillconf.extraIncludes.length === 0 ||
|
||||
wmillconf.extraIncludes.some((i) => minimatch(file, i)))
|
||||
);
|
||||
},
|
||||
@@ -1135,9 +1233,7 @@ async function buildTracker(changes: Change[]) {
|
||||
return tracker;
|
||||
}
|
||||
|
||||
export async function pull(
|
||||
opts: GlobalOptions & SyncOptions & { repository?: string }
|
||||
) {
|
||||
export async function pull(opts: GlobalOptions & SyncOptions & { repository?: string }) {
|
||||
if (opts.stateful) {
|
||||
await ensureDir(path.join(Deno.cwd(), ".wmill"));
|
||||
}
|
||||
@@ -1146,10 +1242,7 @@ export async function pull(
|
||||
await requireLogin(opts);
|
||||
|
||||
// Resolve effective sync options with repository awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(
|
||||
workspace,
|
||||
opts.repository
|
||||
);
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
@@ -1213,14 +1306,12 @@ export async function pull(
|
||||
if (opts.dryRun && opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
changes: changes.map((change) => ({
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length,
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
@@ -1381,14 +1472,12 @@ export async function pull(
|
||||
const result = {
|
||||
success: true,
|
||||
message: `All ${changes.length} changes applied locally and wmill-lock.yaml updated`,
|
||||
changes: changes.map((change) => ({
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length,
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
@@ -1399,13 +1488,7 @@ export async function pull(
|
||||
);
|
||||
}
|
||||
} else if (opts.jsonOutput) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ success: true, message: "No changes to apply", total: 0 },
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
console.log(JSON.stringify({ success: true, message: "No changes to apply", total: 0 }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1460,17 +1543,12 @@ function removeSuffix(str: string, suffix: string) {
|
||||
return str.slice(0, str.length - suffix.length);
|
||||
}
|
||||
|
||||
export async function push(
|
||||
opts: GlobalOptions & SyncOptions & { repository?: string }
|
||||
) {
|
||||
export async function push(opts: GlobalOptions & SyncOptions & { repository?: string }) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Resolve effective sync options with repository awareness
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(
|
||||
workspace,
|
||||
opts.repository
|
||||
);
|
||||
const effectiveOpts = await resolveEffectiveSyncOptions(workspace, opts.repository);
|
||||
|
||||
// Merge CLI flags with resolved settings (CLI flags take precedence only for explicit overrides)
|
||||
opts = mergeCliWithEffectiveOptions(opts, effectiveOpts);
|
||||
@@ -1607,14 +1685,12 @@ export async function push(
|
||||
if (opts.dryRun && opts.jsonOutput) {
|
||||
const result = {
|
||||
success: true,
|
||||
changes: changes.map((change) => ({
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length,
|
||||
total: changes.length
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
@@ -1988,38 +2064,26 @@ export async function push(
|
||||
const result = {
|
||||
success: true,
|
||||
message: `All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}`,
|
||||
changes: changes.map((change) => ({
|
||||
changes: changes.map(change => ({
|
||||
type: change.name,
|
||||
path: change.path,
|
||||
...(change.name === "edited" && change.codebase
|
||||
? { codebase_changed: true }
|
||||
: {}),
|
||||
...(change.name === "edited" && change.codebase ? { codebase_changed: true } : {})
|
||||
})),
|
||||
total: changes.length,
|
||||
duration_ms: Math.round(performance.now() - start),
|
||||
duration_ms: Math.round(performance.now() - start)
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} else {
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${
|
||||
changes.length
|
||||
} changes pushed to the remote workspace ${
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${
|
||||
workspace.workspaceId
|
||||
} named ${workspace.name} (${(performance.now() - start).toFixed(
|
||||
0
|
||||
)}ms)`
|
||||
} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (opts.jsonOutput) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ success: true, message: "No changes to push", total: 0 },
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
console.log(JSON.stringify({ success: true, message: "No changes to push", total: 0 }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
85
cli/windmill-utils-internal/.gitignore
vendored
85
cli/windmill-utils-internal/.gitignore
vendored
@@ -1,85 +0,0 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Generated client
|
||||
src/gen/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE/Editor files
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Runtime data
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Temporary folders
|
||||
tmp/
|
||||
temp/
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"deno.enable": false
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# only install gnu-sed if not already installed
|
||||
if ! command -v gsed &> /dev/null; then
|
||||
brew install gnu-sed
|
||||
fi
|
||||
|
||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
output_dirpath="${script_dirpath}/src/gen"
|
||||
|
||||
rm -rf "${output_dirpath}"
|
||||
|
||||
npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../../backend/windmill-api/openapi.yaml" --output "${output_dirpath}" --useOptions --client legacy/fetch --schemas false
|
||||
cat <<EOF - "${output_dirpath}/core/OpenAPI.ts" > temp_file && mv temp_file "${output_dirpath}/core/OpenAPI.ts"
|
||||
const getEnv = (key: string) => {
|
||||
return process.env[key]
|
||||
};
|
||||
|
||||
const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000";
|
||||
const baseUrlApi = (baseUrl ?? '') + "/api";
|
||||
|
||||
EOF
|
||||
gsed -i 's/WITH_CREDENTIALS: false/WITH_CREDENTIALS: true/g' "${output_dirpath}/core/OpenAPI.ts"
|
||||
gsed -i 's/TOKEN: undefined/TOKEN: getEnv("WM_TOKEN")/g' "${output_dirpath}/core/OpenAPI.ts"
|
||||
gsed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" "${output_dirpath}/core/OpenAPI.ts"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
output_dirpath="${script_dirpath}/src/gen"
|
||||
|
||||
rm -rf "${output_dirpath}"
|
||||
|
||||
npx --yes @hey-api/openapi-ts@0.53.1 --input "${script_dirpath}/../../backend/windmill-api/openapi.yaml" --output "${output_dirpath}" --useOptions --client legacy/fetch --schemas false
|
||||
cat <<EOF - "${output_dirpath}/core/OpenAPI.ts" > temp_file && mv temp_file "${output_dirpath}/core/OpenAPI.ts"
|
||||
const getEnv = (key: string) => {
|
||||
return process.env[key]
|
||||
};
|
||||
|
||||
const baseUrl = getEnv("BASE_INTERNAL_URL") ?? getEnv("BASE_URL") ?? "http://localhost:8000";
|
||||
const baseUrlApi = (baseUrl ?? '') + "/api";
|
||||
|
||||
EOF
|
||||
sed -i 's/WITH_CREDENTIALS: false/WITH_CREDENTIALS: true/g' "${output_dirpath}/core/OpenAPI.ts"
|
||||
sed -i 's/TOKEN: undefined/TOKEN: getEnv("WM_TOKEN")/g' "${output_dirpath}/core/OpenAPI.ts"
|
||||
sed -i "s/BASE: '\/api'/BASE: baseUrlApi/g" "${output_dirpath}/core/OpenAPI.ts"
|
||||
48
cli/windmill-utils-internal/package-lock.json
generated
48
cli/windmill-utils-internal/package-lock.json
generated
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "windmill-utils-internal",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-utils-internal",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.1.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"name": "windmill-utils-internal",
|
||||
"version": "1.0.0",
|
||||
"description": "Internal utility functions for Windmill",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "./gen_wm_client.sh && tsc",
|
||||
"build-mac": "./gen_wm_client-mac.sh && tsc",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"windmill"
|
||||
],
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.1.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eou pipefail
|
||||
script_dirpath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
args=${1:-}
|
||||
|
||||
rm -rf "${script_dirpath}/dist"
|
||||
|
||||
npm install
|
||||
npm run build
|
||||
npm publish ${args}
|
||||
@@ -1,47 +0,0 @@
|
||||
// constants.ts
|
||||
/**
|
||||
* Cross-platform path separator constants
|
||||
* Compatible with both Node.js and Deno environments
|
||||
*/
|
||||
|
||||
/**
|
||||
* Detects if the current platform is Windows by checking various environment indicators.
|
||||
* Uses multiple fallback methods to ensure compatibility across Node.js, Deno, and browser environments.
|
||||
*
|
||||
* @returns True if running on Windows, false otherwise
|
||||
*/
|
||||
const isWindows = (() => {
|
||||
// Try Node.js process.platform first (most reliable)
|
||||
if (typeof process !== "undefined" && process.platform) {
|
||||
return process.platform === "win32";
|
||||
}
|
||||
|
||||
// Try Deno specific detection via globalThis
|
||||
if (typeof globalThis !== "undefined" && "Deno" in globalThis) {
|
||||
return (globalThis as any).Deno?.build?.os === "windows";
|
||||
}
|
||||
|
||||
// Try navigator.platform (browser/some Deno contexts)
|
||||
// @ts-ignore
|
||||
if (typeof navigator !== "undefined" && navigator.platform) {
|
||||
// @ts-ignore
|
||||
return navigator.platform.toLowerCase().includes("win");
|
||||
}
|
||||
|
||||
// Fallback - assume Unix-like (safest default)
|
||||
return false;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Path separator constant - equivalent to Deno's SEPARATOR or Node's path.sep
|
||||
* On Windows: "\"
|
||||
* On Unix-like systems: "/"
|
||||
*/
|
||||
export const SEP = isWindows ? "\\" : "/";
|
||||
|
||||
/**
|
||||
* Path delimiter constant for environment variables like PATH
|
||||
* On Windows: ";"
|
||||
* On Unix-like systems: ":"
|
||||
*/
|
||||
export const DELIMITER = isWindows ? ";" : ":";
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Main entry point for windmill-utils-internal package
|
||||
*
|
||||
* This module provides utilities for handling Windmill flows, scripts, and schemas:
|
||||
* - Inline script extraction and replacement
|
||||
* - Path utilities for different programming languages
|
||||
* - Schema parsing and conversion utilities
|
||||
* - Cross-platform path constants
|
||||
*/
|
||||
|
||||
export * from "./inline-scripts";
|
||||
export * from "./path-utils";
|
||||
export * from "./parse";
|
||||
export { SEP, DELIMITER } from "./constants";
|
||||
@@ -1,117 +0,0 @@
|
||||
import { assignPath } from "../path-utils/path-assigner";
|
||||
import { FlowModule } from "../gen/types.gen";
|
||||
|
||||
/**
|
||||
* Represents an inline script extracted from a flow module
|
||||
*/
|
||||
interface InlineScript {
|
||||
/** File path where the script content should be written */
|
||||
path: string;
|
||||
/** The actual script content */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts inline scripts from flow modules, converting them to separate files
|
||||
* and replacing the original content with file references.
|
||||
*
|
||||
* @param modules - Array of flow modules to process
|
||||
* @param mapping - Optional mapping of module IDs to custom file paths
|
||||
* @param defaultTs - Default TypeScript runtime to use ("bun" or "deno")
|
||||
* @returns Array of inline scripts with their paths and content
|
||||
*/
|
||||
export function extractInlineScripts(
|
||||
modules: FlowModule[],
|
||||
mapping: Record<string, string> = {},
|
||||
separator: string = "/",
|
||||
defaultTs?: "bun" | "deno"
|
||||
): InlineScript[] {
|
||||
return modules.flatMap((m) => {
|
||||
if (m.value.type == "rawscript") {
|
||||
const [basePath, ext] = assignPath(m.id, m.value.language, defaultTs);
|
||||
const path = mapping[m.id] ?? basePath + ext;
|
||||
const content = m.value.content;
|
||||
const r = [{ path: path, content: content }];
|
||||
m.value.content = "!inline " + path.replaceAll(separator, "/");
|
||||
const lock = m.value.lock;
|
||||
if (lock && lock != "") {
|
||||
const lockPath = basePath + "lock";
|
||||
m.value.lock = "!inline " + lockPath.replaceAll(separator, "/");
|
||||
r.push({ path: lockPath, content: lock });
|
||||
}
|
||||
return r;
|
||||
} else if (m.value.type == "forloopflow") {
|
||||
return extractInlineScripts(
|
||||
m.value.modules,
|
||||
mapping,
|
||||
separator,
|
||||
defaultTs
|
||||
);
|
||||
} else if (m.value.type == "branchall") {
|
||||
return m.value.branches.flatMap((b) =>
|
||||
extractInlineScripts(b.modules, mapping, separator, defaultTs)
|
||||
);
|
||||
} else if (m.value.type == "whileloopflow") {
|
||||
return extractInlineScripts(
|
||||
m.value.modules,
|
||||
mapping,
|
||||
separator,
|
||||
defaultTs
|
||||
);
|
||||
} else if (m.value.type == "branchone") {
|
||||
return [
|
||||
...m.value.branches.flatMap((b) =>
|
||||
extractInlineScripts(b.modules, mapping, separator, defaultTs)
|
||||
),
|
||||
...extractInlineScripts(m.value.default, mapping, separator, defaultTs),
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the current mapping of module IDs to file paths from flow modules
|
||||
* by analyzing existing inline script references.
|
||||
*
|
||||
* @param modules - Array of flow modules to analyze (can be undefined)
|
||||
* @param mapping - Existing mapping to extend (defaults to empty object)
|
||||
* @returns Record mapping module IDs to their corresponding file paths
|
||||
*/
|
||||
export function extractCurrentMapping(
|
||||
modules: FlowModule[] | undefined,
|
||||
mapping: Record<string, string> = {}
|
||||
): Record<string, string> {
|
||||
if (!modules || !Array.isArray(modules)) {
|
||||
return mapping;
|
||||
}
|
||||
|
||||
modules.forEach((m) => {
|
||||
if (!m?.value?.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m.value.type === "rawscript") {
|
||||
if (m.value.content && m.value.content.startsWith("!inline ")) {
|
||||
mapping[m.id] = m.value.content.trim().split(" ")[1];
|
||||
}
|
||||
} else if (
|
||||
m.value.type === "forloopflow" ||
|
||||
m.value.type === "whileloopflow"
|
||||
) {
|
||||
extractCurrentMapping(m.value.modules, mapping);
|
||||
} else if (m.value.type === "branchall") {
|
||||
m.value.branches.forEach((b) =>
|
||||
extractCurrentMapping(b.modules, mapping)
|
||||
);
|
||||
} else if (m.value.type === "branchone") {
|
||||
m.value.branches.forEach((b) =>
|
||||
extractCurrentMapping(b.modules, mapping)
|
||||
);
|
||||
extractCurrentMapping(m.value.default, mapping);
|
||||
}
|
||||
});
|
||||
|
||||
return mapping;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./replacer";
|
||||
export * from "./extractor";
|
||||
@@ -1,107 +0,0 @@
|
||||
import { FlowModule } from "../gen/types.gen";
|
||||
|
||||
/**
|
||||
* Replaces inline script references with actual file content from the filesystem.
|
||||
* This function recursively processes all flow modules and their nested structures.
|
||||
*
|
||||
* @param modules - Array of flow modules to process
|
||||
* @param fileReader - Function to read file content (typically fs.readFile or similar)
|
||||
* @param logger - Optional logger object with info and error methods
|
||||
* @param localPath - Base path for resolving relative file paths
|
||||
* @param removeLocks - Optional array of paths for which to remove lock files
|
||||
* @returns Promise that resolves when all inline scripts have been replaced
|
||||
*/
|
||||
export async function replaceInlineScripts(
|
||||
modules: FlowModule[],
|
||||
fileReader: (path: string) => Promise<string>,
|
||||
logger: {
|
||||
info: (message: string) => void,
|
||||
error: (message: string) => void,
|
||||
} = {
|
||||
info: () => {},
|
||||
error: () => {},
|
||||
},
|
||||
localPath: string,
|
||||
separator: string = "/",
|
||||
removeLocks?: string[],
|
||||
renamer?: (path: string, newPath: string) => void,
|
||||
deleter?: (path: string) => void
|
||||
): Promise<void> {
|
||||
await Promise.all(modules.map(async (module) => {
|
||||
if (!module.value) {
|
||||
throw new Error(`Module value is undefined for module ${module.id}`);
|
||||
}
|
||||
|
||||
if (module.value.type === "rawscript" && module.value.content && module.value.content.startsWith("!inline")) {
|
||||
const path = module.value.content.split(" ")[1];
|
||||
const pathPrefix = path.split(".")[0];
|
||||
const pathSuffix = path.split(".").slice(1).join(".");
|
||||
// new path is the module id with the same suffix
|
||||
const newPath = module.id + "." + pathSuffix;
|
||||
|
||||
try {
|
||||
module.value.content = await fileReader(path);
|
||||
} catch {
|
||||
logger.error(`Script file ${path} not found`);
|
||||
// try new path
|
||||
try {
|
||||
module.value.content = await fileReader(newPath);
|
||||
} catch {
|
||||
logger.error(`Script file ${newPath} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// rename the file if the prefix is different from the module id (fix old naming)
|
||||
if (pathPrefix != module.id && renamer) {
|
||||
logger.info(`Renaming ${path} to ${module.id}.${pathSuffix}`);
|
||||
try {
|
||||
renamer(localPath + path, localPath + module.id + "." + pathSuffix);
|
||||
} catch {
|
||||
logger.info(`Failed to rename ${path} to ${module.id}.${pathSuffix}`);
|
||||
}
|
||||
}
|
||||
|
||||
const lock = module.value.lock;
|
||||
if (removeLocks && removeLocks.includes(path)) {
|
||||
module.value.lock = undefined;
|
||||
|
||||
// delete the file if the prefix is different from the module id (fix old naming)
|
||||
if (lock && lock != "") {
|
||||
const path = lock.split(" ")[1];
|
||||
const pathPrefix = path.split(".")[0];
|
||||
if (pathPrefix != module.id && deleter) {
|
||||
logger.info(`Deleting ${path}`);
|
||||
try {
|
||||
deleter(localPath + path);
|
||||
} catch {
|
||||
logger.error(`Failed to delete ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (
|
||||
lock &&
|
||||
typeof lock == "string" &&
|
||||
lock.trimStart().startsWith("!inline ")
|
||||
) {
|
||||
const path = lock.split(" ")[1];
|
||||
try {
|
||||
module.value.lock = await fileReader(path.replaceAll("/", separator));
|
||||
} catch {
|
||||
logger.error(`Lock file ${path} not found`);
|
||||
}
|
||||
}
|
||||
} else if (module.value.type === "forloopflow" || module.value.type === "whileloopflow") {
|
||||
await replaceInlineScripts(module.value.modules, fileReader, logger, localPath, separator, removeLocks);
|
||||
} else if (module.value.type === "branchall") {
|
||||
await Promise.all(module.value.branches.map(async (branch) => {
|
||||
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks);
|
||||
}));
|
||||
} else if (module.value.type === "branchone") {
|
||||
await Promise.all(module.value.branches.map(async (branch) => {
|
||||
await replaceInlineScripts(branch.modules, fileReader, logger, localPath, separator, removeLocks);
|
||||
}));
|
||||
await replaceInlineScripts(module.value.default, fileReader, logger, localPath, separator, removeLocks);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./parse-schema";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./path-assigner";
|
||||
@@ -1,71 +0,0 @@
|
||||
import { RawScript } from "../gen/types.gen";
|
||||
|
||||
/**
|
||||
* Union type of all supported programming languages in Windmill
|
||||
*/
|
||||
export type SupportedLanguage = RawScript["language"] | "frontend" | "bunnative" | "oracledb" | "rust" | "csharp" | "nu" | "ansible" | "java" | "duckdb";
|
||||
|
||||
/**
|
||||
* Mapping of supported languages to their file extensions
|
||||
*/
|
||||
export const LANGUAGE_EXTENSIONS: Record<SupportedLanguage, string> = {
|
||||
python3: "py",
|
||||
bun: "bun.ts",
|
||||
deno: "deno.ts",
|
||||
go: "go",
|
||||
bash: "sh",
|
||||
powershell: "ps1",
|
||||
postgresql: "pg.sql",
|
||||
mysql: "my.sql",
|
||||
bigquery: "bq.sql",
|
||||
oracledb: "odb.sql",
|
||||
snowflake: "sf.sql",
|
||||
mssql: "ms.sql",
|
||||
graphql: "gql",
|
||||
nativets: "native.ts",
|
||||
frontend: "frontend.js",
|
||||
php: "php",
|
||||
rust: "rs",
|
||||
csharp: "cs",
|
||||
nu: "nu",
|
||||
ansible: "playbook.yml",
|
||||
java: "java",
|
||||
duckdb: "duckdb.sql",
|
||||
bunnative: "ts"
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the appropriate file extension for a given programming language.
|
||||
* Handles special cases for TypeScript variants based on the default runtime.
|
||||
*
|
||||
* @param language - The programming language to get extension for
|
||||
* @param defaultTs - Default TypeScript runtime ("bun" or "deno")
|
||||
* @returns File extension string (without the dot)
|
||||
*/
|
||||
export function getLanguageExtension(
|
||||
language: SupportedLanguage,
|
||||
defaultTs: "bun" | "deno" = "bun"
|
||||
): string {
|
||||
if (language === defaultTs || language === "bunnative") {
|
||||
return "ts";
|
||||
}
|
||||
return LANGUAGE_EXTENSIONS[language] || "no_ext";
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a file path for an inline script based on its ID and language.
|
||||
* Returns both the base path and extension as separate components.
|
||||
*
|
||||
* @param id - Unique identifier for the script
|
||||
* @param language - Programming language of the script
|
||||
* @param defaultTs - Default TypeScript runtime ("bun" or "deno")
|
||||
* @returns Tuple containing [basePath, extension]
|
||||
*/
|
||||
export function assignPath(
|
||||
id: string,
|
||||
language: SupportedLanguage,
|
||||
defaultTs: "bun" | "deno" = "bun"
|
||||
): [string, string] {
|
||||
const ext = getLanguageExtension(language, defaultTs);
|
||||
return [`${id}.inline_script.`, ext];
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "./",
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -48,8 +48,6 @@
|
||||
nodejs
|
||||
postgresql
|
||||
pkg-config
|
||||
glibc.dev
|
||||
clang
|
||||
cmake
|
||||
];
|
||||
coursier = pkgs.fetchFromGitHub {
|
||||
|
||||
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.517.0",
|
||||
"version": "1.515.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.517.0",
|
||||
"version": "1.515.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -85,7 +85,6 @@
|
||||
"windmill-parser-wasm-ts": "1.514.1",
|
||||
"windmill-parser-wasm-yaml": "1.510.1",
|
||||
"windmill-sql-datatype-parser-wasm": "1.318.0",
|
||||
"windmill-utils-internal": "^1.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.2",
|
||||
"y-monaco": "^0.1.4",
|
||||
@@ -12995,12 +12994,6 @@
|
||||
"resolved": "https://registry.npmjs.org/windmill-sql-datatype-parser-wasm/-/windmill-sql-datatype-parser-wasm-1.318.0.tgz",
|
||||
"integrity": "sha512-jlRw6abUJi4vDm+7xDSjhb7dvm4tC+lBXv0EEwn52Veadwcl5EB1yGHb9XVqQEfcYr9JU62xfJlN6DoGQmYE/g=="
|
||||
},
|
||||
"node_modules/windmill-utils-internal": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-utils-internal/-/windmill-utils-internal-1.0.0.tgz",
|
||||
"integrity": "sha512-S93XgzdM8WNmDt+MUqiBzyA/3GacZV3Wo9DxxYpAuDHvIP4s8c9zKBhNIZqB008i1opI4tTxB8SwhsYBhTaRuw==",
|
||||
"license": "Apache 2.0"
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.517.0",
|
||||
"version": "1.515.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -152,7 +152,6 @@
|
||||
"windmill-parser-wasm-ts": "1.514.1",
|
||||
"windmill-parser-wasm-yaml": "1.510.1",
|
||||
"windmill-sql-datatype-parser-wasm": "1.318.0",
|
||||
"windmill-utils-internal": "^1.0.0",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.2",
|
||||
"y-monaco": "^0.1.4",
|
||||
|
||||
@@ -583,7 +583,7 @@
|
||||
<div class="mt-6"></div>
|
||||
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert type="warning" title="Enterprise license required"
|
||||
<Alert type="error" title="Enterprise license required"
|
||||
>Deploy to staging/prod from the web UI is only available with an enterprise license</Alert
|
||||
>
|
||||
{:else if notSet == true}
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
isTest?: boolean
|
||||
externalToolbarAvailable?: boolean
|
||||
forceJson?: boolean
|
||||
fixTableSizingToParent?: boolean
|
||||
copilot_fix?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
@@ -111,7 +110,6 @@
|
||||
isTest = true,
|
||||
externalToolbarAvailable = false,
|
||||
forceJson = $bindable(false),
|
||||
fixTableSizingToParent = false,
|
||||
copilot_fix,
|
||||
children
|
||||
}: Props = $props()
|
||||
@@ -588,25 +586,19 @@
|
||||
{#if !forceJson && resultKind === 'table-col'}
|
||||
{@const data = 'table-col' in result ? result['table-col'] : result}
|
||||
<AutoDataTable
|
||||
class={fixTableSizingToParent
|
||||
? 'absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]'
|
||||
: ''}
|
||||
class="absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]"
|
||||
objects={objectOfArraysToObjects(data)}
|
||||
/>
|
||||
{:else if !forceJson && resultKind === 'table-row'}
|
||||
{@const data = 'table-row' in result ? result['table-row'] : result}
|
||||
<AutoDataTable
|
||||
class={fixTableSizingToParent
|
||||
? 'absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]'
|
||||
: ''}
|
||||
class="absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]"
|
||||
objects={arrayOfRowsToObjects(data)}
|
||||
/>
|
||||
{:else if !forceJson && resultKind === 'table-row-object'}
|
||||
{@const data = 'table-row-object' in result ? result['table-row-object'] : result}
|
||||
<AutoDataTable
|
||||
class={fixTableSizingToParent
|
||||
? 'absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]'
|
||||
: ''}
|
||||
class="absolute inset-0 [&>div]:h-full [&>div]:min-h-[10rem]"
|
||||
objects={handleArrayOfObjectsHeaders(data)}
|
||||
/>
|
||||
{:else if !forceJson && resultKind === 'html'}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type { SyncResponse } from '$lib/git-sync'
|
||||
type DiffResult = {
|
||||
added: string[]
|
||||
deleted: string[]
|
||||
modified: string[]
|
||||
repoWmillYaml?: string
|
||||
yamlModified?: boolean
|
||||
}
|
||||
|
||||
let { previewResult } = $props<{
|
||||
previewResult: SyncResponse | undefined
|
||||
previewResult: DiffResult | undefined
|
||||
}>()
|
||||
|
||||
let added = $derived(previewResult?.changes?.filter(c => c.type === 'added').map(c => c.path) || [])
|
||||
let deleted = $derived(previewResult?.changes?.filter(c => c.type === 'deleted').map(c => c.path) || [])
|
||||
let edited = $derived(previewResult?.changes?.filter(c => c.type === 'edited').map(c => c.path) || [])
|
||||
</script>
|
||||
|
||||
<div class="border rounded p-2 text-xs max-h-40 overflow-y-auto bg-surface-secondary">
|
||||
<div class="font-semibold text-[11px] mb-1 text-tertiary">Preview of changes:</div>
|
||||
|
||||
{#if !added.length && !deleted.length && !edited.length}
|
||||
{#if !previewResult?.added?.length && !previewResult?.deleted?.length && !previewResult?.modified?.length && !previewResult?.yamlModified}
|
||||
<div class="mt-2 text-tertiary">No changes found! The workspace is up to date.</div>
|
||||
{:else}
|
||||
{#if added.length}
|
||||
{#if previewResult?.yamlModified}
|
||||
<div class="mt-2">
|
||||
<div class="text-yellow-600">Modified:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
<li>wmill.yaml (Git sync settings)</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if previewResult?.added?.length}
|
||||
<div class="mt-2">
|
||||
<div class="text-green-600">Added:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each added as file}
|
||||
{#each previewResult.added as file}
|
||||
<li>
|
||||
{file}{!file.includes('.') ? ' (dir)' : ''}
|
||||
</li>
|
||||
@@ -28,11 +37,11 @@
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if deleted.length}
|
||||
{#if previewResult?.deleted?.length}
|
||||
<div class="mt-2">
|
||||
<div class="text-red-600">Deleted:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each deleted as file}
|
||||
{#each previewResult.deleted as file}
|
||||
<li>
|
||||
{file}{!file.includes('.') ? ' (dir)' : ''}
|
||||
</li>
|
||||
@@ -40,11 +49,11 @@
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if edited.length}
|
||||
{#if previewResult?.modified?.length}
|
||||
<div class="mt-2">
|
||||
<div class="text-yellow-600">Edited:</div>
|
||||
<div class="text-yellow-600">Modified:</div>
|
||||
<ul class="list-disc list-inside">
|
||||
{#each edited as file}
|
||||
{#each previewResult.modified as file}
|
||||
<li>
|
||||
{file}{!file.includes('.') ? ' (dir)' : ''}
|
||||
</li>
|
||||
|
||||
454
frontend/src/lib/components/InitGitRepoPopover.svelte
Normal file
454
frontend/src/lib/components/InitGitRepoPopover.svelte
Normal file
@@ -0,0 +1,454 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import {
|
||||
Loader2,
|
||||
Eye,
|
||||
Save,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
UploadCloud,
|
||||
AlertTriangle,
|
||||
Terminal,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
|
||||
// Types for git sync result
|
||||
interface GitSyncChange {
|
||||
type: 'added' | 'deleted' | 'modified'
|
||||
path: string
|
||||
}
|
||||
|
||||
interface GitSyncResult {
|
||||
changes: GitSyncChange[]
|
||||
}
|
||||
import GitDiffPreview from './GitDiffPreview.svelte'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
let { gitRepoResourcePath, branchName, uiState } = $props<{
|
||||
gitRepoResourcePath: string
|
||||
branchName?: string
|
||||
uiState: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: string[]
|
||||
}
|
||||
}>()
|
||||
|
||||
let _branchName = $state(branchName ?? '')
|
||||
let previewResult = $state<
|
||||
| {
|
||||
added: string[]
|
||||
deleted: string[]
|
||||
modified: string[]
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
let isPreviewLoading = $state(false)
|
||||
let isInitializing = $state(false)
|
||||
let initResult = $state<{ success: boolean; message: string | undefined } | null>(null)
|
||||
let initGitRepoPopover = $state<{ open: () => void; close: () => void } | null>(null)
|
||||
let previewJobId = $state<string | null>(null)
|
||||
let previewJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let pushJobId = $state<string | null>(null)
|
||||
let pushJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isCliInfoExpanded = $state(false)
|
||||
|
||||
async function previewChanges() {
|
||||
console.log('Previewing changes for repo:', gitRepoResourcePath)
|
||||
isPreviewLoading = true
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) {
|
||||
previewResult = undefined
|
||||
isPreviewLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Pass UI state directly as JSON to CLI
|
||||
const payloadObj = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
branch_to_push: _branchName,
|
||||
dry_run: true,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payloadObj,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
previewJobId = jobId
|
||||
previewJobStatus = 'running'
|
||||
|
||||
// Wait for job completion (polling)
|
||||
let jobSuccess = false
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
jobSuccess = !!testResult.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: {
|
||||
reason: 'Preview job timed out after 15s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
if (jobSuccess) {
|
||||
const result = await JobService.getCompletedJobResult({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
console.log('Preview result:', result)
|
||||
|
||||
// Convert new CLI format to expected format
|
||||
const added: string[] = []
|
||||
const deleted: string[] = []
|
||||
const modified: string[] = []
|
||||
|
||||
if (
|
||||
result &&
|
||||
(result as GitSyncResult).changes &&
|
||||
Array.isArray((result as GitSyncResult).changes)
|
||||
) {
|
||||
for (const change of (result as GitSyncResult).changes) {
|
||||
if (change.type === 'added') {
|
||||
added.push(change.path)
|
||||
} else if (change.type === 'deleted') {
|
||||
deleted.push(change.path)
|
||||
} else if (change.type === 'modified') {
|
||||
modified.push(change.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previewResult = { added, deleted, modified }
|
||||
previewJobStatus = 'success'
|
||||
} else {
|
||||
previewResult = undefined
|
||||
previewJobStatus = 'failure'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to preview changes:', error)
|
||||
previewResult = undefined
|
||||
previewJobStatus = 'failure'
|
||||
} finally {
|
||||
isPreviewLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeRepo() {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
console.log('Initializing repo:', gitRepoResourcePath, 'in workspace:', workspace)
|
||||
isInitializing = true
|
||||
initResult = null
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
try {
|
||||
// Pass UI state directly as JSON to CLI
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
branch_to_push: _branchName,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
pushJobId = jobId
|
||||
pushJobStatus = 'running'
|
||||
|
||||
let jobSuccess = false
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
jobSuccess = !!testResult.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: {
|
||||
reason: 'Push job timed out after 5s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
pushJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
initResult = {
|
||||
success: jobSuccess,
|
||||
message: jobSuccess ? undefined : 'Failed to initialize repository.'
|
||||
}
|
||||
|
||||
// Reset popover state after successful push
|
||||
if (jobSuccess) {
|
||||
setTimeout(() => {
|
||||
previewResult = undefined
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
initResult = null
|
||||
initGitRepoPopover?.close()
|
||||
}, 1500) // Small delay to show success state
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize repo:', error)
|
||||
pushJobStatus = 'failure'
|
||||
initResult = { success: false, message: 'Failed to initialize repository.' }
|
||||
} finally {
|
||||
isInitializing = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
bind:this={initGitRepoPopover}
|
||||
floatingConfig={{
|
||||
placement: 'top-start',
|
||||
strategy: 'fixed',
|
||||
flip: false,
|
||||
shift: true
|
||||
}}
|
||||
contentClasses="p-4 w-1/3"
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
color="dark"
|
||||
size="sm"
|
||||
nonCaptureEvent
|
||||
onclick={initGitRepoPopover?.open}
|
||||
startIcon={{ icon: UploadCloud }}
|
||||
>
|
||||
Push workspace to Git repo
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-semibold">Push workspace to Git repository</h3>
|
||||
<div class="prose max-w-none text-2xs text-tertiary">
|
||||
This action will push all workspace objects that match your current filter settings to the
|
||||
selected branch in your Git repository. <span
|
||||
class="text-orange-600 flex items-center gap-1"
|
||||
><AlertTriangle size={14} /> Any existing content in the branch will be replaced with the
|
||||
filtered workspace content.</span
|
||||
>
|
||||
|
||||
<!-- Collapsible CLI Info Section -->
|
||||
<div class="mt-2 border rounded-md">
|
||||
<button
|
||||
class="w-full flex items-center justify-between p-1.5 bg-surface-secondary hover:bg-surface-hover"
|
||||
onclick={() => (isCliInfoExpanded = !isCliInfoExpanded)}
|
||||
>
|
||||
<span class="font-medium flex items-center gap-2">
|
||||
<Terminal size={14} />
|
||||
Windmill CLI to pull from Windmill and push to git
|
||||
</span>
|
||||
{#if isCliInfoExpanded}
|
||||
<ChevronUp size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isCliInfoExpanded}
|
||||
<div class="p-1 bg-surface-tertiary">
|
||||
<div class="text-2xs mb-2">
|
||||
Not familiar with Windmill CLI? <a
|
||||
href="https://www.windmill.dev/docs/advanced/cli/sync"
|
||||
class="text-blue-500 hover:text-blue-600 underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Check out the docs</a
|
||||
>
|
||||
</div>
|
||||
<div class="font-mono text-2xs">
|
||||
<pre class="overflow-auto max-h-60"
|
||||
><code
|
||||
>npm install -g windmill-cli
|
||||
wmill workspace add {$workspaceStore} {$workspaceStore} {`${$page.url.protocol}//${$page.url.hostname}/`}
|
||||
wmill init
|
||||
# adjust wmill.yaml file configuraton as needed
|
||||
wmill sync pull
|
||||
git add -A
|
||||
git commit -m 'Initial commit'
|
||||
git push</code
|
||||
></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="branch-name" class="text-sm font-medium">Push to new branch (optional)</label>
|
||||
<div class="prose max-w-none text-2xs text-tertiary">
|
||||
Enter a new branch name to push to (e.g so you can merge back into main with a pull
|
||||
request). If left blank, the default branch from the git repository resource will be used.
|
||||
</div>
|
||||
<div class="flex flex-col w-1/4">
|
||||
<input
|
||||
id="branch-name"
|
||||
type="text"
|
||||
bind:value={_branchName}
|
||||
class="border rounded px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if previewResult}
|
||||
<GitDiffPreview {previewResult} />
|
||||
{/if}
|
||||
|
||||
{#if previewJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if previewJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if previewJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if previewJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Preview job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${previewJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{previewJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if pushJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if pushJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if pushJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if pushJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Push job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${pushJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{pushJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Action row: Cancel on left, Preview/Confirm on right -->
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
previewResult = undefined
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
initResult = null
|
||||
close()
|
||||
}}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
{#if !previewResult}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
title="Preview changes again"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{#if previewResult.added?.length || previewResult.deleted?.length || previewResult.modified?.length}
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
on:click={initializeRepo}
|
||||
disabled={isPreviewLoading || isInitializing}
|
||||
startIcon={{ icon: Save }}
|
||||
title="Initialize Git Repo"
|
||||
>
|
||||
Push
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if initResult?.message}
|
||||
<div class="mt-2">
|
||||
<Alert
|
||||
type={initResult.success ? 'success' : 'error'}
|
||||
title={initResult.success ? 'Success' : 'Error'}
|
||||
size="xs"
|
||||
>
|
||||
{initResult.message}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<div class="relative max-h-100">
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert type="warning" title="Enterprise Edition only feature">
|
||||
<Alert type="error" title="Enterprise Edition only feature">
|
||||
Job metrics are only available on Windmill Enterprise Edition.
|
||||
</Alert>
|
||||
{:else if (jobMemoryStats?.length ?? 0) === 0}
|
||||
|
||||
418
frontend/src/lib/components/PullGitRepoPopover.svelte
Normal file
418
frontend/src/lib/components/PullGitRepoPopover.svelte
Normal file
@@ -0,0 +1,418 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import {
|
||||
Loader2,
|
||||
Eye,
|
||||
Save,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
DownloadCloud,
|
||||
AlertTriangle,
|
||||
Terminal,
|
||||
ChevronDown,
|
||||
ChevronUp
|
||||
} from 'lucide-svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import GitDiffPreview from './GitDiffPreview.svelte'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
// Types for git sync result
|
||||
interface GitSyncChange {
|
||||
type: 'added' | 'deleted' | 'modified'
|
||||
path: string
|
||||
}
|
||||
|
||||
interface GitSyncResult {
|
||||
changes: GitSyncChange[]
|
||||
}
|
||||
|
||||
let { gitRepoResourcePath, uiState, onFilterUpdate } = $props<{
|
||||
gitRepoResourcePath: string
|
||||
uiState: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: string[]
|
||||
}
|
||||
onFilterUpdate: (filters: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: string[]
|
||||
}) => void
|
||||
}>()
|
||||
|
||||
type PreviewResult = {
|
||||
added: string[]
|
||||
deleted: string[]
|
||||
modified: string[]
|
||||
}
|
||||
|
||||
let previewResult = $state<PreviewResult | undefined>(undefined)
|
||||
|
||||
let isPreviewLoading = $state(false)
|
||||
let isPulling = $state(false)
|
||||
let pullGitRepoPopover = $state<{ open: () => void; close: () => void } | null>(null)
|
||||
let jobStatus = $state<{
|
||||
id: string | null
|
||||
status: 'running' | 'success' | 'failure' | undefined
|
||||
error?: string
|
||||
type: 'preview' | 'pull'
|
||||
}>({
|
||||
id: null,
|
||||
status: undefined,
|
||||
type: 'preview'
|
||||
})
|
||||
|
||||
let isCliInfoExpanded = $state(false)
|
||||
|
||||
async function handleJobCompletion(jobId: string, workspace: string): Promise<boolean> {
|
||||
let success = false
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const result = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
success = !!result.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: {
|
||||
reason: 'Job timed out after 5s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
return success
|
||||
}
|
||||
|
||||
async function previewChanges() {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
console.log('Previewing changes for repo:', gitRepoResourcePath)
|
||||
isPreviewLoading = true
|
||||
jobStatus = { id: null, status: undefined, type: 'preview' }
|
||||
|
||||
try {
|
||||
// Always use the simplified JSON approach
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
dry_run: true,
|
||||
pull: true,
|
||||
only_wmill_yaml: false,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
jobStatus = { id: jobId, status: 'running', type: 'preview' }
|
||||
const success = await handleJobCompletion(jobId, workspace)
|
||||
|
||||
if (success) {
|
||||
const rawResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
console.log('Preview result:', rawResult)
|
||||
|
||||
// Convert new CLI format to expected format
|
||||
const added: string[] = []
|
||||
const deleted: string[] = []
|
||||
const modified: string[] = []
|
||||
|
||||
if (
|
||||
rawResult &&
|
||||
(rawResult as GitSyncResult).changes &&
|
||||
Array.isArray((rawResult as GitSyncResult).changes)
|
||||
) {
|
||||
for (const change of (rawResult as GitSyncResult).changes) {
|
||||
if (change.type === 'added') {
|
||||
added.push(change.path)
|
||||
} else if (change.type === 'deleted') {
|
||||
deleted.push(change.path)
|
||||
} else if (change.type === 'modified') {
|
||||
modified.push(change.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For full sync mode, just use the CLI results directly
|
||||
// The CLI already handles wmill.yaml changes with --include-wmill-yaml flag
|
||||
previewResult = { added, deleted, modified }
|
||||
jobStatus.status = 'success'
|
||||
} else {
|
||||
previewResult = undefined
|
||||
jobStatus.status = 'failure'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to preview changes:', error)
|
||||
previewResult = undefined
|
||||
jobStatus = {
|
||||
...jobStatus,
|
||||
status: 'failure',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
} finally {
|
||||
isPreviewLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function pullFromRepo() {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
console.log('Pulling from repo:', gitRepoResourcePath)
|
||||
isPulling = true
|
||||
jobStatus = { id: null, status: undefined, type: 'pull' }
|
||||
|
||||
try {
|
||||
// Use init git repo script with dry_run: false (actual pull operation)
|
||||
// The script will read wmill.yaml directly from the cloned repo, no need to pass settings
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
dry_run: false,
|
||||
branch_to_push: '',
|
||||
only_wmill_yaml: false,
|
||||
pull: true,
|
||||
settings_json: undefined // Let script use wmill.yaml from repo
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
jobStatus = { id: jobId, status: 'running', type: 'pull' }
|
||||
const success = await handleJobCompletion(jobId, workspace)
|
||||
jobStatus.status = success ? 'success' : 'failure'
|
||||
|
||||
if (success) {
|
||||
// Get the result which should contain the local git repo settings as JSON
|
||||
const result = (await JobService.getCompletedJobResult({ workspace, id: jobId })) as any
|
||||
console.log('Pull result:', result)
|
||||
|
||||
// Apply the settings from the sync operation result to the UI
|
||||
if (result?.settings_json) {
|
||||
// Directly update the UI state with the JSON result - no YAML conversion needed!
|
||||
const settingsJson = result.settings_json as {
|
||||
include_path: string[]
|
||||
exclude_path?: string[]
|
||||
extra_include_path?: string[]
|
||||
include_type: string[]
|
||||
}
|
||||
onFilterUpdate({
|
||||
include_path: settingsJson.include_path || ['f/**'],
|
||||
exclude_path: settingsJson.exclude_path || [],
|
||||
extra_include_path: settingsJson.extra_include_path || [],
|
||||
include_type: settingsJson.include_type || ['script', 'flow', 'app', 'folder']
|
||||
})
|
||||
sendUserToast('Successfully pulled workspace content from repository')
|
||||
|
||||
// Reset popover state after successful pull
|
||||
previewResult = undefined
|
||||
jobStatus = { id: null, status: undefined, type: 'preview' }
|
||||
pullGitRepoPopover?.close()
|
||||
} else {
|
||||
console.warn('No settings_json returned from pull operation')
|
||||
sendUserToast('Pull completed but could not update filter settings', true)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to pull from repo:', error)
|
||||
jobStatus = {
|
||||
...jobStatus,
|
||||
status: 'failure',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
} finally {
|
||||
isPulling = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover
|
||||
bind:this={pullGitRepoPopover}
|
||||
floatingConfig={{
|
||||
placement: 'top-start',
|
||||
strategy: 'fixed',
|
||||
flip: false,
|
||||
shift: true
|
||||
}}
|
||||
contentClasses="p-4 w-1/3"
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
color="dark"
|
||||
size="sm"
|
||||
nonCaptureEvent
|
||||
onclick={pullGitRepoPopover?.open}
|
||||
startIcon={{ icon: DownloadCloud }}
|
||||
>
|
||||
Pull workspace from Git repo
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-lg font-semibold">Pull workspace from Git repository</h3>
|
||||
<div class="prose max-w-none text-2xs text-tertiary">
|
||||
This action will pull all workspace objects from your Git repository according to the
|
||||
filters set in the Git repository wmill.yaml file and apply those filter settings to the
|
||||
workspace.
|
||||
<span class="text-orange-600 flex items-center gap-1">
|
||||
<AlertTriangle size={14} /> This will overwrite your current workspace content and Git sync
|
||||
filter settings with the content from the Git repository.
|
||||
</span>
|
||||
|
||||
<!-- Collapsible CLI Info Section -->
|
||||
<div class="mt-2 border rounded-md">
|
||||
<button
|
||||
class="w-full flex items-center justify-between p-1.5 bg-surface-secondary hover:bg-surface-hover"
|
||||
onclick={() => (isCliInfoExpanded = !isCliInfoExpanded)}
|
||||
>
|
||||
<span class="font-medium flex items-center gap-2">
|
||||
<Terminal size={14} />
|
||||
Windmill CLI to push local files to Windmill
|
||||
</span>
|
||||
{#if isCliInfoExpanded}
|
||||
<ChevronUp size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isCliInfoExpanded}
|
||||
<div class="p-1 bg-surface-tertiary">
|
||||
<div class="text-2xs mb-2">
|
||||
Not familiar with Windmill CLI? <a
|
||||
href="https://www.windmill.dev/docs/advanced/cli/sync"
|
||||
class="text-blue-500 hover:text-blue-600 underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Check out the docs</a
|
||||
>
|
||||
</div>
|
||||
<div class="font-mono text-2xs">
|
||||
<pre class="overflow-auto max-h-60"
|
||||
><code
|
||||
>npm install -g windmill-cli
|
||||
# Clone your git repository
|
||||
git clone $REPO_URL
|
||||
cd $REPO_NAME
|
||||
# Configure Windmill CLI
|
||||
wmill workspace add {$workspaceStore} {$workspaceStore} {`${$page.url.protocol}//${$page.url.hostname}/`}
|
||||
# Push the content to Windmill
|
||||
wmill sync push --yes
|
||||
# Optional: add --skip-secrets --skip-variables --skip-resources flags as needed</code
|
||||
></pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if previewResult}
|
||||
<GitDiffPreview {previewResult} />
|
||||
{/if}
|
||||
|
||||
{#if jobStatus.id}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if jobStatus.status === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if jobStatus.status === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if jobStatus.status === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
{jobStatus.type === 'preview' ? 'Preview' : 'Pull'} job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${jobStatus.id}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{jobStatus.id}
|
||||
</a>
|
||||
</div>
|
||||
{#if jobStatus.error}
|
||||
<div class="text-xs text-red-600">{jobStatus.error}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
previewResult = undefined
|
||||
jobStatus = { id: null, status: undefined, type: 'preview' }
|
||||
close()
|
||||
}}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div class="flex gap-2">
|
||||
{#if !previewResult}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={previewChanges}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
title="Preview changes again"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
{#if previewResult.added?.length || previewResult.deleted?.length || previewResult.modified?.length}
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
on:click={pullFromRepo}
|
||||
disabled={isPreviewLoading || isPulling}
|
||||
startIcon={{
|
||||
icon: isPulling ? Loader2 : Save,
|
||||
classes: isPulling ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isPulling ? 'Pulling...' : 'Pull'}
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -25,7 +25,6 @@
|
||||
defaultValues?: Record<string, any> | undefined
|
||||
placeholder?: string | undefined
|
||||
onClear?: () => void
|
||||
excludedValues?: string[]
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -40,8 +39,7 @@
|
||||
expressOAuthSetup = false,
|
||||
defaultValues = undefined,
|
||||
placeholder = undefined,
|
||||
onClear = undefined,
|
||||
excludedValues = undefined
|
||||
onClear = undefined
|
||||
}: Props = $props()
|
||||
|
||||
if (initialValue && value == undefined) {
|
||||
@@ -106,7 +104,6 @@
|
||||
const nc = resources
|
||||
.flat()
|
||||
.filter((x) => x.resource_type != 'state' && x.resource_type != 'cache')
|
||||
.filter((x) => !excludedValues || !excludedValues.includes(x.path))
|
||||
.map((x) => ({
|
||||
value: x.path,
|
||||
label: x.path,
|
||||
@@ -143,13 +140,6 @@
|
||||
untrack(() => loadResources(resourceType))
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
excludedValues
|
||||
if ($workspaceStore && resourceType && !disabled) {
|
||||
untrack(() => loadResources(resourceType))
|
||||
}
|
||||
})
|
||||
|
||||
let appConnect: AppConnect | undefined = $state()
|
||||
let resourceEditor: ResourceEditorDrawer | undefined = $state()
|
||||
let dbManagerDrawer: DbManagerDrawer | undefined = $state()
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
init(wasmUrl)
|
||||
|
||||
import { argSigToJsonSchemaType } from 'windmill-utils-internal'
|
||||
import { argSigToJsonSchemaType } from '$lib/inferArgSig'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
actionKind: ActionKind | undefined | 'all',
|
||||
scope: undefined | 'all_workspaces' | 'instance'
|
||||
): Promise<void> {
|
||||
console.log("loading logs")
|
||||
loading = true
|
||||
|
||||
if (username == 'all') {
|
||||
@@ -130,6 +131,7 @@
|
||||
hasMore = logs.length > 0 && logs.length === perPage
|
||||
|
||||
loading = false
|
||||
console.log("loadede logs")
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
@@ -171,7 +173,7 @@
|
||||
addQueryParam('workspace', 'admins')
|
||||
}
|
||||
const query = '?' + queryParams.join('&')
|
||||
goto(query, { replaceState: true, keepFocus: true })
|
||||
goto(query)
|
||||
|
||||
loadLogs(username, page, perPage, before, after, operation, resource, actionKind, scope)
|
||||
}
|
||||
@@ -350,13 +352,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<span class="text-xs absolute -top-4">From</span>
|
||||
<input type="text" value={after ?? 'From'} disabled />
|
||||
<span class="text-xs absolute -top-4">After</span>
|
||||
<input type="text" value={after ?? 'After'} disabled />
|
||||
<CalendarPicker
|
||||
clearable
|
||||
date={after}
|
||||
placement="bottom-end"
|
||||
label="From"
|
||||
label="After"
|
||||
on:change={({ detail }) => {
|
||||
after = new Date(detail).toISOString()
|
||||
}}
|
||||
@@ -366,12 +368,12 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<span class="text-xs absolute -top-4">To</span>
|
||||
<input type="text" value={before ?? 'To'} disabled />
|
||||
<span class="text-xs absolute -top-4">Before</span>
|
||||
<input type="text" value={before ?? 'Before'} disabled />
|
||||
<CalendarPicker
|
||||
clearable
|
||||
bind:date={before}
|
||||
label="To"
|
||||
label="Before"
|
||||
placement="bottom-end"
|
||||
on:change={({ detail }) => {
|
||||
before = new Date(detail).toISOString()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Button from '../button/Button.svelte'
|
||||
import Badge from '../badge/Badge.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import CloseButton from '../CloseButton.svelte'
|
||||
|
||||
@@ -37,7 +38,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown|capture={onKeyDown} />
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if open}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -97,7 +98,9 @@
|
||||
color="light"
|
||||
size="sm"
|
||||
>
|
||||
{cancelText ?? 'Cancel'}
|
||||
<span class="inline-flex gap-2"
|
||||
>{cancelText ?? 'Cancel'}<Badge color="dark-gray">Escape</Badge></span
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -365,7 +365,7 @@
|
||||
}}
|
||||
{placeholder}
|
||||
class={twMerge(
|
||||
'textarea-input resize-none bg-transparent caret-black dark:caret-white overflow-clip',
|
||||
'textarea-input resize-none bg-transparent caret-black dark:caret-white',
|
||||
className
|
||||
)}
|
||||
style={value.length > 0 ? 'color: transparent; -webkit-text-fill-color: transparent;' : ''}
|
||||
|
||||
@@ -652,7 +652,6 @@
|
||||
jobId={undefined}
|
||||
result={mock?.return_value}
|
||||
externalToolbarAvailable
|
||||
fixTableSizingToParent
|
||||
on:toolbar-location-changed={({ detail }) => {
|
||||
toolbarLocationMock = detail
|
||||
}}
|
||||
@@ -680,7 +679,6 @@
|
||||
jobId={selectedJob?.id}
|
||||
result={selectedJob?.result}
|
||||
externalToolbarAvailable
|
||||
fixTableSizingToParent
|
||||
on:toolbar-location-changed={({ detail }) => {
|
||||
toolbarLocationJob = detail
|
||||
}}
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { FileSearch, Save, Loader2, CheckCircle2, XCircle } from 'lucide-svelte'
|
||||
import { Button, Alert } from '$lib/components/common'
|
||||
import { getGitSyncContext } from './GitSyncContext.svelte'
|
||||
import GitSyncFilterSettings from '$lib/components/workspaceSettings/GitSyncFilterSettings.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
let { idx } = $props<{ idx: number }>()
|
||||
|
||||
const gitSyncContext = getGitSyncContext()
|
||||
const repo = $derived(gitSyncContext.getRepository(idx))
|
||||
|
||||
async function handleDetect() {
|
||||
try {
|
||||
await gitSyncContext.detectRepository(idx)
|
||||
} catch (error: any) {
|
||||
console.error('Detection failed:', error)
|
||||
sendUserToast('Detection failed: ' + error.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInitialize() {
|
||||
if (!repo || repo.detectionState !== 'no-wmill') return
|
||||
|
||||
try {
|
||||
// Show push modal for initialization
|
||||
gitSyncContext.showPushModal(idx)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to initialize repository:', error)
|
||||
sendUserToast('Failed to initialize repository: ' + error.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveConnection() {
|
||||
if (!repo || repo.detectionState !== 'has-wmill') return
|
||||
|
||||
try {
|
||||
await gitSyncContext.saveRepository(idx)
|
||||
sendUserToast('Git sync connection saved successfully')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to save connection:', error)
|
||||
sendUserToast('Failed to save connection: ' + error.message, true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if repo}
|
||||
<div class="space-y-4">
|
||||
{#if !repo.detectionState || repo.detectionState === 'idle'}
|
||||
<!-- Step 1: Check repo settings button -->
|
||||
<div class="flex justify-start">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="border"
|
||||
size="sm"
|
||||
onclick={handleDetect}
|
||||
startIcon={{ icon: FileSearch }}
|
||||
>
|
||||
Check repo settings
|
||||
</Button>
|
||||
</div>
|
||||
{:else if repo.detectionState === 'loading'}
|
||||
<!-- Loading state -->
|
||||
<div class="flex items-center gap-2">
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
<span class="text-sm">Checking repository...</span>
|
||||
</div>
|
||||
{:else if repo.detectionState === 'no-wmill'}
|
||||
<!-- No wmill.yaml found - new repository -->
|
||||
<Alert type="info" title="Uninitialized Windmill repository found" class="my-2">
|
||||
No git sync configuration found. Configure your sync settings below.
|
||||
</Alert>
|
||||
|
||||
<GitSyncFilterSettings
|
||||
git_repo_resource_path={repo.git_repo_resource_path}
|
||||
bind:include_path={repo.settings.include_path}
|
||||
bind:include_type={repo.settings.include_type}
|
||||
bind:exclude_types_override={repo.exclude_types_override}
|
||||
isLegacyRepo={false}
|
||||
bind:excludes={repo.settings.exclude_path}
|
||||
bind:extraIncludes={repo.settings.extra_include_path}
|
||||
isInitialSetup={true}
|
||||
requiresMigration={false}
|
||||
/>
|
||||
|
||||
<!-- Toggles for new repositories -->
|
||||
<div class="space-y-3">
|
||||
<Toggle
|
||||
disabled={!repo.git_repo_resource_path}
|
||||
bind:checked={repo.use_individual_branch}
|
||||
options={{
|
||||
right: 'Create one branch per deployed object',
|
||||
rightTooltip: "If set, Windmill will create a unique branch per object being pushed based on its path, prefixed with 'wm_deploy/'."
|
||||
}}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
disabled={!repo.git_repo_resource_path || !repo.use_individual_branch}
|
||||
bind:checked={repo.group_by_folder}
|
||||
options={{
|
||||
right: 'Group deployed objects by folder',
|
||||
rightTooltip: 'Instead of creating a branch per object, Windmill will create a branch per folder containing objects being deployed.'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Initialize button -->
|
||||
<div class="flex justify-start">
|
||||
<Button
|
||||
size="md"
|
||||
onclick={handleInitialize}
|
||||
startIcon={{ icon: Save }}
|
||||
>
|
||||
Initialize Git repository
|
||||
</Button>
|
||||
</div>
|
||||
{:else if repo.detectionState === 'has-wmill'}
|
||||
<!-- wmill.yaml found - existing repository -->
|
||||
<Alert type="success" title="Existing Windmill repository found" class="my-2">
|
||||
Found existing git sync configuration. Settings loaded from repository.
|
||||
</Alert>
|
||||
|
||||
<GitSyncFilterSettings
|
||||
git_repo_resource_path={repo.git_repo_resource_path}
|
||||
bind:include_path={repo.settings.include_path}
|
||||
bind:include_type={repo.settings.include_type}
|
||||
bind:exclude_types_override={repo.exclude_types_override}
|
||||
isLegacyRepo={false}
|
||||
bind:excludes={repo.settings.exclude_path}
|
||||
bind:extraIncludes={repo.settings.extra_include_path}
|
||||
isInitialSetup={false}
|
||||
requiresMigration={false}
|
||||
/>
|
||||
|
||||
<!-- Toggles for existing repositories -->
|
||||
<div class="space-y-3">
|
||||
<Toggle
|
||||
disabled={!repo.git_repo_resource_path}
|
||||
bind:checked={repo.use_individual_branch}
|
||||
options={{
|
||||
right: 'Create one branch per deployed object',
|
||||
rightTooltip: "If set, Windmill will create a unique branch per object being pushed based on its path, prefixed with 'wm_deploy/'."
|
||||
}}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
disabled={!repo.git_repo_resource_path || !repo.use_individual_branch}
|
||||
bind:checked={repo.group_by_folder}
|
||||
options={{
|
||||
right: 'Group deployed objects by folder',
|
||||
rightTooltip: 'Instead of creating a branch per object, Windmill will create a branch per folder containing objects being deployed.'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Save connection button -->
|
||||
<div class="flex justify-start">
|
||||
<Button
|
||||
size="md"
|
||||
onclick={handleSaveConnection}
|
||||
startIcon={{ icon: Save }}
|
||||
>
|
||||
Save connection
|
||||
</Button>
|
||||
</div>
|
||||
{:else if repo.detectionState === 'error'}
|
||||
<!-- Error state -->
|
||||
<Alert type="error" title="Detection error" class="my-2">
|
||||
{repo.detectionError || 'Failed to check repository'}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- Job status display -->
|
||||
{#if repo.detectionJobId && (repo.detectionState === 'loading' || repo.detectionState === 'error')}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if repo.detectionJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if repo.detectionJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if repo.detectionJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Detection job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${repo.detectionJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{repo.detectionJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,583 +0,0 @@
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import { JobService, WorkspaceService } from '$lib/gen'
|
||||
import type { GitRepositorySettings as BackendGitRepositorySettings, GitSyncObjectType } from '$lib/gen'
|
||||
import { jobManager } from '$lib/services/JobManager'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import type { SettingsObject } from '$lib/git-sync'
|
||||
|
||||
export type GitSyncRepository = BackendGitRepositorySettings & {
|
||||
settings: SettingsObject
|
||||
exclude_types_override: GitSyncObjectType[]
|
||||
legacyImported?: boolean
|
||||
isUnsavedConnection?: boolean
|
||||
collapsed?: boolean
|
||||
// Repository detection state for new connections
|
||||
detectionState?: 'idle' | 'loading' | 'no-wmill' | 'has-wmill' | 'error'
|
||||
extractedSettings?: SettingsObject
|
||||
detectionError?: string
|
||||
// Job tracking for detection
|
||||
detectionJobId?: string
|
||||
detectionJobStatus?: 'running' | 'success' | 'failure'
|
||||
// Internal tracking for resource path changes
|
||||
_trackedPath?: string
|
||||
}
|
||||
|
||||
export type GitSyncTestJob = {
|
||||
jobId: string
|
||||
status: 'running' | 'success' | 'failure' | undefined
|
||||
}
|
||||
|
||||
export type GitSyncSettings = {
|
||||
repositories: GitSyncRepository[]
|
||||
}
|
||||
|
||||
export type ModalState = {
|
||||
push: { idx: number, repo: GitSyncRepository, open: boolean } | null
|
||||
pull: { idx: number, repo: GitSyncRepository, open: boolean, settingsOnly?: boolean } | null
|
||||
success: { open: boolean, savedWithoutInit?: boolean } | null
|
||||
}
|
||||
|
||||
export type ValidationState = {
|
||||
isValid: boolean
|
||||
isDuplicate: boolean
|
||||
hasChanges: boolean
|
||||
}
|
||||
|
||||
const GIT_SYNC_CONTEXT_KEY = Symbol('git-sync-context')
|
||||
|
||||
// Context implementation
|
||||
export function createGitSyncContext(workspace: string) {
|
||||
const repositories = $state<GitSyncRepository[]>([])
|
||||
const initialRepositories = $state<GitSyncRepository[]>([])
|
||||
const gitSyncTestJobs = $state<GitSyncTestJob[]>([])
|
||||
let loading = $state(false)
|
||||
const activeModals = $state<ModalState>({ push: null, pull: null, success: null })
|
||||
|
||||
// Legacy workspace-level settings state
|
||||
const legacyWorkspaceIncludePath = $state<string[]>([])
|
||||
const legacyWorkspaceIncludeType = $state<GitSyncObjectType[]>([])
|
||||
|
||||
// Derived state for legacy detection
|
||||
const hasWorkspaceLevelSettings = $derived(
|
||||
legacyWorkspaceIncludePath.length > 0 || legacyWorkspaceIncludeType.length > 0
|
||||
)
|
||||
|
||||
// Watch for changes to git repository paths and reset detection state
|
||||
$effect(() => {
|
||||
repositories.forEach((repo) => {
|
||||
if (repo.isUnsavedConnection) {
|
||||
const currentPath = repo.git_repo_resource_path
|
||||
|
||||
if (repo._trackedPath && repo._trackedPath !== currentPath && repo.detectionState && repo.detectionState !== 'idle') {
|
||||
_resetRepoDetectionState(repo)
|
||||
}
|
||||
|
||||
repo._trackedPath = currentPath
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const getValidationStates = () => {
|
||||
return repositories.map((repo, idx) => ({
|
||||
isValid: validateRepository(repo, idx),
|
||||
isDuplicate: checkDuplicate(repo, idx),
|
||||
hasChanges: checkChanges(repo, idx)
|
||||
}))
|
||||
}
|
||||
|
||||
const getHasAnyChanges = () => {
|
||||
const validationStates = getValidationStates()
|
||||
|
||||
// Check if any individual repositories have changes
|
||||
const individualChanges = validationStates.some(v => v.hasChanges)
|
||||
|
||||
// Check if any legacy repos were imported
|
||||
const anyLegacyImported = repositories.some(r => r.legacyImported)
|
||||
|
||||
// Check if the set of repositories has changed (added/removed repos)
|
||||
const repositorySetChanged = (() => {
|
||||
if (loading) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!initialRepositories || initialRepositories.length === 0) {
|
||||
return repositories.filter((_,i) => validationStates[i]?.isValid).length > 0
|
||||
}
|
||||
|
||||
const initialValidPaths = new Set(
|
||||
initialRepositories
|
||||
.filter(r => r.git_repo_resource_path && r.git_repo_resource_path.trim() !== '')
|
||||
.map(r => r.git_repo_resource_path)
|
||||
)
|
||||
const currentValidPaths = new Set(
|
||||
repositories
|
||||
.filter((_,i) => validationStates[i]?.isValid)
|
||||
.map(r => r.git_repo_resource_path)
|
||||
)
|
||||
|
||||
// Check if sets are different (repos added or removed)
|
||||
return initialValidPaths.size !== currentValidPaths.size ||
|
||||
[...initialValidPaths].some(path => !currentValidPaths.has(path)) ||
|
||||
[...currentValidPaths].some(path => !initialValidPaths.has(path))
|
||||
})()
|
||||
|
||||
return individualChanges || anyLegacyImported || repositorySetChanged
|
||||
}
|
||||
|
||||
const getAllRepositoriesValid = () => getValidationStates().every(v => v.isValid)
|
||||
const getHasUnsavedConnections = () => repositories.some(repo => repo.isUnsavedConnection)
|
||||
|
||||
function validateRepository(repo: GitSyncRepository, idx: number): boolean {
|
||||
if (!repo.git_repo_resource_path) return false
|
||||
return !checkDuplicate(repo, idx)
|
||||
}
|
||||
|
||||
function checkDuplicate(repo: GitSyncRepository, idx: number): boolean {
|
||||
if (!repo.git_repo_resource_path) return false
|
||||
const firstIdx = repositories.findIndex(r => r.git_repo_resource_path === repo.git_repo_resource_path)
|
||||
return firstIdx !== -1 && firstIdx < idx
|
||||
}
|
||||
|
||||
function checkChanges(repo: GitSyncRepository, idx: number): boolean {
|
||||
const initial = initialRepositories[idx]
|
||||
if (!initial) return true
|
||||
|
||||
// Legacy repositories always have "changes" because they need migration
|
||||
if (repo.legacyImported) return true
|
||||
|
||||
return JSON.stringify(serializeRepository(repo)) !== JSON.stringify(serializeRepository(initial))
|
||||
}
|
||||
|
||||
function serializeRepository(repo: GitSyncRepository) {
|
||||
return {
|
||||
git_repo_resource_path: repo.git_repo_resource_path,
|
||||
script_path: repo.script_path,
|
||||
use_individual_branch: repo.use_individual_branch,
|
||||
group_by_folder: repo.group_by_folder,
|
||||
settings: repo.settings,
|
||||
exclude_types_override: repo.exclude_types_override
|
||||
}
|
||||
}
|
||||
|
||||
function addRepository() {
|
||||
repositories.push({
|
||||
git_repo_resource_path: '',
|
||||
script_path: hubPaths.gitSync,
|
||||
use_individual_branch: false,
|
||||
group_by_folder: false,
|
||||
settings: {
|
||||
include_path: ['f/**'],
|
||||
exclude_path: [],
|
||||
extra_include_path: [],
|
||||
include_type: ['script', 'flow', 'app', 'folder']
|
||||
},
|
||||
exclude_types_override: [],
|
||||
legacyImported: false,
|
||||
isUnsavedConnection: true,
|
||||
collapsed: false
|
||||
})
|
||||
gitSyncTestJobs.push({
|
||||
jobId: '',
|
||||
status: undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function removeRepository(idx: number) {
|
||||
const repo = repositories[idx]
|
||||
if (!repo) return
|
||||
|
||||
// Check if this repository exists in the initial (saved) state
|
||||
const existsInInitialState = initialRepositories.some(
|
||||
initialRepo => initialRepo.git_repo_resource_path === repo.git_repo_resource_path
|
||||
)
|
||||
|
||||
// Only call backend API if repository exists in the saved state
|
||||
if (existsInInitialState && repo.git_repo_resource_path) {
|
||||
await WorkspaceService.deleteGitSyncRepository({
|
||||
workspace,
|
||||
requestBody: {
|
||||
git_repo_resource_path: `$res:${repo.git_repo_resource_path}`
|
||||
}
|
||||
})
|
||||
|
||||
// Update initial state to remove the deleted repository
|
||||
const initialIdx = initialRepositories.findIndex(
|
||||
initialRepo => initialRepo.git_repo_resource_path === repo.git_repo_resource_path
|
||||
)
|
||||
if (initialIdx !== -1) {
|
||||
initialRepositories.splice(initialIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from local state
|
||||
repositories.splice(idx, 1)
|
||||
gitSyncTestJobs.splice(idx, 1)
|
||||
}
|
||||
|
||||
function getRepository(idx: number) {
|
||||
return repositories[idx]
|
||||
}
|
||||
|
||||
function showPushModal(idx: number) {
|
||||
const repo = repositories[idx]
|
||||
if (repo) {
|
||||
activeModals.push = { idx, repo, open: true }
|
||||
}
|
||||
}
|
||||
|
||||
function showPullModal(idx: number, settingsOnly = false) {
|
||||
const repo = repositories[idx]
|
||||
if (repo) {
|
||||
activeModals.pull = { idx, repo, open: true, settingsOnly }
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(type: 'push' | 'pull' | 'success') {
|
||||
if (activeModals[type]) {
|
||||
activeModals[type]!.open = false
|
||||
}
|
||||
setTimeout(() => {
|
||||
activeModals[type] = null
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function closePushModal() {
|
||||
closeModal('push')
|
||||
}
|
||||
|
||||
function closePullModal() {
|
||||
closeModal('pull')
|
||||
}
|
||||
|
||||
function showSuccessModal(savedWithoutInit?: boolean) {
|
||||
activeModals.success = { open: true, savedWithoutInit }
|
||||
}
|
||||
|
||||
function closeSuccessModal() {
|
||||
closeModal('success')
|
||||
}
|
||||
|
||||
function getValidation(idx: number): ValidationState {
|
||||
const states = getValidationStates()
|
||||
return states[idx] || { isValid: false, isDuplicate: false, hasChanges: false }
|
||||
}
|
||||
|
||||
async function detectRepository(idx: number) {
|
||||
const repo = repositories[idx]
|
||||
if (!repo || !repo.git_repo_resource_path) {
|
||||
throw new Error('Repository not found or no resource path')
|
||||
}
|
||||
|
||||
repo.detectionState = 'loading'
|
||||
repo.detectionError = undefined
|
||||
repo.detectionJobId = undefined
|
||||
repo.detectionJobStatus = undefined
|
||||
|
||||
try {
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: repo.git_repo_resource_path,
|
||||
dry_run: true,
|
||||
pull: false,
|
||||
only_wmill_yaml: true,
|
||||
settings_json: JSON.stringify(repo.settings)
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
repo.detectionJobId = jobId
|
||||
repo.detectionJobStatus = 'running'
|
||||
|
||||
// Use JobManager for polling - result will be the actual job response
|
||||
await jobManager.runWithProgress(
|
||||
() => Promise.resolve(jobId),
|
||||
{
|
||||
workspace,
|
||||
timeout: 30000,
|
||||
timeoutMessage: 'Detection job timed out after 30s',
|
||||
onProgress: (status) => {
|
||||
repo.detectionJobStatus = status.status
|
||||
|
||||
// Process successful detection result
|
||||
if (status.status === 'success' && status.result) {
|
||||
const response = status.result as any
|
||||
if (response.isInitialSetup) {
|
||||
repo.detectionState = 'no-wmill'
|
||||
} else {
|
||||
repo.detectionState = 'has-wmill'
|
||||
// Apply extracted settings from the git repository
|
||||
if (response.local) {
|
||||
repo.extractedSettings = response.local
|
||||
// Auto-apply the extracted settings
|
||||
repo.settings = { ...response.local }
|
||||
}
|
||||
}
|
||||
} else if (status.status === 'failure') {
|
||||
repo.detectionState = 'error'
|
||||
repo.detectionError = status.error || 'Detection failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (error: any) {
|
||||
repo.detectionState = 'error'
|
||||
repo.detectionError = error?.message || error?.toString() || 'Failed to detect repository'
|
||||
repo.detectionJobStatus = 'failure'
|
||||
}
|
||||
}
|
||||
|
||||
// Settings management
|
||||
async function loadSettings() {
|
||||
loading = true
|
||||
try {
|
||||
const settings = await WorkspaceService.getSettings({ workspace })
|
||||
|
||||
if (settings.git_sync !== undefined && settings.git_sync !== null) {
|
||||
// Detect workspace-level legacy settings (outside repositories)
|
||||
const workspaceLegacyIncludePath: string[] = (settings.git_sync as any)?.include_path ?? []
|
||||
const workspaceLegacyIncludeTypeRaw: GitSyncObjectType[] = (settings.git_sync as any)?.include_type ?? []
|
||||
const workspaceLegacyIncludeType: GitSyncObjectType[] = [...workspaceLegacyIncludeTypeRaw]
|
||||
|
||||
// Update legacy workspace state
|
||||
legacyWorkspaceIncludePath.splice(0, legacyWorkspaceIncludePath.length, ...workspaceLegacyIncludePath)
|
||||
legacyWorkspaceIncludeType.splice(0, legacyWorkspaceIncludeType.length, ...workspaceLegacyIncludeType)
|
||||
|
||||
if (settings.git_sync.repositories) {
|
||||
repositories.splice(0, repositories.length, ...settings.git_sync.repositories.map(repo => {
|
||||
// Check if this is a legacy repo (no nested settings object)
|
||||
const isRepoLegacy = !repo.settings
|
||||
const repoExcludeTypesOverride = repo.exclude_types_override ?? []
|
||||
|
||||
// Determine default types - use workspace legacy or fallback
|
||||
const defaultTypes: GitSyncObjectType[] = workspaceLegacyIncludeType.length > 0
|
||||
? [...workspaceLegacyIncludeType]
|
||||
: ['script', 'flow', 'app', 'folder']
|
||||
|
||||
let repoSettings: SettingsObject
|
||||
if (isRepoLegacy) {
|
||||
// Legacy repo: inherit from workspace-level settings and apply exclude_types_override
|
||||
const inheritedIncludeType = repo.settings?.include_type ?? [...defaultTypes]
|
||||
const effectiveIncludeType = repoExcludeTypesOverride.length > 0
|
||||
? inheritedIncludeType.filter(type => !repoExcludeTypesOverride.includes(type))
|
||||
: inheritedIncludeType
|
||||
|
||||
repoSettings = {
|
||||
include_path: repo.settings?.include_path ?? [...workspaceLegacyIncludePath],
|
||||
exclude_path: repo.settings?.exclude_path ?? [],
|
||||
extra_include_path: repo.settings?.extra_include_path ?? [],
|
||||
include_type: effectiveIncludeType
|
||||
}
|
||||
} else {
|
||||
// New format: use repo's own settings
|
||||
repoSettings = {
|
||||
include_path: repo.settings?.include_path ?? ['f/**'],
|
||||
exclude_path: repo.settings?.exclude_path ?? [],
|
||||
extra_include_path: repo.settings?.extra_include_path ?? [],
|
||||
include_type: repo.settings?.include_type ?? ['script', 'flow', 'app']
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...repo,
|
||||
git_repo_resource_path: repo.git_repo_resource_path.replace('$res:', ''),
|
||||
settings: repoSettings,
|
||||
exclude_types_override: repoExcludeTypesOverride,
|
||||
// Mark legacy repos for UI handling
|
||||
legacyImported: isRepoLegacy
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Store initial state for change tracking
|
||||
initialRepositories.splice(0, initialRepositories.length, ...repositories.map(repo => ({ ...repo })))
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Migration utility for legacy repositories
|
||||
function migrateLegacyRepository(repo: GitSyncRepository): GitSyncRepository {
|
||||
if (!repo.legacyImported) {
|
||||
return repo // Already migrated or not legacy
|
||||
}
|
||||
|
||||
// Create migrated repository - exclude_types_override should already be applied in settings.include_type
|
||||
// from the loadSettings logic, so we just need to clear the override and mark as migrated
|
||||
return {
|
||||
...repo,
|
||||
exclude_types_override: [], // Clear the override since it's now integrated into include_type
|
||||
legacyImported: false // Mark as migrated
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRepository(idx: number, savedWithoutInit = false) {
|
||||
const repo = repositories[idx]
|
||||
if (!repo || !validateRepository(repo, idx)) {
|
||||
throw new Error('Cannot save invalid repository')
|
||||
}
|
||||
|
||||
// Migrate legacy repository if needed
|
||||
const repoToSave = repo.legacyImported ? migrateLegacyRepository(repo) : repo
|
||||
|
||||
// Use the new individual repository API instead of saving all repositories
|
||||
await WorkspaceService.editGitSyncRepository({
|
||||
workspace,
|
||||
requestBody: {
|
||||
git_repo_resource_path: `$res:${repoToSave.git_repo_resource_path}`,
|
||||
repository: {
|
||||
git_repo_resource_path: `$res:${repoToSave.git_repo_resource_path}`,
|
||||
script_path: repoToSave.script_path,
|
||||
use_individual_branch: repoToSave.use_individual_branch,
|
||||
group_by_folder: repoToSave.group_by_folder,
|
||||
settings: repoToSave.settings,
|
||||
exclude_types_override: repoToSave.exclude_types_override
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Update local state with migrated repository
|
||||
repositories[idx] = repoToSave
|
||||
initialRepositories[idx] = { ...repoToSave }
|
||||
|
||||
// Update local state
|
||||
if (repoToSave.isUnsavedConnection) {
|
||||
repoToSave.isUnsavedConnection = false
|
||||
repoToSave.detectionState = undefined
|
||||
repoToSave.extractedSettings = undefined
|
||||
// Show success modal for new connections
|
||||
showSuccessModal(savedWithoutInit)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Helper functions for original functionality
|
||||
|
||||
function revertRepository(idx: number) {
|
||||
const initial = initialRepositories[idx]
|
||||
if (initial) {
|
||||
repositories[idx] = JSON.parse(JSON.stringify(initial))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Reset detection state for a repository
|
||||
function resetDetectionState(idx: number) {
|
||||
const repo = repositories[idx]
|
||||
if (!repo || !repo.isUnsavedConnection) return
|
||||
|
||||
_resetRepoDetectionState(repo)
|
||||
}
|
||||
|
||||
// Helper function to reset detection state on a repository object
|
||||
function _resetRepoDetectionState(repo: GitSyncRepository) {
|
||||
repo.detectionState = 'idle'
|
||||
repo.extractedSettings = undefined
|
||||
repo.detectionError = undefined
|
||||
repo.detectionJobId = undefined
|
||||
repo.detectionJobStatus = undefined
|
||||
}
|
||||
|
||||
|
||||
async function runTestJob(idx: number) {
|
||||
const repo = repositories[idx]
|
||||
if (!repo?.git_repo_resource_path || !repo?.script_path) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitSyncTest,
|
||||
requestBody: {
|
||||
repo_url_resource_path: repo.git_repo_resource_path
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
gitSyncTestJobs[idx] = {
|
||||
jobId: jobId,
|
||||
status: 'running'
|
||||
}
|
||||
|
||||
// Use JobManager for polling
|
||||
await jobManager.runWithProgress(
|
||||
() => Promise.resolve(jobId),
|
||||
{
|
||||
workspace,
|
||||
timeout: 5000,
|
||||
timeoutMessage: 'Git sync test job timed out after 5s',
|
||||
onProgress: (status) => {
|
||||
gitSyncTestJobs[idx].status = status.status === 'success' ? 'success' :
|
||||
status.status === 'failure' ? 'failure' : 'running'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// If we get here, the job completed successfully
|
||||
gitSyncTestJobs[idx].status = 'success'
|
||||
} catch (error) {
|
||||
gitSyncTestJobs[idx].status = 'failure'
|
||||
}
|
||||
}
|
||||
|
||||
// Return context object
|
||||
return {
|
||||
// State (read-only access)
|
||||
get repositories() { return repositories },
|
||||
get loading() { return loading },
|
||||
get activeModals() { return activeModals },
|
||||
get gitSyncTestJobs() { return gitSyncTestJobs },
|
||||
get initialRepositories() { return initialRepositories },
|
||||
get legacyWorkspaceIncludePath() { return legacyWorkspaceIncludePath },
|
||||
get legacyWorkspaceIncludeType() { return legacyWorkspaceIncludeType },
|
||||
|
||||
// Computed states - use getter functions that compute on access
|
||||
get validationStates() { return getValidationStates() },
|
||||
get hasAnyChanges() { return getHasAnyChanges() },
|
||||
get allRepositoriesValid() { return getAllRepositoriesValid() },
|
||||
get hasUnsavedConnections() { return getHasUnsavedConnections() },
|
||||
get hasWorkspaceLevelSettings() { return hasWorkspaceLevelSettings },
|
||||
|
||||
// Methods
|
||||
addRepository,
|
||||
removeRepository,
|
||||
getRepository,
|
||||
getValidation,
|
||||
revertRepository,
|
||||
runTestJob,
|
||||
resetDetectionState,
|
||||
detectRepository,
|
||||
migrateLegacyRepository,
|
||||
showPushModal,
|
||||
showPullModal,
|
||||
closePushModal,
|
||||
closePullModal,
|
||||
showSuccessModal,
|
||||
closeSuccessModal,
|
||||
loadSettings,
|
||||
saveRepository,
|
||||
}
|
||||
}
|
||||
|
||||
export type GitSyncContextType = ReturnType<typeof createGitSyncContext>
|
||||
|
||||
export function setGitSyncContext(workspace: string): GitSyncContextType {
|
||||
const context = createGitSyncContext(workspace)
|
||||
setContext(GIT_SYNC_CONTEXT_KEY, context)
|
||||
return context
|
||||
}
|
||||
|
||||
export function getGitSyncContext(): GitSyncContextType {
|
||||
const context = getContext<GitSyncContextType>(GIT_SYNC_CONTEXT_KEY)
|
||||
if (!context) {
|
||||
throw new Error('Git sync context not found. Make sure to call setGitSyncContext first.')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getGitSyncContext } from './GitSyncContext.svelte'
|
||||
import PushWorkspaceModal from '$lib/components/git_sync/PushWorkspaceModal.svelte'
|
||||
import PullWorkspaceModal from '$lib/components/git_sync/PullWorkspaceModal.svelte'
|
||||
import GitSyncSuccessModal from '$lib/components/git_sync/GitSyncSuccessModal.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
const gitSyncContext = getGitSyncContext()
|
||||
|
||||
function handlePushSuccess() {
|
||||
const pushModal = gitSyncContext.activeModals.push
|
||||
if (!pushModal) return
|
||||
|
||||
const { idx, repo } = pushModal
|
||||
|
||||
// If this was a repository initialization, auto-save the connection
|
||||
if (repo.isUnsavedConnection && repo.detectionState === 'no-wmill') {
|
||||
gitSyncContext.saveRepository(idx).then(() => {
|
||||
sendUserToast('Repository initialized and connection saved successfully')
|
||||
}).catch((error) => {
|
||||
sendUserToast('Repository initialized but failed to save connection: ' + error.message, true)
|
||||
})
|
||||
} else {
|
||||
sendUserToast('Successfully pushed to git repository')
|
||||
}
|
||||
|
||||
gitSyncContext.closePushModal()
|
||||
}
|
||||
|
||||
|
||||
function handlePullSuccess() {
|
||||
sendUserToast('Successfully pulled from git repository')
|
||||
gitSyncContext.closePullModal()
|
||||
}
|
||||
|
||||
function handleFilterUpdate(idx: number, filters: any) {
|
||||
// Update the repository settings in the context
|
||||
const repo = gitSyncContext.getRepository(idx)
|
||||
if (repo) {
|
||||
repo.settings = filters
|
||||
}
|
||||
}
|
||||
|
||||
function handleSettingsSaved() {
|
||||
// Update initial state to reflect that current state has been saved externally
|
||||
gitSyncContext.initialRepositories.splice(0, gitSyncContext.initialRepositories.length, ...gitSyncContext.repositories.map(repo => ({ ...repo })))
|
||||
sendUserToast('Settings applied successfully')
|
||||
}
|
||||
|
||||
|
||||
async function handleSaveWithoutInit(idx: number) {
|
||||
try {
|
||||
await gitSyncContext.saveRepository(idx, true)
|
||||
sendUserToast('Connection saved successfully without initializing repository')
|
||||
gitSyncContext.closePushModal()
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to save connection: ' + error.message, true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Push Modal -->
|
||||
{#if gitSyncContext.activeModals.push}
|
||||
{@const { idx, repo } = gitSyncContext.activeModals.push}
|
||||
{@const isNewConnection = repo.isUnsavedConnection && repo.detectionState === 'no-wmill'}
|
||||
<PushWorkspaceModal
|
||||
bind:open={gitSyncContext.activeModals.push.open}
|
||||
gitRepoResourcePath={repo.git_repo_resource_path}
|
||||
uiState={{
|
||||
include_path: repo.settings.include_path,
|
||||
exclude_path: repo.settings.exclude_path,
|
||||
extra_include_path: repo.settings.extra_include_path,
|
||||
include_type: repo.settings.include_type
|
||||
}}
|
||||
isNewConnection={isNewConnection}
|
||||
onSuccess={handlePushSuccess}
|
||||
onSaveWithoutInit={isNewConnection ? () => handleSaveWithoutInit(idx) : undefined}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Pull Modal -->
|
||||
{#if gitSyncContext.activeModals.pull}
|
||||
{@const { idx, repo, settingsOnly } = gitSyncContext.activeModals.pull}
|
||||
<PullWorkspaceModal
|
||||
bind:open={gitSyncContext.activeModals.pull.open}
|
||||
gitRepoResourcePath={repo.git_repo_resource_path}
|
||||
repoIndex={idx}
|
||||
currentGitSyncSettings={gitSyncContext}
|
||||
uiState={{
|
||||
include_path: repo.settings.include_path,
|
||||
exclude_path: repo.settings.exclude_path,
|
||||
extra_include_path: repo.settings.extra_include_path,
|
||||
include_type: repo.settings.include_type
|
||||
}}
|
||||
onFilterUpdate={(filters) => handleFilterUpdate(idx, filters)}
|
||||
onSettingsSaved={handleSettingsSaved}
|
||||
onSuccess={handlePullSuccess}
|
||||
{settingsOnly}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Success Modal -->
|
||||
{#if gitSyncContext.activeModals.success}
|
||||
<GitSyncSuccessModal
|
||||
bind:open={gitSyncContext.activeModals.success.open}
|
||||
savedWithoutInit={gitSyncContext.activeModals.success.savedWithoutInit}
|
||||
/>
|
||||
{/if}
|
||||
@@ -1,361 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Save, Trash, XCircle, CheckCircle2, RotateCw, RotateCcw, Download, Upload } from 'lucide-svelte'
|
||||
import { Button, Alert } from '$lib/components/common'
|
||||
import { getGitSyncContext } from './GitSyncContext.svelte'
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import GitSyncFilterSettings from '$lib/components/workspaceSettings/GitSyncFilterSettings.svelte'
|
||||
import DetectionFlow from './DetectionFlow.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
|
||||
let { idx } = $props<{ idx: number }>()
|
||||
|
||||
const gitSyncContext = getGitSyncContext()
|
||||
const repo = $derived(gitSyncContext.getRepository(idx))
|
||||
const validation = $derived(gitSyncContext.getValidation(idx))
|
||||
const gitSyncTestJob = $derived(gitSyncContext.gitSyncTestJobs?.[idx])
|
||||
let confirmingDelete = $state(false)
|
||||
|
||||
// Compute already-used repository paths to exclude from picker
|
||||
const usedRepositoryPaths = $derived(
|
||||
gitSyncContext.repositories
|
||||
.map((r, i) => i !== idx ? r.git_repo_resource_path : null)
|
||||
.filter((path): path is string => Boolean(path?.trim()))
|
||||
)
|
||||
|
||||
async function handleSave() {
|
||||
if (!repo) return
|
||||
|
||||
try {
|
||||
await gitSyncContext.saveRepository(idx)
|
||||
sendUserToast('Repository settings updated')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to save repository:', error)
|
||||
sendUserToast('Failed to save repository: ' + error.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRevert() {
|
||||
if (!repo) return
|
||||
try {
|
||||
gitSyncContext.revertRepository?.(idx)
|
||||
sendUserToast('Reverted repository settings')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to revert repository:', error)
|
||||
sendUserToast('Failed to revert repository: ' + error.message, true)
|
||||
}
|
||||
}
|
||||
|
||||
function initiateDelete() {
|
||||
confirmingDelete = true
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
try {
|
||||
await gitSyncContext.removeRepository(idx)
|
||||
sendUserToast('Repository connection removed successfully')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to remove repository:', error)
|
||||
sendUserToast('Failed to remove repository: ' + error.message, true)
|
||||
} finally {
|
||||
confirmingDelete = false
|
||||
}
|
||||
}
|
||||
|
||||
function cancelDelete() {
|
||||
confirmingDelete = false
|
||||
}
|
||||
|
||||
function runGitSyncTestJob() {
|
||||
if (gitSyncContext.runTestJob) {
|
||||
gitSyncContext.runTestJob(idx)
|
||||
}
|
||||
}
|
||||
|
||||
function emptyString(str: string | undefined | null): boolean {
|
||||
return !str || str.trim() === ''
|
||||
}
|
||||
|
||||
function handlePullSettings() {
|
||||
gitSyncContext.showPullModal(idx, true) // true for settingsOnly
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if repo}
|
||||
<div class="rounded-lg shadow-sm border p-0 w-full mb-4">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center justify-between min-h-10 px-4 py-1 border-b">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Repository #{idx + 1}</span>
|
||||
{#if repo.legacyImported}
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800 border border-orange-200">
|
||||
Legacy Configuration
|
||||
</span>
|
||||
{/if}
|
||||
<span class="text-xs text-tertiary pt-1 pl-8">
|
||||
{repo.git_repo_resource_path}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if validation.hasChanges && validation.isValid && !repo.isUnsavedConnection}
|
||||
<Button
|
||||
size="xs"
|
||||
onclick={handleSave}
|
||||
startIcon={{ icon: Save }}
|
||||
>
|
||||
{repo.legacyImported ? 'Migrate and save' : 'Save changes'}
|
||||
</Button>
|
||||
{#if gitSyncContext.initialRepositories[idx] && !repo.legacyImported}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
onclick={handleRevert}
|
||||
startIcon={{ icon: RotateCcw }}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<button
|
||||
class="text-secondary hover:text-primary focus:outline-none"
|
||||
onclick={() => (repo.collapsed = !repo.collapsed)}
|
||||
aria-label="Toggle collapse"
|
||||
>
|
||||
{#if repo.collapsed}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{#if !confirmingDelete}
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="rounded-full p-2 bg-surface-secondary duration-200 hover:bg-surface-hover"
|
||||
aria-label="Remove repository"
|
||||
onclick={initiateDelete}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="px-3 py-1 text-xs bg-red-500 text-white rounded duration-200 hover:bg-red-600"
|
||||
onclick={confirmDelete}
|
||||
>
|
||||
Confirm delete
|
||||
</button>
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="px-2 py-1 text-xs bg-surface-secondary rounded duration-200 hover:bg-surface-hover"
|
||||
onclick={cancelDelete}
|
||||
>
|
||||
<XCircle size={12} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if !repo.collapsed}
|
||||
<div class="px-4 py-2">
|
||||
<div class="flex mt-5 mb-1 gap-1">
|
||||
{#key repo}
|
||||
<div class="pt-1 font-semibold">Resource: </div>
|
||||
<ResourcePicker
|
||||
bind:value={repo.git_repo_resource_path}
|
||||
resourceType={'git_repository'}
|
||||
disabled={!repo.isUnsavedConnection}
|
||||
excludedValues={usedRepositoryPaths}
|
||||
/>
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<Button
|
||||
disabled={emptyString(repo.script_path)}
|
||||
color="dark"
|
||||
onclick={runGitSyncTestJob}
|
||||
size="xs">Test connection</Button
|
||||
>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<div class="flex mb-5 text-normal text-2xs gap-1">
|
||||
{#if validation.isDuplicate}
|
||||
<span class="text-red-600">This resource is already used by another repository.</span>
|
||||
{/if}
|
||||
{#if gitSyncTestJob && gitSyncTestJob.status !== undefined}
|
||||
{#if gitSyncTestJob.status === 'running'}
|
||||
<RotateCw size={14} class="animate-spin" />
|
||||
{:else if gitSyncTestJob.status === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Git sync resource checked via Windmill job
|
||||
<a
|
||||
target="_blank"
|
||||
href={`/run/${gitSyncTestJob.jobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{gitSyncTestJob.jobId}
|
||||
</a>WARNING: Only read permissions are verified.
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if repo.legacyImported}
|
||||
<Alert type="warning" title="Legacy git sync settings imported">
|
||||
This repository was initialized from workspace-level legacy Git-Sync settings. Review the filters and press <b>Save</b> to migrate.
|
||||
</Alert>
|
||||
{/if}
|
||||
<div class="flex flex-col mt-5 mb-1 gap-4">
|
||||
{#if repo}
|
||||
{#if repo.script_path != hubPaths.gitSync}
|
||||
<Alert type="warning" title="Script version mismatch">
|
||||
The git sync version for this repository is not latest. Current: <a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{repo.script_path}</a
|
||||
>, latest:
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{hubPaths.gitSync}</a
|
||||
>
|
||||
<div class="flex mt-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
onclick={() => {
|
||||
if (repo) {
|
||||
repo.script_path = hubPaths.gitSync
|
||||
}
|
||||
}}
|
||||
>Update git sync script (require save git settings to be applied)</Button
|
||||
>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if repo.isUnsavedConnection && !emptyString(repo.git_repo_resource_path)}
|
||||
<!-- Use DetectionFlow component -->
|
||||
<div class="mt-4">
|
||||
<DetectionFlow {idx} />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Existing saved connection flow -->
|
||||
<GitSyncFilterSettings
|
||||
bind:git_repo_resource_path={repo.git_repo_resource_path}
|
||||
bind:include_path={repo.settings.include_path}
|
||||
bind:include_type={repo.settings.include_type}
|
||||
bind:exclude_types_override={repo.exclude_types_override}
|
||||
isLegacyRepo={repo.legacyImported}
|
||||
bind:excludes={repo.settings.exclude_path}
|
||||
bind:extraIncludes={repo.settings.extra_include_path}
|
||||
isInitialSetup={false}
|
||||
requiresMigration={repo.legacyImported}
|
||||
>
|
||||
{#snippet actions()}
|
||||
<Button
|
||||
size="md"
|
||||
onclick={handlePullSettings}
|
||||
startIcon={{ icon: Download }}
|
||||
>
|
||||
Pull settings
|
||||
</Button>
|
||||
{/snippet}
|
||||
</GitSyncFilterSettings>
|
||||
{/if}
|
||||
|
||||
{#if !repo.isUnsavedConnection}
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Toggle
|
||||
disabled={emptyString(repo.git_repo_resource_path)}
|
||||
bind:checked={repo.use_individual_branch}
|
||||
options={{
|
||||
right: 'Create one branch per deployed object',
|
||||
rightTooltip:
|
||||
"If set, Windmill will create a unique branch per object being pushed based on its path, prefixed with 'wm_deploy/'."
|
||||
}}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
disabled={emptyString(repo.git_repo_resource_path) ||
|
||||
!repo.use_individual_branch}
|
||||
bind:checked={repo.group_by_folder}
|
||||
options={{
|
||||
right: 'Group deployed objects by folder',
|
||||
rightTooltip:
|
||||
'Instead of creating a branch per object, Windmill will create a branch per folder containing objects being deployed.'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Manual sync section for existing repos -->
|
||||
{#if !emptyString(repo.git_repo_resource_path) && !repo.legacyImported}
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm text-secondary mb-2">Manual workspace content sync</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => gitSyncContext.showPullModal(idx)}
|
||||
startIcon={{ icon: Download }}
|
||||
>
|
||||
Pull from repo
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => gitSyncContext.showPushModal(idx)}
|
||||
startIcon={{ icon: Upload }}
|
||||
>
|
||||
Push to repo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-tertiary pt-1 pl-8">Please select a Git repository resource.</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getGitSyncContext } from './GitSyncContext.svelte'
|
||||
import GitSyncRepositoryCard from './GitSyncRepositoryCard.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
|
||||
const gitSyncContext = getGitSyncContext()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if gitSyncContext.repositories.length === 0}
|
||||
<Alert type="info" title="No repositories configured">
|
||||
Add your first Git repository to start syncing your workspace.
|
||||
</Alert>
|
||||
{:else}
|
||||
{#each gitSyncContext.repositories as repository, idx (repository.git_repo_resource_path || idx)}
|
||||
<GitSyncRepositoryCard {idx} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,93 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Plus, ExternalLink } from 'lucide-svelte'
|
||||
import { Button, Alert } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { setGitSyncContext } from './GitSyncContext.svelte'
|
||||
import GitSyncRepositoryList from './GitSyncRepositoryList.svelte'
|
||||
import GitSyncModalManager from './GitSyncModalManager.svelte'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
|
||||
// Create context reactively based on workspaceStore
|
||||
const gitSyncContext = $derived($workspaceStore ? setGitSyncContext($workspaceStore) : null)
|
||||
|
||||
// Load settings when workspace context changes
|
||||
$effect(() => {
|
||||
if (gitSyncContext) {
|
||||
untrack(async () => {
|
||||
try {
|
||||
await gitSyncContext.loadSettings()
|
||||
} catch (error) {
|
||||
console.error('Failed to load git sync settings:', error)
|
||||
sendUserToast('Failed to load git sync settings', true)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
{#if !gitSyncContext}
|
||||
<div class="flex items-center justify-center p-8">
|
||||
<div class="text-sm text-secondary">Loading workspace...</div>
|
||||
</div>
|
||||
{:else if gitSyncContext.loading}
|
||||
<div class="flex items-center justify-center p-8">
|
||||
<div class="text-sm text-secondary">Loading git sync settings...</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-primary text-lg font-semibold">Git Sync</div>
|
||||
<Description link="https://www.windmill.dev/docs/advanced/git_sync">
|
||||
Connect the Windmill workspace to a Git repository to automatically commit and push
|
||||
scripts, flows, and apps to the repository on each deploy.
|
||||
</Description>
|
||||
</div>
|
||||
<Alert type="info" title="Only new updates trigger git sync">
|
||||
Only new changes matching the filters will trigger a git sync. You still need to initialize
|
||||
the repo to the desired state first.
|
||||
</Alert>
|
||||
</div>
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="mb-2"></div>
|
||||
|
||||
<Alert type="warning" title="Syncing workspace to Git is an EE feature">
|
||||
Automatically saving scripts to a Git repository on each deploy is a Windmill EE feature.
|
||||
</Alert>
|
||||
<div class="mb-2"></div>
|
||||
{/if}
|
||||
{#if $enterpriseLicense && gitSyncContext.repositories != undefined}
|
||||
<div class="flex mt-5 mb-5 gap-8">
|
||||
<Button
|
||||
color="dark"
|
||||
target="_blank"
|
||||
endIcon={{ icon: ExternalLink }}
|
||||
href={`/runs?job_kinds=deploymentcallbacks&workspace=${$workspaceStore}`}
|
||||
>
|
||||
See sync jobs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="pt-2"></div>
|
||||
|
||||
<!-- Repository list -->
|
||||
<GitSyncRepositoryList />
|
||||
|
||||
<!-- Add repository button -->
|
||||
<div class="flex mt-5 mb-5">
|
||||
<Button
|
||||
startIcon={{ icon: Plus }}
|
||||
color="dark"
|
||||
variant="border"
|
||||
onclick={() => gitSyncContext.addRepository()}
|
||||
>
|
||||
Add connection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<GitSyncModalManager />
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,69 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/common/modal/Modal.svelte'
|
||||
import { CheckCircle2, ExternalLink, ArrowRight } from 'lucide-svelte'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
savedWithoutInit?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
savedWithoutInit = false
|
||||
}: Props = $props()
|
||||
|
||||
</script>
|
||||
|
||||
<Modal bind:open title="Git Sync Connection Saved" class="sm:max-w-4xl" cancelText="Close">
|
||||
<div class="flex flex-col gap-6 p-6">
|
||||
<!-- Success header -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
<CheckCircle2 class="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-primary">Git sync connection saved successfully!</h3>
|
||||
<p class="text-sm text-secondary mt-1">Your repository is now configured to receive changes from Windmill.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info box for saved without init -->
|
||||
{#if savedWithoutInit}
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h4 class="font-medium text-blue-900 mb-2">Repository saved without initialization</h4>
|
||||
<p class="text-sm text-blue-800">
|
||||
Only new changes will be pushed to this repository. Existing content in Windmill has not been initialized to the repository.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Optional setup section -->
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||||
<h4 class="font-medium text-amber-900 mb-2 flex items-center gap-2">
|
||||
<ArrowRight class="h-4 w-4" />
|
||||
Optional: Enable automatic deployment from Git to Windmill
|
||||
</h4>
|
||||
<p class="text-sm text-amber-800 mb-3">
|
||||
To automatically deploy changes from your Git repository back to Windmill (when PRs are merged), you can set up GitHub Actions or similar CI/CD workflows.
|
||||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm text-amber-700">This setup enables:</p>
|
||||
<ul class="text-sm text-amber-700 ml-4 list-disc space-y-1">
|
||||
<li>Automatic deployment to Windmill when PRs are merged</li>
|
||||
<li>Full bidirectional sync between Git and Windmill</li>
|
||||
</ul>
|
||||
<div class="mt-3">
|
||||
<a
|
||||
href="https://www.windmill.dev/docs/advanced/deploy_gh_gl#github-actions-setup"
|
||||
target="_blank"
|
||||
class="text-sm text-amber-700 hover:text-amber-900 underline flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink class="h-3 w-3" />
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -1,538 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/common/modal/Modal.svelte'
|
||||
import { Button, Alert, Badge } from '$lib/components/common'
|
||||
import { Loader2, CheckCircle2, XCircle, Terminal, ChevronDown, ChevronUp, Save } from 'lucide-svelte'
|
||||
import GitDiffPreview from '../GitDiffPreview.svelte'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
import type { SyncResponse, SettingsResponse, SettingsObject } from '$lib/git-sync'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
gitRepoResourcePath: string
|
||||
uiState: SettingsObject
|
||||
repoIndex?: number
|
||||
currentGitSyncSettings?: any
|
||||
onFilterUpdate?: (filters: SettingsObject) => void
|
||||
onSettingsSaved?: () => void
|
||||
onSuccess?: () => void
|
||||
settingsOnly?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
gitRepoResourcePath,
|
||||
uiState,
|
||||
repoIndex,
|
||||
currentGitSyncSettings,
|
||||
onFilterUpdate,
|
||||
onSettingsSaved,
|
||||
onSuccess,
|
||||
settingsOnly = false
|
||||
}: Props = $props()
|
||||
|
||||
// Job state
|
||||
let previewJobId = $state<string | null>(null)
|
||||
let previewJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isPreviewLoading = $state(false)
|
||||
let previewError = $state('')
|
||||
|
||||
let applyJobId = $state<string | null>(null)
|
||||
let applyJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isApplying = $state(false)
|
||||
let applyError = $state('')
|
||||
|
||||
// UI state
|
||||
let showCliInstructions = $state(false)
|
||||
let previewResult = $state<SyncResponse | SettingsResponse | null>(null)
|
||||
let settingsApplied = $state(false)
|
||||
|
||||
// Helper functions to reduce type casting repetition
|
||||
const getSettingsChanges = (result: SyncResponse | SettingsResponse | null) => {
|
||||
if (!result) return { hasChanges: false, data: null, diff: null }
|
||||
|
||||
if (settingsOnly) {
|
||||
const settingsResponse = result as SettingsResponse
|
||||
return {
|
||||
hasChanges: settingsResponse.hasChanges ?? false,
|
||||
data: settingsResponse.local,
|
||||
diff: settingsResponse
|
||||
}
|
||||
} else {
|
||||
const syncResponse = result as SyncResponse
|
||||
return {
|
||||
hasChanges: syncResponse.settingsDiffResult?.hasChanges ?? false,
|
||||
data: syncResponse.settingsDiffResult?.local,
|
||||
diff: syncResponse.settingsDiffResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getWorkspaceChanges = (result: SyncResponse | SettingsResponse | null) => {
|
||||
if (!result || settingsOnly) return { hasChanges: false, changes: [] }
|
||||
const syncResponse = result as SyncResponse
|
||||
return {
|
||||
hasChanges: (syncResponse.changes?.length ?? 0) > 0,
|
||||
changes: syncResponse.changes ?? []
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-save settings after successful pull with settings updates
|
||||
async function saveUpdatedSettings() {
|
||||
if (!currentGitSyncSettings || repoIndex === undefined) return
|
||||
|
||||
try {
|
||||
// Save only the specific repository that was updated
|
||||
await currentGitSyncSettings.saveRepository(repoIndex)
|
||||
onSettingsSaved?.()
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
sendUserToast('Failed to save updated settings', true)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state when modal opens/closes
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
isPreviewLoading = false
|
||||
previewError = ''
|
||||
applyJobId = null
|
||||
applyJobStatus = undefined
|
||||
isApplying = false
|
||||
applyError = ''
|
||||
showCliInstructions = false
|
||||
previewResult = null
|
||||
settingsApplied = false
|
||||
} else if (settingsOnly && !previewResult && !isPreviewLoading) {
|
||||
// Auto-trigger settings preview when modal opens in settings-only mode
|
||||
executeJob(true, true)
|
||||
}
|
||||
})
|
||||
|
||||
// Execute job with dry run or actual execution
|
||||
async function executeJob(isDryRun: boolean, settingsOnly: boolean = false) {
|
||||
const isPreview = isDryRun
|
||||
|
||||
if (isPreview) {
|
||||
isPreviewLoading = true
|
||||
previewError = ''
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
} else {
|
||||
isApplying = true
|
||||
applyError = ''
|
||||
applyJobId = null
|
||||
applyJobStatus = undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
const payload = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
dry_run: isDryRun,
|
||||
pull: true,
|
||||
only_wmill_yaml: settingsOnly,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payload,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
if (isPreview) {
|
||||
previewJobId = jobId
|
||||
previewJobStatus = 'running'
|
||||
} else {
|
||||
applyJobId = jobId
|
||||
applyJobStatus = 'running'
|
||||
}
|
||||
|
||||
let jobSuccess = false
|
||||
let result: any = {}
|
||||
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
if (jobSuccess) {
|
||||
const jobResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
result = jobResult
|
||||
}
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: `${isPreview ? 'Preview' : 'Apply'} job timed out after 60s` }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 60000
|
||||
})
|
||||
|
||||
if (isPreview) {
|
||||
previewJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
previewResult = result
|
||||
} else {
|
||||
previewError = 'Preview failed'
|
||||
}
|
||||
} else {
|
||||
applyJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
const settingsData = result?.local
|
||||
const hasSettingsChanges = settingsData && onFilterUpdate
|
||||
if (hasSettingsChanges) {
|
||||
onFilterUpdate(settingsData)
|
||||
await saveUpdatedSettings()
|
||||
}
|
||||
onSuccess?.()
|
||||
} else {
|
||||
applyError = 'Pull failed'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e?.message || 'Operation failed'
|
||||
if (isPreview) {
|
||||
previewJobStatus = 'failure'
|
||||
previewError = errorMsg
|
||||
} else {
|
||||
applyJobStatus = 'failure'
|
||||
applyError = errorMsg
|
||||
}
|
||||
} finally {
|
||||
if (isPreview) {
|
||||
isPreviewLoading = false
|
||||
} else {
|
||||
isApplying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Apply settings only (no job needed - we have the data from preview)
|
||||
async function applySettingsOnly() {
|
||||
isApplying = true
|
||||
|
||||
try {
|
||||
const settingsChanges = getSettingsChanges(previewResult)
|
||||
|
||||
if (!settingsChanges.hasChanges) {
|
||||
sendUserToast('No settings changes to apply', true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!settingsChanges.data) {
|
||||
sendUserToast('Settings data not available', true)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the UI state with the new settings
|
||||
if (onFilterUpdate) {
|
||||
onFilterUpdate(settingsChanges.data)
|
||||
}
|
||||
|
||||
// Save the updated settings
|
||||
await saveUpdatedSettings()
|
||||
|
||||
if (settingsOnly) {
|
||||
// Settings-only mode - we're done, onSuccess will handle the toast
|
||||
onSuccess?.()
|
||||
} else {
|
||||
// Two-step flow - transition to step 2
|
||||
settingsApplied = true
|
||||
sendUserToast('Settings applied successfully. You can now review workspace changes.')
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Failed to apply settings:', error)
|
||||
sendUserToast('Failed to apply settings: ' + error.message, true)
|
||||
} finally {
|
||||
isApplying = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<Modal bind:open title={settingsOnly ? "Pull Settings from Git Repository" : "Pull Workspace from Git Repository"} class="sm:max-w-4xl" cancelText={settingsOnly && !getSettingsChanges(previewResult).hasChanges ? "Close" : "Cancel"}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-secondary">
|
||||
{#if settingsOnly}
|
||||
Pull and apply settings changes from the Git repository to your workspace. This will update your sync filter settings only.
|
||||
{:else}
|
||||
Pull and apply changes from the Git repository to your workspace. If settings changes are detected, you can choose to pull just the settings or everything.
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<!-- Warning about overwrites - only show for full pulls, not settings-only -->
|
||||
{#if !settingsOnly}
|
||||
<Alert type="warning" title="This will overwrite local changes">
|
||||
Pulling from the repository will overwrite any local changes to files that exist in the repository.
|
||||
Make sure to preview the changes before applying.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- Preview section -->
|
||||
{#if !previewResult}
|
||||
<div class="flex justify-start pt-4">
|
||||
<Button
|
||||
size="md"
|
||||
color="dark"
|
||||
onclick={() => executeJob(true, settingsOnly)}
|
||||
disabled={isPreviewLoading}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : undefined,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isPreviewLoading ? 'Previewing...' : 'Preview changes'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Job status for preview -->
|
||||
{#if previewJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if previewJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if previewJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if previewJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Preview job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${previewJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{previewJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Preview error -->
|
||||
{#if previewError}
|
||||
<Alert type="error" title="Preview failed">
|
||||
{previewError}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- Preview results -->
|
||||
{#if previewResult && !previewError}
|
||||
{@const settingsChanges = getSettingsChanges(previewResult)}
|
||||
{@const workspaceChanges = getWorkspaceChanges(previewResult)}
|
||||
<div class="space-y-4">
|
||||
<!-- Settings changes (always show first if present) -->
|
||||
{#if settingsChanges.hasChanges && !settingsApplied}
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-primary mb-2">
|
||||
Filter Settings from Repository
|
||||
<Badge color="blue" size="xs" class="ml-2">wmill.yaml</Badge>
|
||||
</h4>
|
||||
|
||||
<div class="bg-surface-secondary rounded-lg p-4 space-y-1">
|
||||
{#if settingsChanges.diff?.diff}
|
||||
{#each Object.entries(settingsChanges.diff.diff) as [field, change]}
|
||||
{@const fieldName = field.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
||||
{@const typedChange = change as {from: any, to: any}}
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<span class="text-tertiary min-w-0 flex-shrink-0">{fieldName}:</span>
|
||||
{#if Array.isArray(typedChange.from) && Array.isArray(typedChange.to)}
|
||||
<span class="text-red-600">{typedChange.from.length === 0 ? 'None' : typedChange.from.join(', ')}</span>
|
||||
<span class="text-tertiary">→</span>
|
||||
<span class="text-green-600">{typedChange.to.length === 0 ? 'None' : typedChange.to.join(', ')}</span>
|
||||
{:else}
|
||||
<span class="text-red-600">{typedChange.from}</span>
|
||||
<span class="text-tertiary">→</span>
|
||||
<span class="text-green-600">{typedChange.to}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-xs text-tertiary">
|
||||
Settings changes detected but no detailed diff available.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- No settings changes detected (settings-only mode) -->
|
||||
{#if settingsOnly && !settingsChanges.hasChanges}
|
||||
<div class="bg-surface-secondary rounded-lg p-3">
|
||||
<div class="text-sm text-tertiary">
|
||||
No settings changes detected. Your local sync filter settings are already up to date with the repository.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Workspace changes (show when no settings changes and there are workspace changes) -->
|
||||
{#if !settingsOnly && !settingsChanges.hasChanges && workspaceChanges.hasChanges}
|
||||
<div class={settingsChanges.hasChanges && settingsApplied ? 'border-t pt-4' : ''}>
|
||||
<h4 class="text-sm font-semibold text-primary mb-2">Workspace changes to pull</h4>
|
||||
|
||||
{#if workspaceChanges.hasChanges}
|
||||
<GitDiffPreview previewResult={previewResult as SyncResponse} />
|
||||
{:else}
|
||||
<div class="bg-surface-secondary rounded-lg p-3">
|
||||
<div class="text-sm text-tertiary">No changes to pull from the repository.</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Apply section (shown after successful preview) -->
|
||||
{#if previewResult && !previewError}
|
||||
{@const settingsChanges = getSettingsChanges(previewResult)}
|
||||
{@const workspaceChanges = getWorkspaceChanges(previewResult)}
|
||||
|
||||
{#if settingsChanges.hasChanges || workspaceChanges.hasChanges}
|
||||
<div class="border-t pt-4 mt-4">
|
||||
{#if settingsChanges.hasChanges && workspaceChanges.hasChanges && !settingsApplied}
|
||||
<!-- Step 1: Settings changes first when both are present -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-sm font-medium text-primary">Step 1 of 2: Apply settings changes</div>
|
||||
<div class="text-xs text-tertiary">Settings changes detected. Apply these first to ensure workspace content is pulled with the correct configuration.</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="md"
|
||||
onclick={applySettingsOnly}
|
||||
disabled={isApplying}
|
||||
startIcon={{
|
||||
icon: isApplying ? Loader2 : Save,
|
||||
classes: isApplying ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isApplying ? 'Applying...' : 'Apply settings'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if settingsChanges.hasChanges && !workspaceChanges.hasChanges && !settingsApplied}
|
||||
<!-- Only settings changes -->
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="md"
|
||||
onclick={applySettingsOnly}
|
||||
disabled={isApplying}
|
||||
startIcon={{
|
||||
icon: isApplying ? Loader2 : Save,
|
||||
classes: isApplying ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isApplying ? 'Applying...' : 'Apply settings'}
|
||||
</Button>
|
||||
</div>
|
||||
{:else if workspaceChanges.hasChanges && (!settingsChanges.hasChanges || settingsApplied)}
|
||||
<!-- Step 2: Workspace changes (either no settings changes, or settings already applied) -->
|
||||
<div class="flex flex-col gap-3">
|
||||
{#if settingsApplied}
|
||||
<div class="text-sm font-medium text-primary">Step 2 of 2: Pull Workspace Changes</div>
|
||||
<div class="text-xs text-green-600">✓ Settings applied successfully. Now you can pull the workspace changes.</div>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="md"
|
||||
onclick={() => executeJob(false, false)}
|
||||
disabled={isApplying}
|
||||
startIcon={{
|
||||
icon: isApplying ? Loader2 : Save,
|
||||
classes: isApplying ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isApplying ? 'Pulling...' : 'Pull from repository'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- No changes to pull -->
|
||||
<div class="bg-surface-secondary rounded-lg p-3">
|
||||
<div class="text-sm text-tertiary">No changes to pull from the repository.</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Job status for apply -->
|
||||
{#if applyJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if applyJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if applyJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if applyJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Pull job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${applyJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{applyJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Apply error -->
|
||||
{#if applyError}
|
||||
<Alert type="error" title="Pull failed">
|
||||
{applyError}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- CLI Instructions (collapsible) -->
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<button
|
||||
class="flex items-center gap-2 text-sm text-secondary hover:text-primary transition-colors"
|
||||
onclick={() => showCliInstructions = !showCliInstructions}
|
||||
>
|
||||
<Terminal size={16} />
|
||||
<span>CLI Instructions</span>
|
||||
{#if showCliInstructions}
|
||||
<ChevronUp size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if showCliInstructions}
|
||||
<div class="mt-3 bg-surface-secondary rounded-lg p-3">
|
||||
<pre class="text-xs bg-surface p-3 rounded overflow-x-auto whitespace-pre-wrap break-all">
|
||||
# Setup (only needed if local folder not initialized yet)
|
||||
npm install -g windmill-cli
|
||||
wmill workspace add {$workspaceStore} {$workspaceStore} {window.location.origin}
|
||||
wmill init --workspace {$workspaceStore} --repository {gitRepoResourcePath}
|
||||
|
||||
{#if !settingsOnly}
|
||||
# Push from git repository to workspace
|
||||
wmill sync push --workspace {$workspaceStore} --repository {gitRepoResourcePath}
|
||||
|
||||
{/if}
|
||||
# Push settings only from git repository
|
||||
wmill gitsync-settings push --workspace {$workspaceStore} --repository {gitRepoResourcePath}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -1,344 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/common/modal/Modal.svelte'
|
||||
import { Button, Alert } from '$lib/components/common'
|
||||
import { Loader2, CheckCircle2, XCircle, Terminal, ChevronDown, ChevronUp } from 'lucide-svelte'
|
||||
import GitDiffPreview from '../GitDiffPreview.svelte'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
import type { SyncResponse, SettingsObject } from '$lib/git-sync'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
gitRepoResourcePath: string
|
||||
uiState: SettingsObject
|
||||
onSuccess?: () => void
|
||||
isNewConnection?: boolean
|
||||
onSaveWithoutInit?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
gitRepoResourcePath,
|
||||
uiState,
|
||||
onSuccess,
|
||||
isNewConnection = false,
|
||||
onSaveWithoutInit
|
||||
}: Props = $props()
|
||||
|
||||
// Job state
|
||||
let previewJobId = $state<string | null>(null)
|
||||
let previewJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isPreviewLoading = $state(false)
|
||||
let previewError = $state('')
|
||||
|
||||
let applyJobId = $state<string | null>(null)
|
||||
let applyJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isApplying = $state(false)
|
||||
let applyError = $state('')
|
||||
|
||||
// UI state
|
||||
let showCliInstructions = $state(false)
|
||||
let previewResult = $state<SyncResponse | null>(null)
|
||||
|
||||
// Note: Escape key is handled by the Modal component itself
|
||||
|
||||
// Reset state when modal opens/closes
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
isPreviewLoading = false
|
||||
previewError = ''
|
||||
applyJobId = null
|
||||
applyJobStatus = undefined
|
||||
isApplying = false
|
||||
applyError = ''
|
||||
showCliInstructions = false
|
||||
previewResult = null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Execute job with dry run or actual execution
|
||||
async function executeJob(isDryRun: boolean) {
|
||||
const isPreview = isDryRun
|
||||
|
||||
if (isPreview) {
|
||||
isPreviewLoading = true
|
||||
previewError = ''
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
} else {
|
||||
isApplying = true
|
||||
applyError = ''
|
||||
applyJobId = null
|
||||
applyJobStatus = undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
const payload = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: gitRepoResourcePath,
|
||||
dry_run: isDryRun,
|
||||
pull: false,
|
||||
settings_json: JSON.stringify(uiState)
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payload,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
if (isPreview) {
|
||||
previewJobId = jobId
|
||||
previewJobStatus = 'running'
|
||||
} else {
|
||||
applyJobId = jobId
|
||||
applyJobStatus = 'running'
|
||||
}
|
||||
|
||||
let jobSuccess = false
|
||||
let result: any = {}
|
||||
|
||||
await tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
if (jobSuccess) {
|
||||
const jobResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
result = jobResult
|
||||
}
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: `${isPreview ? 'Preview' : 'Apply'} job timed out after 60s` }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 60000
|
||||
})
|
||||
|
||||
if (isPreview) {
|
||||
previewJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
previewResult = result
|
||||
} else {
|
||||
previewError = 'Preview failed'
|
||||
}
|
||||
} else {
|
||||
applyJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
onSuccess?.()
|
||||
} else {
|
||||
applyError = 'Push failed'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e?.message || 'Operation failed'
|
||||
if (isPreview) {
|
||||
previewJobStatus = 'failure'
|
||||
previewError = errorMsg
|
||||
} else {
|
||||
applyJobStatus = 'failure'
|
||||
applyError = errorMsg
|
||||
}
|
||||
} finally {
|
||||
if (isPreview) {
|
||||
isPreviewLoading = false
|
||||
} else {
|
||||
isApplying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<Modal bind:open title="Push Workspace to Git Repository" class="sm:max-w-4xl">
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Description -->
|
||||
<p class="text-sm text-secondary">Push your current workspace content to the connected Git repository based on the configured filters.</p>
|
||||
<p class="text-sm text-tertiary">Note: This will not update git sync settings in wmill.yaml. Settings can only be pulled from the repository as it is the source of truth.</p>
|
||||
|
||||
<!-- Settings display for new connections -->
|
||||
{#if isNewConnection}
|
||||
<div class="bg-surface-secondary border border-border rounded-lg p-3">
|
||||
<h4 class="text-sm font-medium text-primary mb-2">Settings that will be pushed to repository</h4>
|
||||
<div class="text-xs text-secondary space-y-1">
|
||||
<div><strong>Include paths:</strong> {uiState.include_path?.join(', ') || 'None'}</div>
|
||||
<div><strong>Exclude paths:</strong> {uiState.exclude_path?.join(', ') || 'None'}</div>
|
||||
{#if uiState.extra_include_path?.length > 0}
|
||||
<div><strong>Extra include paths:</strong> {uiState.extra_include_path.join(', ')}</div>
|
||||
{/if}
|
||||
<div><strong>Include types:</strong> {uiState.include_type?.join(', ') || 'None'}</div>
|
||||
</div>
|
||||
<p class="text-xs text-tertiary mt-2">To modify these settings, cancel and configure them in the workspace settings.</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Preview section -->
|
||||
{#if !previewResult}
|
||||
<div class="flex justify-start pt-4">
|
||||
<Button
|
||||
size="md"
|
||||
color="dark"
|
||||
onclick={() => executeJob(true)}
|
||||
disabled={isPreviewLoading}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : undefined,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isPreviewLoading ? 'Previewing...' : 'Preview changes'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Job status for preview -->
|
||||
{#if previewJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if previewJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if previewJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if previewJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Preview job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${previewJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{previewJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Preview error -->
|
||||
{#if previewError}
|
||||
<Alert type="error" title="Preview failed">
|
||||
{previewError}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- Preview results -->
|
||||
{#if previewResult && !previewError}
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-sm font-semibold text-primary">Changes to Push</h4>
|
||||
|
||||
{#if previewResult.changes?.length > 0}
|
||||
<GitDiffPreview previewResult={previewResult} />
|
||||
{:else}
|
||||
<div class="bg-surface-secondary rounded-lg p-3">
|
||||
<div class="text-sm text-tertiary">No changes to push to the repository.</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Apply section (shown after successful preview) -->
|
||||
{#if previewResult && !previewError}
|
||||
{@const hasChanges = previewResult.changes?.length > 0}
|
||||
{#if hasChanges}
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<div class="flex justify-start gap-2">
|
||||
<Button
|
||||
size="xs"
|
||||
onclick={() => executeJob(false)}
|
||||
disabled={isApplying}
|
||||
startIcon={{
|
||||
icon: isApplying ? Loader2 : undefined,
|
||||
classes: isApplying ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isApplying ?
|
||||
(isNewConnection ? 'Initializing...' : 'Pushing...') :
|
||||
(isNewConnection ? 'Initialize repo and save connection' : 'Push to repository')
|
||||
}
|
||||
</Button>
|
||||
{#if isNewConnection && onSaveWithoutInit}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
onclick={onSaveWithoutInit}
|
||||
disabled={isApplying}
|
||||
>
|
||||
Save without initializing repo
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Job status for apply -->
|
||||
{#if applyJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary">
|
||||
{#if applyJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if applyJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if applyJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Push job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${applyJobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{applyJobId}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Apply error -->
|
||||
{#if applyError}
|
||||
<Alert type="error" title="Push failed">
|
||||
{applyError}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<!-- CLI Instructions (collapsible) -->
|
||||
<div class="border-t pt-4 mt-4">
|
||||
<button
|
||||
class="flex items-center gap-2 text-sm text-secondary hover:text-primary transition-colors"
|
||||
onclick={() => showCliInstructions = !showCliInstructions}
|
||||
>
|
||||
<Terminal size={16} />
|
||||
<span>CLI Instructions</span>
|
||||
{#if showCliInstructions}
|
||||
<ChevronUp size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if showCliInstructions}
|
||||
<div class="mt-3 bg-surface-secondary rounded-lg p-3">
|
||||
<pre class="text-xs bg-surface p-3 rounded overflow-x-auto whitespace-pre-wrap break-all">
|
||||
# Setup (only needed if local folder not initialized yet)
|
||||
npm install -g windmill-cli
|
||||
wmill workspace add {$workspaceStore} {$workspaceStore} {window.location.origin}
|
||||
wmill init --workspace {$workspaceStore} --repository {gitRepoResourcePath}
|
||||
|
||||
# Pull workspace content to git repository
|
||||
wmill sync pull --workspace {$workspaceStore} --repository {gitRepoResourcePath}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -153,7 +153,7 @@
|
||||
{@render children?.()}
|
||||
{#if previewJob != undefined && 'result' in previewJob}
|
||||
<div class="relative w-full h-full p-2">
|
||||
<div class="relative h-full">
|
||||
<div class="relative">
|
||||
<DisplayResult
|
||||
bind:forceJson
|
||||
workspaceId={previewJob?.workspace_id}
|
||||
@@ -161,7 +161,6 @@
|
||||
result={previewJob.result}
|
||||
customUi={customUi?.displayResult}
|
||||
language={lang}
|
||||
fixTableSizingToParent
|
||||
>
|
||||
{#snippet copilot_fix()}
|
||||
{#if lang && editor && diffEditor && args && previewJob && !previewJob.success && getStringError(previewJob.result)}
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Filter, Terminal, ChevronDown, ChevronUp } from 'lucide-svelte'
|
||||
import { Filter, Save, Eye, Loader2, CheckCircle2, XCircle, Check } from 'lucide-svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import yaml from 'js-yaml'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
import { JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import FilterList from './FilterList.svelte'
|
||||
import { Tabs, Tab } from '$lib/components/common'
|
||||
import type { GitSyncObjectType } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
type ObjectType =
|
||||
| 'script'
|
||||
| 'flow'
|
||||
| 'app'
|
||||
| 'folder'
|
||||
| 'resource'
|
||||
| 'variable'
|
||||
| 'secret'
|
||||
| 'resourcetype'
|
||||
| 'schedule'
|
||||
| 'user'
|
||||
| 'group'
|
||||
| 'trigger'
|
||||
| 'settings'
|
||||
| 'key'
|
||||
|
||||
type GitSyncTypeMap = {
|
||||
scripts: boolean
|
||||
@@ -24,25 +44,53 @@
|
||||
key: boolean
|
||||
}
|
||||
|
||||
type PreviewResult = {
|
||||
diff?: { [key: string]: { from: any; to: any } }
|
||||
hasChanges?: boolean
|
||||
isInitialSetup?: boolean
|
||||
message?: string
|
||||
local?: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: ObjectType[]
|
||||
}
|
||||
backend?: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: ObjectType[]
|
||||
}
|
||||
}
|
||||
|
||||
let {
|
||||
git_repo_resource_path = $bindable(''),
|
||||
include_path = $bindable(['f/**']),
|
||||
include_type = $bindable(['script', 'flow', 'app', 'folder'] as GitSyncObjectType[]),
|
||||
exclude_types_override = $bindable([] as GitSyncObjectType[]),
|
||||
include_type = $bindable(['script', 'flow', 'app', 'folder'] as ObjectType[]),
|
||||
exclude_types_override = $bindable([] as ObjectType[]),
|
||||
isLegacyRepo = false,
|
||||
yamlText = $bindable(''),
|
||||
onSettingsChange = (settings: { yaml: string }) => {},
|
||||
excludes = $bindable([] as string[]),
|
||||
extraIncludes = $bindable([] as string[]),
|
||||
isInitialSetup = false,
|
||||
requiresMigration = false,
|
||||
actions = undefined
|
||||
extraIncludes = $bindable([] as string[])
|
||||
} = $props()
|
||||
|
||||
// Component state
|
||||
let collapsed = $state(false)
|
||||
let showCliInstructions = $state(false)
|
||||
let editAsYaml = $state(false)
|
||||
let yamlError = $state('')
|
||||
let isPullMode = $state(false)
|
||||
|
||||
// Determine if component should be editable or read-only
|
||||
const isEditable = $derived(isInitialSetup || requiresMigration)
|
||||
// Preview/Push state
|
||||
let previewResult = $state<PreviewResult | null>(null)
|
||||
let previewJobId = $state<string | null>(null)
|
||||
let previewJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let pushJobId = $state<string | null>(null)
|
||||
let pushJobStatus = $state<'running' | 'success' | 'failure' | undefined>(undefined)
|
||||
let isPreviewLoading = $state(false)
|
||||
let isPushing = $state(false)
|
||||
let previewError = $state('')
|
||||
let previewSettingsSnapshot = $state<string | null>(null)
|
||||
|
||||
// Compute effective include types (include_type minus exclude_types_override for legacy repos only)
|
||||
const effectiveIncludeTypes = $derived(
|
||||
@@ -74,7 +122,7 @@
|
||||
|
||||
function updateIncludeType(key: keyof GitSyncTypeMap, value: boolean) {
|
||||
const newTypes = new Set(include_type)
|
||||
const typeMap: Record<keyof GitSyncTypeMap, GitSyncObjectType> = {
|
||||
const typeMap: Record<keyof GitSyncTypeMap, ObjectType> = {
|
||||
scripts: 'script',
|
||||
flows: 'flow',
|
||||
apps: 'app',
|
||||
@@ -107,7 +155,369 @@
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
// Simple JSON-based UI state helper
|
||||
function getUIState() {
|
||||
return {
|
||||
include_path,
|
||||
exclude_path: excludes,
|
||||
extra_include_path: extraIncludes,
|
||||
include_type
|
||||
}
|
||||
}
|
||||
|
||||
// Apply settings from backend format (used by both local git repo and backend settings)
|
||||
function fromBackendFormat(settings: {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: ObjectType[]
|
||||
}) {
|
||||
include_path = settings.include_path || []
|
||||
excludes = settings.exclude_path || []
|
||||
extraIncludes = settings.extra_include_path || []
|
||||
include_type = settings.include_type || []
|
||||
}
|
||||
|
||||
// Simplified YAML parsing for manual editing
|
||||
function fromYaml(yamlStr: string) {
|
||||
yamlError = ''
|
||||
try {
|
||||
const parsed = yaml.load(yamlStr)
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('Invalid YAML structure')
|
||||
}
|
||||
|
||||
const obj: any = parsed
|
||||
yamlText = yamlStr
|
||||
|
||||
// Extract includes - reset to default if not present
|
||||
if (obj.includes && Array.isArray(obj.includes)) {
|
||||
include_path = obj.includes.map((p: any) => {
|
||||
if (typeof p !== 'string') {
|
||||
throw new Error('includes must contain only strings')
|
||||
}
|
||||
// Handle quoted strings
|
||||
if (/^['"].*['"]$/.test(p)) {
|
||||
return p.slice(1, -1).replace(/''/g, "'")
|
||||
}
|
||||
return p
|
||||
})
|
||||
} else {
|
||||
// Reset to default if includes is not present
|
||||
include_path = ['f/**']
|
||||
}
|
||||
|
||||
// Build the type set based on the YAML flags
|
||||
const newTypes = new Set<ObjectType>()
|
||||
|
||||
// Always include core types (these are fundamental and not controlled by flags)
|
||||
newTypes.add('script')
|
||||
newTypes.add('flow')
|
||||
newTypes.add('app')
|
||||
newTypes.add('folder')
|
||||
|
||||
// Handle skip flags (if skipX is false or undefined, include the type)
|
||||
if (obj.skipResourceTypes !== true) newTypes.add('resourcetype')
|
||||
if (obj.skipResources !== true) newTypes.add('resource')
|
||||
if (obj.skipVariables !== true) newTypes.add('variable')
|
||||
if (obj.skipSecrets !== true) newTypes.add('secret')
|
||||
|
||||
// Handle include flags (if includeX is true, include the type)
|
||||
if (obj.includeSchedules === true) newTypes.add('schedule')
|
||||
if (obj.includeTriggers === true) newTypes.add('trigger')
|
||||
if (obj.includeUsers === true) newTypes.add('user')
|
||||
if (obj.includeGroups === true) newTypes.add('group')
|
||||
if (obj.includeSettings === true) newTypes.add('settings')
|
||||
if (obj.includeKey === true) newTypes.add('key')
|
||||
|
||||
// Apply business rule: secrets can only be included if variables are included
|
||||
// This matches the UI behavior where turning off variables also turns off secrets
|
||||
if (!newTypes.has('variable')) {
|
||||
newTypes.delete('secret')
|
||||
}
|
||||
|
||||
include_type = Array.from(newTypes)
|
||||
} catch (e) {
|
||||
yamlError = e.message || 'Invalid YAML'
|
||||
console.error('Error parsing YAML:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple YAML generation for manual editing mode
|
||||
function generateYamlFromUI() {
|
||||
try {
|
||||
const validIncludePath = include_path
|
||||
const validExcludePath = excludes
|
||||
const validExtraInclude = extraIncludes
|
||||
|
||||
// Basic YAML structure - let the CLI handle the proper normalization
|
||||
let config: any = {
|
||||
includes: validIncludePath,
|
||||
excludes: validExcludePath,
|
||||
extraIncludes: validExtraInclude,
|
||||
codebases: []
|
||||
}
|
||||
|
||||
// Let the CLI handle the optimization of skip/include flags
|
||||
// Just convert the UI state directly
|
||||
if (!include_type.includes('variable')) config.skipVariables = true
|
||||
if (!include_type.includes('resource')) config.skipResources = true
|
||||
if (!include_type.includes('secret')) config.skipSecrets = true
|
||||
if (!include_type.includes('resourcetype')) config.skipResourceTypes = true
|
||||
|
||||
if (include_type.includes('schedule')) config.includeSchedules = true
|
||||
if (include_type.includes('trigger')) config.includeTriggers = true
|
||||
if (include_type.includes('user')) config.includeUsers = true
|
||||
if (include_type.includes('group')) config.includeGroups = true
|
||||
if (include_type.includes('settings')) config.includeSettings = true
|
||||
if (include_type.includes('key')) config.includeKey = true
|
||||
|
||||
return yaml.dump(config, {
|
||||
indent: 2,
|
||||
lineWidth: -1,
|
||||
quotingType: '"',
|
||||
forceQuotes: false,
|
||||
noRefs: true
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('Failed to generate YAML:', e)
|
||||
yamlError = e.message || 'Failed to generate YAML'
|
||||
return `includes:
|
||||
- f/**
|
||||
excludes: []
|
||||
extraIncludes: []
|
||||
codebases: []`
|
||||
}
|
||||
}
|
||||
|
||||
function switchToYaml() {
|
||||
yamlText = generateYamlFromUI()
|
||||
yamlError = ''
|
||||
editAsYaml = true
|
||||
}
|
||||
|
||||
function switchToUI() {
|
||||
fromYaml(yamlText)
|
||||
if (!yamlError) {
|
||||
editAsYaml = false
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified preview function - always uses JSON approach
|
||||
async function previewFiltersToGitRepo() {
|
||||
isPreviewLoading = true
|
||||
previewError = ''
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
|
||||
// Take a snapshot of current settings
|
||||
previewSettingsSnapshot = JSON.stringify({
|
||||
include_path,
|
||||
excludes,
|
||||
extraIncludes,
|
||||
include_type
|
||||
})
|
||||
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
// Always pass UI state as JSON - the backend now handles this uniformly
|
||||
const payloadObj = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: git_repo_resource_path,
|
||||
only_wmill_yaml: true,
|
||||
dry_run: true,
|
||||
pull: isPullMode,
|
||||
settings_json: JSON.stringify(getUIState())
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payloadObj,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
previewJobId = jobId
|
||||
previewJobStatus = 'running'
|
||||
let jobSuccess = false
|
||||
let result: PreviewResult = {}
|
||||
|
||||
await (
|
||||
await import('$lib/utils')
|
||||
).tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
if (jobSuccess) {
|
||||
const jobResult = await JobService.getCompletedJobResult({ workspace, id: jobId })
|
||||
result = jobResult as PreviewResult
|
||||
}
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: 'Preview job timed out after 5s' }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
previewJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
previewResult = result
|
||||
} else {
|
||||
previewError = 'Preview failed'
|
||||
}
|
||||
} catch (e) {
|
||||
previewJobStatus = 'failure'
|
||||
previewError = e?.message || 'Preview failed'
|
||||
previewResult = null
|
||||
} finally {
|
||||
isPreviewLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified push function - always uses JSON approach
|
||||
async function pushFiltersToGitRepo() {
|
||||
if (isPullMode) {
|
||||
// In pull mode, apply the local settings (from git repo) to UI
|
||||
if (previewResult?.local) {
|
||||
try {
|
||||
fromBackendFormat(previewResult.local)
|
||||
yamlText = generateYamlFromUI()
|
||||
onSettingsChange({ yaml: yamlText })
|
||||
sendUserToast('Changes applied - remember to save repository settings to persist changes')
|
||||
|
||||
// Clear the preview state after applying settings
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
previewError = ''
|
||||
} catch (e) {
|
||||
previewError = 'Failed to apply pulled settings: ' + e.message
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Push mode - send current UI state as JSON
|
||||
isPushing = true
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
|
||||
try {
|
||||
const workspace = $workspaceStore
|
||||
if (!workspace) return
|
||||
|
||||
const payloadObj = {
|
||||
workspace_id: workspace,
|
||||
repo_url_resource_path: git_repo_resource_path,
|
||||
dry_run: false,
|
||||
pull: isPullMode,
|
||||
only_wmill_yaml: true,
|
||||
settings_json: JSON.stringify(getUIState())
|
||||
}
|
||||
|
||||
const jobId = await JobService.runScriptByPath({
|
||||
workspace,
|
||||
path: hubPaths.gitInitRepo,
|
||||
requestBody: payloadObj,
|
||||
skipPreprocessor: true
|
||||
})
|
||||
|
||||
pushJobId = jobId
|
||||
pushJobStatus = 'running'
|
||||
let jobSuccess = false
|
||||
|
||||
await (
|
||||
await import('$lib/utils')
|
||||
).tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({ workspace, id: jobId })
|
||||
jobSuccess = !!testResult.success
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: 'Push job timed out after 5s' }
|
||||
})
|
||||
} catch (err) {}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
pushJobStatus = jobSuccess ? 'success' : 'failure'
|
||||
if (jobSuccess) {
|
||||
// Reset preview state after successful push
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
previewError = ''
|
||||
}
|
||||
} catch (e) {
|
||||
pushJobStatus = 'failure'
|
||||
} finally {
|
||||
isPushing = false
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified export function for backward compatibility
|
||||
export function toYaml() {
|
||||
return generateYamlFromUI()
|
||||
}
|
||||
|
||||
export function setSettings(settings: { yaml: string }) {
|
||||
yamlText = settings.yaml
|
||||
fromYaml(settings.yaml)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// Reset preview state when switching modes
|
||||
if (isPullMode !== undefined) {
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
pushJobId = null
|
||||
pushJobStatus = undefined
|
||||
isPreviewLoading = false
|
||||
isPushing = false
|
||||
previewError = ''
|
||||
}
|
||||
})
|
||||
|
||||
// Reset preview state when settings change (making preview stale)
|
||||
$effect(() => {
|
||||
// Track all the settings that affect the preview
|
||||
const currentSettings = JSON.stringify({
|
||||
include_path,
|
||||
excludes,
|
||||
extraIncludes,
|
||||
include_type
|
||||
})
|
||||
|
||||
// If we have an existing preview result and settings have changed from snapshot, clear it
|
||||
if (
|
||||
previewResult !== null &&
|
||||
previewSettingsSnapshot !== null &&
|
||||
currentSettings !== previewSettingsSnapshot
|
||||
) {
|
||||
previewResult = null
|
||||
previewJobId = null
|
||||
previewJobStatus = undefined
|
||||
previewError = ''
|
||||
previewSettingsSnapshot = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg shadow-sm border p-0 w-full">
|
||||
@@ -116,19 +526,18 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<Filter size={18} class="text-primary" />
|
||||
<span class="font-semibold text-sm">Git Sync filter settings</span>
|
||||
{#if isLegacyRepo}
|
||||
<Tooltip>
|
||||
This repository uses legacy configuration format and inherits settings from workspace-level defaults. Excluded types are filtered out from inherited types. Save to migrate to the new format.
|
||||
</Tooltip>
|
||||
{:else if !isEditable}
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/advanced/cli/sync#wmillyaml">
|
||||
These settings are controlled by the wmill.yaml file in your git repository. Click "Pull from repo" to check for settings drift and pull settings from repo.
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if !collapsed}
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded border border-gray-300 bg-surface-primary hover:bg-surface-secondary"
|
||||
onclick={editAsYaml ? switchToUI : switchToYaml}
|
||||
>
|
||||
{editAsYaml ? 'Edit in UI' : 'Edit as YAML'}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="text-secondary hover:text-primary focus:outline-none"
|
||||
class="text-gray-500 hover:text-primary focus:outline-none"
|
||||
onclick={() => (collapsed = !collapsed)}
|
||||
aria-label="Toggle collapse"
|
||||
>
|
||||
@@ -167,8 +576,18 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if !collapsed}
|
||||
{#if isEditable}
|
||||
<!-- Editable mode -->
|
||||
{#if editAsYaml}
|
||||
<div class="px-4 py-4">
|
||||
<textarea
|
||||
class="w-full h-64 font-mono text-xs border rounded p-2 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
spellcheck="false"
|
||||
bind:value={yamlText}
|
||||
></textarea>
|
||||
{#if yamlError}
|
||||
<div class="text-xs text-red-600 mt-2">{yamlError}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-4 py-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 md:gap-32">
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -334,105 +753,124 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 p-2 border-t">
|
||||
<div class="text-xs text-tertiary mb-2">
|
||||
{isInitialSetup ? 'Configure initial sync settings' : 'Review migration settings'}
|
||||
<div class="mt-6 flex flex-col gap-2 p-2">
|
||||
<div class="flex flex-col gap-2 mb-2">
|
||||
<Toggle
|
||||
size="sm"
|
||||
bind:checked={isPullMode}
|
||||
options={{
|
||||
left: 'Push',
|
||||
right: 'Pull'
|
||||
}}
|
||||
/>
|
||||
<span class="text-xs text-tertiary">
|
||||
{isPullMode ? 'Pull settings from Git repository' : 'Push settings to Git repository'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Read-only view -->
|
||||
<div class="px-4 py-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 md:gap-8">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<h4 class="font-semibold text-sm mb-1">Include Paths</h4>
|
||||
{#if include_path.length > 0}
|
||||
<div class="flex flex-wrap gap-1 text-xs">
|
||||
{#each include_path as path}
|
||||
<span class="bg-surface-secondary text-primary rounded-full px-2 py-1">{path}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-tertiary text-xs">No include paths configured</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-semibold text-sm mb-1">Exclude Paths</h4>
|
||||
{#if excludes.length > 0}
|
||||
<div class="flex flex-wrap gap-1 text-xs">
|
||||
{#each excludes as path}
|
||||
<span class="bg-red-100 text-red-800 rounded-full px-2 py-1">{path}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-tertiary text-xs">No exclude paths configured</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<h4 class="font-semibold text-sm">Included Types</h4>
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
{#each Object.entries(typeToggles) as [key, enabled]}
|
||||
<div class="flex items-center gap-1">
|
||||
<div class={enabled ? 'text-green-600' : 'text-gray-400'}>
|
||||
{enabled ? '✓' : '✗'}
|
||||
</div>
|
||||
<span class={enabled ? 'text-primary' : 'text-tertiary'}>
|
||||
{capitalize(key)}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions slot for custom buttons -->
|
||||
{#if actions}
|
||||
<div class="flex justify-start mt-4">
|
||||
{@render actions()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- CLI Instructions (collapsible) -->
|
||||
<div class="border-t pt-2 mt-4">
|
||||
<button
|
||||
class="flex items-center gap-2 text-sm text-secondary hover:text-primary transition-colors"
|
||||
onclick={() => showCliInstructions = !showCliInstructions}
|
||||
<div class="flex gap-2 items-center">
|
||||
<Button
|
||||
size="sm"
|
||||
on:click={previewFiltersToGitRepo}
|
||||
disabled={isPreviewLoading || isPushing}
|
||||
startIcon={{
|
||||
icon: isPreviewLoading ? Loader2 : Eye,
|
||||
classes: isPreviewLoading ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
<Terminal size={16} />
|
||||
<span>Update settings with CLI</span>
|
||||
{#if showCliInstructions}
|
||||
<ChevronUp size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if showCliInstructions}
|
||||
<div class="mt-3 bg-surface-secondary rounded-lg p-3">
|
||||
<div class="text-xs text-tertiary mb-2">
|
||||
These filter settings are sourced from the <code class="bg-surface px-1 py-0.5 rounded">wmill.yaml</code> file in your git repository.
|
||||
To modify them, edit the file in your repository, commit the changes, and sync using these commands:
|
||||
</div>
|
||||
<pre class="text-xs bg-surface p-3 rounded overflow-x-auto whitespace-pre-wrap break-all">
|
||||
# Make sure your repo is up to date
|
||||
git pull
|
||||
|
||||
# Edit wmill.yaml file
|
||||
vim wmill.yaml
|
||||
|
||||
# Push changes to workspace
|
||||
wmill gitsync-settings push --workspace {$workspaceStore} --repository {git_repo_resource_path}
|
||||
|
||||
# Commit changes
|
||||
git add wmill.yaml
|
||||
git commit
|
||||
git push</pre>
|
||||
</div>
|
||||
{isPreviewLoading ? 'Previewing...' : 'Preview'}
|
||||
</Button>
|
||||
{#if previewResult?.hasChanges && (previewResult?.isInitialSetup || (previewResult?.diff && Object.keys(previewResult.diff).length > 0))}
|
||||
<Button
|
||||
size="sm"
|
||||
on:click={pushFiltersToGitRepo}
|
||||
disabled={isPushing || isPreviewLoading}
|
||||
color={isPullMode ? 'dark' : 'red'}
|
||||
startIcon={{
|
||||
icon: isPushing ? Loader2 : isPullMode ? Check : Save,
|
||||
classes: isPushing ? 'animate-spin' : ''
|
||||
}}
|
||||
>
|
||||
{isPushing
|
||||
? isPullMode
|
||||
? 'Applying...'
|
||||
: 'Pushing...'
|
||||
: isPullMode
|
||||
? 'Apply'
|
||||
: 'Push Settings to Git'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if previewError}
|
||||
<div class="text-xs text-red-600 mt-2">{previewError}</div>
|
||||
{/if}
|
||||
{#if previewJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary mt-1">
|
||||
{#if previewJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if previewJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if previewJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Preview job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${previewJobId}?workspace=${$workspaceStore}`}>{previewJobId}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if previewResult}
|
||||
<div
|
||||
class="border rounded p-2 text-xs max-h-40 overflow-y-auto bg-surface-secondary mt-2"
|
||||
>
|
||||
<div class="font-semibold text-[11px] mb-1 text-tertiary">Preview of changes:</div>
|
||||
{#if previewResult.isInitialSetup}
|
||||
<div class="mt-2 text-green-600">
|
||||
{previewResult.message || 'wmill.yaml will be created with repository settings'}
|
||||
</div>
|
||||
{:else if previewResult.hasChanges && previewResult.diff && Object.keys(previewResult.diff).length > 0}
|
||||
<div class="mt-2 space-y-1">
|
||||
{#each Object.entries(previewResult.diff) as [field, change]}
|
||||
<div class="flex items-start gap-2 text-2xs">
|
||||
<span class="font-mono text-tertiary min-w-0 flex-shrink-0">{field}:</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
{#if Array.isArray(change.from) || Array.isArray(change.to)}
|
||||
<div class="space-y-0.5">
|
||||
<div class="text-red-600">- {JSON.stringify(change.from)}</div>
|
||||
<div class="text-green-600">+ {JSON.stringify(change.to)}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-red-600">{JSON.stringify(change.from)}</span>
|
||||
<span class="text-tertiary"> → </span>
|
||||
<span class="text-green-600">{JSON.stringify(change.to)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2 text-tertiary">No changes found! The file is up to date.</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if pushJobId}
|
||||
<div class="flex items-center gap-2 text-xs text-tertiary mt-1">
|
||||
{#if pushJobStatus === 'running'}
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
{:else if pushJobStatus === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else if pushJobStatus === 'failure'}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Push job:
|
||||
<a
|
||||
target="_blank"
|
||||
class="underline"
|
||||
href={`/run/${pushJobId}?workspace=${$workspaceStore}`}>{pushJobId}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Deterministic backend response formats from hub script and CLI
|
||||
export interface SyncResponse {
|
||||
success: true
|
||||
changes: Array<{
|
||||
type: 'added' | 'edited' | 'deleted'
|
||||
path: string
|
||||
codebase_changed?: boolean
|
||||
}>
|
||||
total: number
|
||||
settingsDiffResult?: {
|
||||
hasChanges: boolean
|
||||
diff: Record<string, { from: any; to: any }>
|
||||
local: SettingsObject
|
||||
backend: SettingsObject
|
||||
}
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
success: true
|
||||
hasChanges: boolean
|
||||
local: SettingsObject
|
||||
backend: SettingsObject
|
||||
diff: Record<string, { from: any; to: any }>
|
||||
repository: string
|
||||
}
|
||||
|
||||
import type { GitSyncObjectType } from '$lib/gen'
|
||||
|
||||
export interface SettingsObject {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: GitSyncObjectType[]
|
||||
}
|
||||
@@ -8,13 +8,12 @@
|
||||
"gitSync_6": "hub/11666/sync-script-to-git-repo-windmill",
|
||||
"gitSync_7": "hub/11668/sync-script-to-git-repo-windmill",
|
||||
"gitSync_8": "hub/19673/sync-script-to-git-repo-windmill",
|
||||
"gitSync_9": "hub/19738/sync-script-to-git-repo-windmill",
|
||||
"gitSync": "hub/19785/sync-script-to-git-repo-windmill",
|
||||
"gitSync": "hub/19738/sync-script-to-git-repo-windmill",
|
||||
"gitSyncTest_0": "hub/9073/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest_1": "hub/11499/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest_2": "hub/11667/git-repo-test-read-write-windmill",
|
||||
"gitSyncTest": "hub/11669/git-repo-test-read-write-windmill",
|
||||
"gitInitRepo": "hub/19784/git-sync%3A-init-repository-windmill",
|
||||
"gitInitRepo": "hub/19740/git-sync%3A-init-repository-windmill",
|
||||
"slackErrorHandler": "hub/19741/workspace-or-schedule-error-handler-slack",
|
||||
"slackErrorHandler_0": "hub/9079/workspace-or-schedule-error-handler-slack",
|
||||
"slackErrorHandler_1": "hub/9206/workspace-or-schedule-error-handler-slack",
|
||||
|
||||
@@ -39,7 +39,7 @@ import wasmUrlCSharp from 'windmill-parser-wasm-csharp/windmill_parser_wasm_bg.w
|
||||
import wasmUrlNu from 'windmill-parser-wasm-nu/windmill_parser_wasm_bg.wasm?url'
|
||||
import wasmUrlJava from 'windmill-parser-wasm-java/windmill_parser_wasm_bg.wasm?url'
|
||||
import { workspaceStore } from './stores.js'
|
||||
import { argSigToJsonSchemaType } from 'windmill-utils-internal'
|
||||
import { argSigToJsonSchemaType } from './inferArgSig.js'
|
||||
import { type AssetWithAccessType } from './components/assets/lib.js'
|
||||
|
||||
const loadSchemaLastRun =
|
||||
|
||||
@@ -1,53 +1,5 @@
|
||||
/**
|
||||
* Type alias for enum values - can be an array of strings or undefined
|
||||
*/
|
||||
export type EnumType = string[] | undefined
|
||||
import type { SchemaProperty } from './common'
|
||||
|
||||
/**
|
||||
* Represents a property in a JSON schema with various validation and display options
|
||||
*/
|
||||
export interface SchemaProperty {
|
||||
type: string | undefined
|
||||
description?: string
|
||||
pattern?: string
|
||||
default?: any
|
||||
enum?: EnumType
|
||||
contentEncoding?: 'base64' | 'binary'
|
||||
format?: string
|
||||
items?: {
|
||||
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
|
||||
contentEncoding?: 'base64'
|
||||
enum?: string[]
|
||||
resourceType?: string
|
||||
properties?: { [name: string]: SchemaProperty }
|
||||
}
|
||||
min?: number
|
||||
max?: number
|
||||
currency?: string
|
||||
currencyLocale?: string
|
||||
multiselect?: boolean
|
||||
customErrorMessage?: string
|
||||
properties?: { [name: string]: SchemaProperty }
|
||||
required?: string[]
|
||||
showExpr?: string
|
||||
password?: boolean
|
||||
order?: string[]
|
||||
nullable?: boolean
|
||||
dateFormat?: string
|
||||
title?: string
|
||||
placeholder?: string
|
||||
oneOf?: SchemaProperty[]
|
||||
originalType?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts argument signature types to JSON schema properties.
|
||||
* This function handles various Windmill-specific types and converts them
|
||||
* to standard JSON schema format while preserving existing property metadata.
|
||||
*
|
||||
* @param t - The argument signature type definition (can be string or complex object types)
|
||||
* @param oldS - Existing schema property to update with new type information
|
||||
*/
|
||||
export function argSigToJsonSchemaType(
|
||||
t:
|
||||
| string
|
||||
@@ -239,5 +191,9 @@ export function argSigToJsonSchemaType(
|
||||
}
|
||||
|
||||
Object.assign(oldS, newS)
|
||||
// if (sameItems && savedItems != undefined && savedItems.enum != undefined) {
|
||||
// sendUserToast(JSON.stringify(savedItems))
|
||||
// oldS.items = savedItems
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { JobService } from '$lib/gen'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
|
||||
export interface JobStatus {
|
||||
status: 'running' | 'success' | 'failure'
|
||||
result?: any
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface JobOptions {
|
||||
onProgress?: (status: JobStatus) => void
|
||||
timeout?: number
|
||||
workspace: string
|
||||
interval?: number
|
||||
timeoutMessage?: string
|
||||
}
|
||||
|
||||
interface JobEntry {
|
||||
controller: AbortController
|
||||
startTime: number
|
||||
}
|
||||
|
||||
export class JobManager {
|
||||
private activeJobs = new Map<string, JobEntry>()
|
||||
private cleanupInterval: NodeJS.Timeout | null = null
|
||||
private readonly STALE_TIMEOUT = 300000 // 5 minutes
|
||||
private readonly CLEANUP_INTERVAL = 60000 // 1 minute
|
||||
|
||||
constructor() {
|
||||
this.startCleanupTimer()
|
||||
}
|
||||
|
||||
private startCleanupTimer() {
|
||||
if (typeof window !== 'undefined') {
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cleanup()
|
||||
}, this.CLEANUP_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
const now = Date.now()
|
||||
const staleJobs: string[] = []
|
||||
|
||||
for (const [jobId, entry] of this.activeJobs.entries()) {
|
||||
if (now - entry.startTime > this.STALE_TIMEOUT) {
|
||||
entry.controller.abort()
|
||||
staleJobs.push(jobId)
|
||||
}
|
||||
}
|
||||
|
||||
staleJobs.forEach(jobId => {
|
||||
this.activeJobs.delete(jobId)
|
||||
})
|
||||
|
||||
if (staleJobs.length > 0) {
|
||||
console.warn(`Cleaned up ${staleJobs.length} stale job controllers`)
|
||||
}
|
||||
}
|
||||
|
||||
async runWithProgress<T>(
|
||||
jobRunner: () => Promise<string>,
|
||||
options: JobOptions
|
||||
): Promise<T> {
|
||||
const {
|
||||
onProgress,
|
||||
timeout = 60000,
|
||||
workspace,
|
||||
interval = 500,
|
||||
timeoutMessage = `Job timed out after ${timeout / 1000}s`
|
||||
} = options
|
||||
|
||||
const controller = new AbortController()
|
||||
const jobId = await jobRunner()
|
||||
|
||||
this.activeJobs.set(jobId, {
|
||||
controller,
|
||||
startTime: Date.now()
|
||||
})
|
||||
|
||||
try {
|
||||
onProgress?.({ status: 'running' })
|
||||
|
||||
const result = await tryEvery({
|
||||
tryCode: async () => {
|
||||
if (controller.signal.aborted) {
|
||||
throw new Error('Job was cancelled')
|
||||
}
|
||||
|
||||
const jobResult = await JobService.getCompletedJob({
|
||||
workspace,
|
||||
id: jobId
|
||||
})
|
||||
|
||||
const success = !!jobResult.success
|
||||
const status: JobStatus = {
|
||||
status: success ? 'success' : 'failure',
|
||||
result: jobResult.result,
|
||||
error: success ? undefined : (jobResult.result as any)?.error?.message || 'Job failed'
|
||||
}
|
||||
|
||||
onProgress?.(status)
|
||||
|
||||
if (!success) {
|
||||
throw new Error(status.error)
|
||||
}
|
||||
|
||||
return jobResult.result as T
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace,
|
||||
id: jobId,
|
||||
requestBody: { reason: timeoutMessage }
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to cancel job:', err)
|
||||
}
|
||||
|
||||
onProgress?.({ status: 'failure', error: timeoutMessage })
|
||||
throw new Error(timeoutMessage)
|
||||
},
|
||||
interval,
|
||||
timeout
|
||||
})
|
||||
|
||||
return result as T
|
||||
} finally {
|
||||
this.activeJobs.delete(jobId)
|
||||
}
|
||||
}
|
||||
|
||||
cancel(jobId: string) {
|
||||
const entry = this.activeJobs.get(jobId)
|
||||
if (entry) {
|
||||
entry.controller.abort()
|
||||
this.activeJobs.delete(jobId)
|
||||
}
|
||||
}
|
||||
|
||||
cancelAll() {
|
||||
this.activeJobs.forEach(entry => entry.controller.abort())
|
||||
this.activeJobs.clear()
|
||||
}
|
||||
|
||||
isActive(jobId: string): boolean {
|
||||
return this.activeJobs.has(jobId)
|
||||
}
|
||||
|
||||
get activeJobCount(): number {
|
||||
return this.activeJobs.size
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval)
|
||||
this.cleanupInterval = null
|
||||
}
|
||||
this.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance for global usage
|
||||
export const jobManager = new JobManager()
|
||||
@@ -4,10 +4,12 @@
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Alert, Button, Skeleton, Tab, Tabs } from '$lib/components/common'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
|
||||
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
@@ -16,6 +18,7 @@
|
||||
import {
|
||||
OauthService,
|
||||
WorkspaceService,
|
||||
JobService,
|
||||
ResourceService,
|
||||
SettingService,
|
||||
type AIConfig
|
||||
@@ -29,15 +32,23 @@
|
||||
isCriticalAlertsUIOpen
|
||||
} from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { emptyString, tryEvery } from '$lib/utils'
|
||||
import {
|
||||
XCircle,
|
||||
RotateCw,
|
||||
Save
|
||||
RotateCcw,
|
||||
CheckCircle2,
|
||||
Trash,
|
||||
Plus,
|
||||
Loader2,
|
||||
Save,
|
||||
ExternalLink
|
||||
} from 'lucide-svelte'
|
||||
|
||||
import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
import { fade } from 'svelte/transition'
|
||||
import ChangeWorkspaceName from '$lib/components/settings/ChangeWorkspaceName.svelte'
|
||||
import ChangeWorkspaceId from '$lib/components/settings/ChangeWorkspaceId.svelte'
|
||||
import ChangeWorkspaceColor from '$lib/components/settings/ChangeWorkspaceColor.svelte'
|
||||
@@ -46,13 +57,61 @@
|
||||
type S3ResourceSettings
|
||||
} from '$lib/workspace_settings'
|
||||
import { base } from '$lib/base'
|
||||
import { hubPaths } from '$lib/hub'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import ConnectionSection from '$lib/components/ConnectionSection.svelte'
|
||||
import AISettings from '$lib/components/workspaceSettings/AISettings.svelte'
|
||||
import StorageSettings from '$lib/components/workspaceSettings/StorageSettings.svelte'
|
||||
import GitSyncSection from '$lib/components/git_sync/GitSyncSection.svelte'
|
||||
import InitGitRepoPopover from '$lib/components/InitGitRepoPopover.svelte'
|
||||
import PullGitRepoPopover from '$lib/components/PullGitRepoPopover.svelte'
|
||||
import GitSyncFilterSettings from '$lib/components/workspaceSettings/GitSyncFilterSettings.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
// Shared defaults for new Git-Sync repositories
|
||||
const DEFAULT_INCLUDE_PATH = ['f/**'] as const;
|
||||
const DEFAULT_EXCLUDE_PATH: string[] = [];
|
||||
const DEFAULT_EXTRA_INCLUDE_PATH: string[] = [];
|
||||
|
||||
type ObjectType =
|
||||
| 'script'
|
||||
| 'flow'
|
||||
| 'app'
|
||||
| 'folder'
|
||||
| 'resource'
|
||||
| 'variable'
|
||||
| 'secret'
|
||||
| 'resourcetype'
|
||||
| 'schedule'
|
||||
| 'user'
|
||||
| 'group'
|
||||
| 'trigger'
|
||||
| 'settings'
|
||||
| 'key'
|
||||
|
||||
type GitSyncSettings = {
|
||||
repositories: GitSyncRepository[]
|
||||
}
|
||||
|
||||
type GitRepositorySettings = {
|
||||
include_path: string[]
|
||||
exclude_path: string[]
|
||||
extra_include_path: string[]
|
||||
include_type: ObjectType[]
|
||||
}
|
||||
|
||||
// Import the generated backend type
|
||||
import type { GitRepositorySettings as BackendGitRepositorySettings } from '$lib/gen'
|
||||
|
||||
// Frontend repository format extends backend with guaranteed settings and additional UI state
|
||||
type GitSyncRepository = BackendGitRepositorySettings & {
|
||||
settings: GitRepositorySettings // Required in frontend after transformation
|
||||
legacyImported?: boolean
|
||||
}
|
||||
|
||||
// Workspace-level legacy filter arrays (populated if we imported legacy settings)
|
||||
let legacyWorkspaceIncludePath = $state<string[]>([])
|
||||
let legacyWorkspaceIncludeType = $state<ObjectType[]>([])
|
||||
|
||||
let slackInitialPath: string = $state('')
|
||||
let slackScriptPath: string = $state('')
|
||||
let teamsInitialPath: string = $state('')
|
||||
@@ -84,6 +143,16 @@
|
||||
secondaryStorage: undefined
|
||||
})
|
||||
|
||||
let gitSyncSettings = $state<GitSyncSettings>({
|
||||
repositories: []
|
||||
})
|
||||
|
||||
let gitSyncTestJobs = $state<
|
||||
{
|
||||
jobId: string | undefined
|
||||
status: 'running' | 'success' | 'failure' | undefined
|
||||
}[]
|
||||
>([])
|
||||
|
||||
let workspaceDefaultAppPath: string | undefined = $state(undefined)
|
||||
let workspaceEncryptionKey: string | undefined = $state(undefined)
|
||||
@@ -103,14 +172,245 @@
|
||||
)
|
||||
let usingOpenaiClientCredentialsOauth = $state(false)
|
||||
|
||||
|
||||
|
||||
let yamlText = $state('')
|
||||
let initialGitSyncSettings = $state<GitSyncSettings | undefined>(undefined)
|
||||
let loadedSettings = $state(false)
|
||||
|
||||
// Reactive trigger to ensure UI updates when repository data changes
|
||||
let repoReactivityTrigger = $state(0)
|
||||
|
||||
|
||||
|
||||
const latestGitSyncHubScript = hubPaths.gitSync
|
||||
|
||||
// Each repository may have been populated from workspace-level legacy settings. Track on the repo itself.
|
||||
const anyLegacyImported = $derived(gitSyncSettings.repositories.some((r) => r.legacyImported))
|
||||
|
||||
// Track changes in repositories
|
||||
const repoChanges = $derived(
|
||||
(() => {
|
||||
// Force reactivity check by accessing the trigger
|
||||
repoReactivityTrigger;
|
||||
|
||||
return gitSyncSettings.repositories.map((repo, idx) => {
|
||||
const repoValid = isRepoValid(idx)
|
||||
|
||||
// If there were no initial repos, treat each repo as changed only when valid
|
||||
if (!initialGitSyncSettings || !initialGitSyncSettings.repositories || initialGitSyncSettings.repositories.length === 0) {
|
||||
return repoValid
|
||||
}
|
||||
|
||||
const initial = initialGitSyncSettings.repositories.find(
|
||||
initialRepo => initialRepo.git_repo_resource_path === repo.git_repo_resource_path
|
||||
)
|
||||
|
||||
// If no matching initial repo found, this is a new repo - changed only when valid
|
||||
if (!initial) return repoValid
|
||||
|
||||
// Handle array ordering for consistent comparison
|
||||
const settings1 = {
|
||||
include_path: [...(initial.settings?.include_path ?? [])].sort(),
|
||||
exclude_path: [...(initial.settings?.exclude_path ?? [])].sort(),
|
||||
extra_include_path: [...(initial.settings?.extra_include_path ?? [])].sort(),
|
||||
include_type: [...(initial.settings?.include_type ?? [])].sort()
|
||||
};
|
||||
|
||||
const settings2 = {
|
||||
include_path: [...(repo.settings?.include_path ?? [])].sort(),
|
||||
exclude_path: [...(repo.settings?.exclude_path ?? [])].sort(),
|
||||
extra_include_path: [...(repo.settings?.extra_include_path ?? [])].sort(),
|
||||
include_type: [...(repo.settings?.include_type ?? [])].sort()
|
||||
};
|
||||
|
||||
// Compare all properties in a consistent way
|
||||
const isChanged = !deepEqual(
|
||||
{
|
||||
settings: settings1,
|
||||
use_individual_branch: initial.use_individual_branch,
|
||||
group_by_folder: initial.group_by_folder,
|
||||
script_path: initial.script_path,
|
||||
git_repo_resource_path: initial.git_repo_resource_path,
|
||||
exclude_types_override: [...(initial.exclude_types_override ?? [])].sort()
|
||||
},
|
||||
{
|
||||
settings: settings2,
|
||||
use_individual_branch: repo.use_individual_branch,
|
||||
group_by_folder: repo.group_by_folder,
|
||||
script_path: repo.script_path,
|
||||
git_repo_resource_path: repo.git_repo_resource_path,
|
||||
exclude_types_override: [...(repo.exclude_types_override ?? [])].sort()
|
||||
}
|
||||
)
|
||||
|
||||
return isChanged && repoValid
|
||||
})
|
||||
})()
|
||||
)
|
||||
|
||||
const hasAnyChanges = $derived(
|
||||
repoChanges.some(Boolean) ||
|
||||
anyLegacyImported ||
|
||||
// Check if the set of valid repos has changed (added/removed repos)
|
||||
(() => {
|
||||
if (!initialGitSyncSettings?.repositories) return gitSyncSettings.repositories.filter((_,i)=>isRepoValid(i)).length > 0
|
||||
|
||||
const initialValidPaths = new Set(
|
||||
initialGitSyncSettings.repositories
|
||||
.filter(r => !emptyString(r.git_repo_resource_path))
|
||||
.map(r => r.git_repo_resource_path)
|
||||
)
|
||||
const currentValidPaths = new Set(
|
||||
gitSyncSettings.repositories
|
||||
.filter((_,i) => isRepoValid(i))
|
||||
.map(r => r.git_repo_resource_path)
|
||||
)
|
||||
|
||||
// Check if sets are different (repos added or removed)
|
||||
return initialValidPaths.size !== currentValidPaths.size ||
|
||||
[...initialValidPaths].some(path => !currentValidPaths.has(path)) ||
|
||||
[...currentValidPaths].some(path => !initialValidPaths.has(path))
|
||||
})()
|
||||
)
|
||||
|
||||
// Helper that tells if a repo card is valid (resource selected and not duplicated)
|
||||
function isRepoValid(idx: number): boolean {
|
||||
const repo = gitSyncSettings.repositories[idx]
|
||||
if (!repo) return false
|
||||
if (emptyString(repo.git_repo_resource_path)) return false
|
||||
return !isRepoDuplicate(idx)
|
||||
}
|
||||
|
||||
// Helper: true if repo shares its resource with an earlier repo
|
||||
function isRepoDuplicate(idx: number): boolean {
|
||||
const repo = gitSyncSettings.repositories[idx]
|
||||
if (!repo || emptyString(repo.git_repo_resource_path)) return false
|
||||
const firstIdx = gitSyncSettings.repositories.findIndex(r => r.git_repo_resource_path === repo.git_repo_resource_path)
|
||||
return firstIdx !== idx
|
||||
}
|
||||
|
||||
function serializeRepo(repo: GitSyncRepository) {
|
||||
const serialized: any = {
|
||||
script_path: repo.script_path,
|
||||
git_repo_resource_path: `$res:${repo.git_repo_resource_path.replace('$res:', '')}`,
|
||||
use_individual_branch: repo.use_individual_branch,
|
||||
group_by_folder: repo.group_by_folder,
|
||||
settings: repo.settings
|
||||
}
|
||||
|
||||
// exclude_types_override should never be included for migrated repos (only legacy repos have it)
|
||||
// Migration removes excluded types from include_type and drops exclude_types_override
|
||||
|
||||
return serialized
|
||||
}
|
||||
|
||||
async function saveRepoSettings(idx: number): Promise<void> {
|
||||
const currentRepo = gitSyncSettings.repositories[idx]
|
||||
if (!currentRepo || !isRepoValid(idx)) {
|
||||
sendUserToast('Cannot save invalid repository (missing or duplicate resource)', true)
|
||||
return
|
||||
}
|
||||
|
||||
// If we started with empty settings, we need to save all valid repositories
|
||||
if (!initialGitSyncSettings || !initialGitSyncSettings.repositories || initialGitSyncSettings.repositories.length === 0) {
|
||||
// For new repositories starting from empty, save all current repositories with valid resources
|
||||
const validRepositories = gitSyncSettings.repositories
|
||||
.filter((_,i)=>isRepoValid(i))
|
||||
.map(repo => serializeRepo(repo))
|
||||
|
||||
await WorkspaceService.editWorkspaceGitSyncConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
git_sync_settings: { repositories: validRepositories }
|
||||
}
|
||||
})
|
||||
|
||||
// Mark all repos migrated and reset legacy arrays
|
||||
gitSyncSettings.repositories.forEach(r => r.legacyImported = false)
|
||||
legacyWorkspaceIncludePath = []
|
||||
legacyWorkspaceIncludeType = []
|
||||
// Update initial settings to reflect what we just saved
|
||||
initialGitSyncSettings = JSON.parse(JSON.stringify(gitSyncSettings))
|
||||
} else {
|
||||
// Build repositories array: include all repos but serialize differently based on migration status
|
||||
let repositories: any[] = []
|
||||
|
||||
// Process all repos that should be in the final payload
|
||||
for (const repo of gitSyncSettings.repositories) {
|
||||
if (repo === currentRepo) {
|
||||
// This is the repo we're saving - migrate if legacy, otherwise serialize normally
|
||||
if (currentRepo.legacyImported) {
|
||||
// Migrate legacy repo: remove excluded types from include_type and drop exclude_types_override
|
||||
const migratedRepo = {
|
||||
...currentRepo,
|
||||
settings: {
|
||||
...currentRepo.settings,
|
||||
include_type: currentRepo.settings.include_type.filter(
|
||||
type => !currentRepo.exclude_types_override?.includes(type)
|
||||
)
|
||||
},
|
||||
exclude_types_override: [], // Clear this for migrated repo
|
||||
legacyImported: false
|
||||
}
|
||||
repositories.push(serializeRepo(migratedRepo))
|
||||
// Update the current repo in the UI to reflect migration
|
||||
Object.assign(currentRepo, migratedRepo)
|
||||
} else {
|
||||
repositories.push(serializeRepo(currentRepo))
|
||||
}
|
||||
} else if (repo.legacyImported) {
|
||||
// This is a legacy repo - serialize without settings field at all
|
||||
const legacyRepoData: any = {
|
||||
script_path: repo.script_path,
|
||||
git_repo_resource_path: `$res:${repo.git_repo_resource_path.replace('$res:', '')}`,
|
||||
use_individual_branch: repo.use_individual_branch,
|
||||
group_by_folder: repo.group_by_folder
|
||||
}
|
||||
// Include exclude_types_override if it has values
|
||||
if (repo.exclude_types_override && repo.exclude_types_override.length > 0) {
|
||||
legacyRepoData.exclude_types_override = repo.exclude_types_override
|
||||
}
|
||||
repositories.push(legacyRepoData)
|
||||
} else {
|
||||
// This is an already-migrated repo
|
||||
repositories.push(serializeRepo(repo))
|
||||
}
|
||||
}
|
||||
|
||||
// Mark current repo as migrated
|
||||
currentRepo.legacyImported = false;
|
||||
|
||||
// Check if there are still legacy repos
|
||||
const remainingLegacy = gitSyncSettings.repositories.some(r => r.legacyImported)
|
||||
|
||||
const gitSyncPayload: any = {
|
||||
git_sync_settings: {
|
||||
repositories,
|
||||
...(remainingLegacy && {
|
||||
include_path: legacyWorkspaceIncludePath,
|
||||
include_type: legacyWorkspaceIncludeType
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Sending payload:', JSON.stringify(gitSyncPayload, null, 2))
|
||||
|
||||
await WorkspaceService.editWorkspaceGitSyncConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: gitSyncPayload
|
||||
})
|
||||
|
||||
// Update initial settings to reflect current state
|
||||
initialGitSyncSettings = JSON.parse(JSON.stringify(gitSyncSettings))
|
||||
|
||||
// if no more legacy repos, clear workspace-level legacy arrays
|
||||
if (!remainingLegacy) {
|
||||
legacyWorkspaceIncludePath = []
|
||||
legacyWorkspaceIncludeType = []
|
||||
}
|
||||
}
|
||||
|
||||
sendUserToast('Repository settings updated')
|
||||
}
|
||||
|
||||
async function editWorkspaceCommand(platform: 'slack' | 'teams'): Promise<void> {
|
||||
if (platform === 'slack') {
|
||||
@@ -166,6 +466,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function editWindmillGitSyncSettings(): Promise<void> {
|
||||
// Filter out repositories with empty resource paths before processing
|
||||
const validRepos = gitSyncSettings.repositories.filter((_,i)=>isRepoValid(i))
|
||||
|
||||
let alreadySeenResource: string[] = []
|
||||
let repositories = validRepos.map((repo) => {
|
||||
alreadySeenResource.push(repo.git_repo_resource_path)
|
||||
|
||||
return serializeRepo(repo)
|
||||
})
|
||||
|
||||
if (alreadySeenResource.some((res, index) => alreadySeenResource.indexOf(res) !== index)) {
|
||||
sendUserToast('Same Git resource used more than once', true)
|
||||
return
|
||||
}
|
||||
|
||||
if (repositories.length > 0) {
|
||||
await WorkspaceService.editWorkspaceGitSyncConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
git_sync_settings: {
|
||||
repositories
|
||||
}
|
||||
}
|
||||
})
|
||||
// Update initial settings to reflect what we just saved
|
||||
initialGitSyncSettings = {
|
||||
repositories: gitSyncSettings.repositories.filter((_,i)=>isRepoValid(i))
|
||||
}
|
||||
sendUserToast('Workspace Git sync settings updated')
|
||||
gitSyncSettings.repositories.forEach(r => r.legacyImported = false);
|
||||
legacyWorkspaceIncludePath = [];
|
||||
legacyWorkspaceIncludeType = [];
|
||||
} else {
|
||||
await WorkspaceService.editWorkspaceGitSyncConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
git_sync_settings: { repositories: [] }
|
||||
}
|
||||
})
|
||||
initialGitSyncSettings = { repositories: [] }
|
||||
sendUserToast('Workspace Git sync settings updated (no repositories)')
|
||||
}
|
||||
}
|
||||
|
||||
async function editWorkspaceDefaultApp(appPath: string | undefined): Promise<void> {
|
||||
if (emptyString(appPath)) {
|
||||
@@ -269,6 +613,55 @@
|
||||
|
||||
s3ResourceSettings = convertBackendSettingsToFrontendSettings(settings.large_file_storage)
|
||||
|
||||
if (settings.git_sync !== undefined && settings.git_sync !== null) {
|
||||
gitSyncTestJobs = []
|
||||
// Derive workspace-level legacy defaults (outside repositories)
|
||||
const workspaceLegacyIncludePath: string[] = (settings.git_sync as any)?.include_path ?? [];
|
||||
const workspaceLegacyIncludeTypeRaw: ObjectType[] = (settings.git_sync as any)?.include_type ?? [];
|
||||
// Note: exclude_types_override is only at repository level, not workspace level
|
||||
const workspaceLegacyIncludeType: ObjectType[] = [...workspaceLegacyIncludeTypeRaw];
|
||||
|
||||
legacyWorkspaceIncludePath = [...workspaceLegacyIncludePath];
|
||||
legacyWorkspaceIncludeType = [...workspaceLegacyIncludeType];
|
||||
|
||||
gitSyncSettings.repositories = (settings.git_sync.repositories ?? []).map((repo: BackendGitRepositorySettings) => {
|
||||
gitSyncTestJobs.push({
|
||||
jobId: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// Now we have proper nested settings structure from the backend
|
||||
const defaultTypes: ObjectType[] = workspaceLegacyIncludeType.length > 0
|
||||
? [...workspaceLegacyIncludeType]
|
||||
: (['script', 'flow', 'app', 'folder'] as ObjectType[]);
|
||||
|
||||
// Check if this is a legacy repo (no nested settings object)
|
||||
const isRepoLegacy = !repo.settings;
|
||||
const repoExcludeTypesOverride = repo.exclude_types_override ?? [];
|
||||
|
||||
const repoSettings: GitRepositorySettings = {
|
||||
include_path: repo.settings?.include_path ?? [...workspaceLegacyIncludePath],
|
||||
exclude_path: repo.settings?.exclude_path ?? [],
|
||||
extra_include_path: repo.settings?.extra_include_path ?? [],
|
||||
include_type: (repo.settings?.include_type ?? [...defaultTypes]) as ObjectType[]
|
||||
};
|
||||
|
||||
return {
|
||||
...repo,
|
||||
git_repo_resource_path: repo.git_repo_resource_path.replace('$res:', ''),
|
||||
collapsed: repo.collapsed ?? false,
|
||||
settings: repoSettings,
|
||||
exclude_types_override: repoExcludeTypesOverride,
|
||||
legacyImported: isRepoLegacy && (legacyWorkspaceIncludePath.length > 0 || legacyWorkspaceIncludeType.length > 0)
|
||||
} satisfies GitSyncRepository
|
||||
})
|
||||
// Store initial settings
|
||||
initialGitSyncSettings = JSON.parse(JSON.stringify(gitSyncSettings))
|
||||
} else {
|
||||
gitSyncSettings.repositories = []
|
||||
gitSyncTestJobs = []
|
||||
initialGitSyncSettings = undefined
|
||||
}
|
||||
if (settings.deploy_ui != undefined && settings.deploy_ui != null) {
|
||||
deployUiSettings = {
|
||||
include_path:
|
||||
@@ -339,9 +732,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function runGitSyncTestJob(settingsIdx: number) {
|
||||
let gitSyncRepository = gitSyncSettings.repositories[settingsIdx]
|
||||
if (emptyString(gitSyncRepository.script_path)) {
|
||||
return
|
||||
}
|
||||
let jobId = await JobService.runScriptByPath({
|
||||
workspace: $workspaceStore!,
|
||||
path: hubPaths.gitSyncTest,
|
||||
requestBody: {
|
||||
repo_url_resource_path: gitSyncRepository.git_repo_resource_path.replace('$res:', '')
|
||||
},
|
||||
skipPreprocessor: true
|
||||
})
|
||||
gitSyncTestJobs[settingsIdx] = {
|
||||
jobId: jobId,
|
||||
status: 'running'
|
||||
}
|
||||
gitSyncTestJobs = [...gitSyncTestJobs]
|
||||
tryEvery({
|
||||
tryCode: async () => {
|
||||
const testResult = await JobService.getCompletedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: jobId
|
||||
})
|
||||
gitSyncTestJobs[settingsIdx].status = testResult.success ? 'success' : 'failure'
|
||||
gitSyncTestJobs = [...gitSyncTestJobs]
|
||||
},
|
||||
timeoutCode: async () => {
|
||||
try {
|
||||
await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore!,
|
||||
id: jobId,
|
||||
requestBody: {
|
||||
reason: 'Git sync test job timed out after 5s'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
gitSyncTestJobs[settingsIdx].status = 'failure'
|
||||
gitSyncTestJobs = [...gitSyncTestJobs]
|
||||
}
|
||||
},
|
||||
interval: 500,
|
||||
timeout: 5000
|
||||
})
|
||||
}
|
||||
|
||||
async function editCriticalAlertMuteSetting() {
|
||||
await SettingService.workspaceMuteCriticalAlertsUi({
|
||||
@@ -521,7 +958,7 @@
|
||||
<DeployToSetting bind:workspaceToDeployTo bind:deployUiSettings />
|
||||
{:else}
|
||||
<div class="my-2"
|
||||
><Alert type="warning" title="Enterprise license required"
|
||||
><Alert type="error" title="Enterprise license required"
|
||||
>Deploy to staging/prod from the web UI is only available with an enterprise license</Alert
|
||||
></div
|
||||
>
|
||||
@@ -572,7 +1009,7 @@
|
||||
{:else if slack_tabs === 'teams_commands'}
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="pt-4"></div>
|
||||
<Alert type="warning" title="Workspace Teams commands is an EE feature">
|
||||
<Alert type="info" title="Workspace Teams commands is an EE feature">
|
||||
Workspace Teams commands is a Windmill EE feature. It enables using your current Slack
|
||||
/ Teams connection to run a custom script and send notifications.
|
||||
</Alert>
|
||||
@@ -700,7 +1137,7 @@
|
||||
{:else if tab == 'error_handler'}
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="pt-4"></div>
|
||||
<Alert type="warning" title="Workspace error handler is an EE feature">
|
||||
<Alert type="info" title="Workspace error handler is an EE feature">
|
||||
Workspace error handler is a Windmill EE feature. It enables using your current Slack
|
||||
connection or a custom script to send notifications anytime any job would fail.
|
||||
</Alert>
|
||||
@@ -817,12 +1254,326 @@
|
||||
{:else if tab == 'windmill_lfs'}
|
||||
<StorageSettings bind:s3ResourceSettings />
|
||||
{:else if tab == 'git_sync'}
|
||||
{#if $workspaceStore}
|
||||
<GitSyncSection />
|
||||
{:else}
|
||||
<div class="flex items-center justify-center p-8">
|
||||
<div class="text-sm text-secondary">Loading workspace...</div>
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-primary text-lg font-semibold">Git Sync</div>
|
||||
<Description link="https://www.windmill.dev/docs/advanced/git_sync">
|
||||
Connect the Windmill workspace to a Git repository to automatically commit and push
|
||||
scripts, flows, and apps to the repository on each deploy.
|
||||
</Description>
|
||||
</div>
|
||||
<Alert type="info" title="Only new updates trigger git sync">
|
||||
Only new changes matching the filters will trigger a git sync. You still need to initialize
|
||||
the repo to the desired state first.
|
||||
</Alert>
|
||||
</div>
|
||||
{#if !$enterpriseLicense}
|
||||
<div class="mb-2"></div>
|
||||
|
||||
<Alert type="warning" title="Syncing workspace to Git is an EE feature">
|
||||
Automatically saving scripts to a Git repository on each deploy is a Windmill EE feature.
|
||||
</Alert>
|
||||
<div class="mb-2"></div>
|
||||
{/if}
|
||||
{#if gitSyncSettings != undefined}
|
||||
<div class="flex mt-5 mb-5 gap-8">
|
||||
<Button
|
||||
color="red"
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={!$enterpriseLicense || !hasAnyChanges || gitSyncSettings.repositories.some((_,i)=>!isRepoValid(i))}
|
||||
on:click={() => {
|
||||
editWindmillGitSyncSettings()
|
||||
console.log('Saving git sync settings', gitSyncSettings)
|
||||
}}>Save all git sync settings {!$enterpriseLicense ? '(ee only)' : ''}</Button
|
||||
>
|
||||
|
||||
<Button
|
||||
color="dark"
|
||||
target="_blank"
|
||||
endIcon={{ icon: ExternalLink }}
|
||||
href={`/runs?job_kinds=deploymentcallbacks&workspace=${$workspaceStore}`}
|
||||
>See sync jobs</Button
|
||||
>
|
||||
</div>
|
||||
<div class="pt-2"></div>
|
||||
|
||||
|
||||
|
||||
{#if Array.isArray(gitSyncSettings.repositories)}
|
||||
{#each gitSyncSettings.repositories as repo, idx}
|
||||
<div class="rounded-lg shadow-sm border p-0 w-full mb-4">
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-center justify-between min-h-10 px-4 py-1 border-b">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Repository #{idx + 1}</span>
|
||||
<span class="text-xs text-tertiary pt-1 pl-8">
|
||||
{repo.git_repo_resource_path}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if (repoChanges[idx] || repo.legacyImported) && isRepoValid(idx)}
|
||||
<Button
|
||||
color="red"
|
||||
size="xs"
|
||||
on:click={() => saveRepoSettings(idx)}
|
||||
startIcon={{ icon: Save }}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
// Revert to initial repository settings
|
||||
if (initialGitSyncSettings?.repositories[idx]) {
|
||||
gitSyncSettings.repositories[idx] = JSON.parse(JSON.stringify(initialGitSyncSettings.repositories[idx]));
|
||||
sendUserToast('Reverted repository settings');
|
||||
}
|
||||
}}
|
||||
startIcon={{ icon: RotateCcw }}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
{/if}
|
||||
<button
|
||||
class="text-secondary hover:text-primary focus:outline-none"
|
||||
onclick={() => (repo.collapsed = !repo.collapsed)}
|
||||
aria-label="Toggle collapse"
|
||||
>
|
||||
{#if repo.collapsed}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="rounded-full p-2 bg-surface-secondary duration-200 hover:bg-surface-hover"
|
||||
aria-label="Remove repository"
|
||||
onclick={() => {
|
||||
gitSyncSettings.repositories = gitSyncSettings.repositories.filter((_, i) => i !== idx)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if !repo.collapsed}
|
||||
<div class="px-4 py-2">
|
||||
<div class="flex mt-5 mb-1 gap-1">
|
||||
{#key repo}
|
||||
<div class="pt-1 font-semibold">Resource: </div>
|
||||
<ResourcePicker
|
||||
bind:value={repo.git_repo_resource_path}
|
||||
resourceType={'git_repository'}
|
||||
/>
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<Button
|
||||
disabled={emptyString(repo.script_path)}
|
||||
color="dark"
|
||||
on:click={() => runGitSyncTestJob(idx)}
|
||||
size="xs">Test connection</Button
|
||||
>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if !emptyString(repo.git_repo_resource_path)}
|
||||
<div class="flex mb-5 text-normal text-2xs gap-1">
|
||||
{#if isRepoDuplicate(idx)}
|
||||
<span class="text-red-700">Using the same resource twice is not allowed.</span
|
||||
>
|
||||
{/if}
|
||||
{#if gitSyncTestJobs[idx].status !== undefined}
|
||||
{#if gitSyncTestJobs[idx].status === 'running'}
|
||||
<RotateCw size={14} class="animate-spin" />
|
||||
{:else if gitSyncTestJobs[idx].status === 'success'}
|
||||
<CheckCircle2 size={14} class="text-green-600" />
|
||||
{:else}
|
||||
<XCircle size={14} class="text-red-700" />
|
||||
{/if}
|
||||
Git sync resource checked via Windmill job
|
||||
<a
|
||||
target="_blank"
|
||||
href={`/run/${gitSyncTestJobs[idx].jobId}?workspace=${$workspaceStore}`}
|
||||
>
|
||||
{gitSyncTestJobs[idx].jobId}
|
||||
</a>WARNING: Only read permissions are verified.
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if repo.legacyImported}
|
||||
<Alert type="warning" title="Legacy git sync settings imported">
|
||||
This repository was initialized from workspace-level legacy Git-Sync settings. Review the filters and press <b>Save</b> to migrate.
|
||||
</Alert>
|
||||
{/if}
|
||||
<div class="flex flex-col mt-5 mb-1 gap-4">
|
||||
{#if gitSyncSettings && repo}
|
||||
{#if repo.script_path != latestGitSyncHubScript}
|
||||
<Alert type="warning" title="Script version mismatch">
|
||||
The git sync version for this repository is not latest. Current: <a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{repo.script_path}</a
|
||||
>, latest:
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://hub.windmill.dev/scripts/windmill/6943/sync-script-to-git-repo-windmill/9014/versions"
|
||||
>{latestGitSyncHubScript}</a
|
||||
>
|
||||
<div class="flex mt-2">
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
repo.script_path = latestGitSyncHubScript
|
||||
}}
|
||||
>Update git sync script (require save git settings to be applied)</Button
|
||||
>
|
||||
</div>
|
||||
</Alert>
|
||||
{/if}
|
||||
<GitSyncFilterSettings
|
||||
git_repo_resource_path={repo.git_repo_resource_path}
|
||||
bind:include_path={repo.settings.include_path}
|
||||
bind:include_type={repo.settings.include_type}
|
||||
bind:exclude_types_override={repo.exclude_types_override}
|
||||
isLegacyRepo={repo.legacyImported}
|
||||
bind:excludes={repo.settings.exclude_path}
|
||||
bind:extraIncludes={repo.settings.extra_include_path}
|
||||
bind:yamlText
|
||||
onSettingsChange={(settings) => {
|
||||
yamlText = settings.yaml
|
||||
// Force reactivity update
|
||||
repoReactivityTrigger = repoReactivityTrigger + 1
|
||||
}}
|
||||
/>
|
||||
<div class="w-1/3 flex gap-2">
|
||||
<InitGitRepoPopover
|
||||
gitRepoResourcePath={repo.git_repo_resource_path}
|
||||
uiState={{
|
||||
include_path: repo.settings.include_path,
|
||||
exclude_path: repo.settings.exclude_path || [],
|
||||
extra_include_path: repo.settings.extra_include_path || [],
|
||||
include_type: repo.settings.include_type
|
||||
}}
|
||||
/>
|
||||
<PullGitRepoPopover
|
||||
gitRepoResourcePath={repo.git_repo_resource_path}
|
||||
uiState={{
|
||||
include_path: repo.settings.include_path,
|
||||
exclude_path: repo.settings.exclude_path || [],
|
||||
extra_include_path: repo.settings.extra_include_path || [],
|
||||
include_type: repo.settings.include_type
|
||||
}}
|
||||
onFilterUpdate={(filters: { include_path: string[], exclude_path: string[], extra_include_path: string[], include_type: string[] }) => {
|
||||
// Direct prop update - much simpler!
|
||||
repo.settings.include_path = filters.include_path
|
||||
repo.settings.exclude_path = filters.exclude_path
|
||||
repo.settings.extra_include_path = filters.extra_include_path
|
||||
repo.settings.include_type = filters.include_type as ObjectType[]
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Toggle
|
||||
disabled={emptyString(repo.git_repo_resource_path)}
|
||||
bind:checked={repo.use_individual_branch}
|
||||
options={{
|
||||
right: 'Create one branch per deployed object',
|
||||
rightTooltip:
|
||||
"If set, Windmill will create a unique branch per object being pushed based on its path, prefixed with 'wm_deploy/'."
|
||||
}}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
disabled={emptyString(repo.git_repo_resource_path) ||
|
||||
!repo.use_individual_branch}
|
||||
bind:checked={repo.group_by_folder}
|
||||
options={{
|
||||
right: 'Group deployed objects by folder',
|
||||
rightTooltip:
|
||||
'Instead of creating a branch per object, Windmill will create a branch per folder containing objects being deployed.'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-tertiary text-sm mt-3 mb-2">
|
||||
Select a git repository resource to configure sync settings.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="flex mt-5 mb-5 gap-1">
|
||||
<Button
|
||||
color="none"
|
||||
variant="border"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
gitSyncSettings.repositories = [
|
||||
...gitSyncSettings.repositories,
|
||||
{
|
||||
script_path: latestGitSyncHubScript,
|
||||
git_repo_resource_path: '',
|
||||
use_individual_branch: false,
|
||||
group_by_folder: false,
|
||||
collapsed: false,
|
||||
settings: {
|
||||
include_path: [...DEFAULT_INCLUDE_PATH],
|
||||
exclude_path: [...DEFAULT_EXCLUDE_PATH],
|
||||
extra_include_path: [...DEFAULT_EXTRA_INCLUDE_PATH],
|
||||
include_type: ['script', 'flow', 'app', 'folder'] as ObjectType[]
|
||||
},
|
||||
exclude_types_override: [],
|
||||
legacyImported: false
|
||||
}
|
||||
]
|
||||
gitSyncTestJobs = [
|
||||
...gitSyncTestJobs,
|
||||
{
|
||||
jobId: undefined,
|
||||
status: undefined
|
||||
}
|
||||
]
|
||||
}}
|
||||
id="git-sync-add-connection"
|
||||
startIcon={{ icon: Plus }}
|
||||
>
|
||||
Add connection
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<Loader2 class="animate-spin mt-4" size={20} />
|
||||
{/if}
|
||||
{:else if tab == 'default_app'}
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
@@ -839,7 +1590,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert type="warning" title="Windmill EE only feature">
|
||||
<Alert type="info" title="Windmill EE only feature">
|
||||
Default app can only be set on Windmill Enterprise Edition.
|
||||
</Alert>
|
||||
{/if}
|
||||
@@ -918,7 +1669,5 @@
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.517.0"
|
||||
wmill_pg = ">=1.517.0"
|
||||
wmill = ">=1.515.1"
|
||||
wmill_pg = ">=1.515.1"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.517.0
|
||||
version: 1.515.1
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.517.0'
|
||||
ModuleVersion = '1.515.1'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.517.0"
|
||||
version = "1.515.1"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -23,7 +23,7 @@ def main [ --publish(-p) --check(-c) --test(-t) ] {
|
||||
open windmill_api/Cargo.toml
|
||||
# Use rustls - otherwise compilation will fail due to missing libssl
|
||||
| update dependencies.reqwest.features [json, multipart, rustls-tls]
|
||||
| update dependencies.reqwest.default-features false
|
||||
| insert dependencies.reqwest.default-features false
|
||||
| update package.license "Apache-2.0"
|
||||
| insert package.homepage "https://windmill.dev"
|
||||
| save -f windmill_api/Cargo.toml
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.517.0",
|
||||
"version": "1.515.1",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.517.0",
|
||||
"version": "1.515.1",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.517.0
|
||||
1.515.1
|
||||
|
||||
Reference in New Issue
Block a user