feat: add operator mode
This commit is contained in:
3
backend/migrations/20221213112420_add_operator.down.sql
Normal file
3
backend/migrations/20221213112420_add_operator.down.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE workspace_invite DROP COLUMN operator;
|
||||
ALTER TABLE workspace_settings DROP COLUMN auto_invite_operator;
|
||||
4
backend/migrations/20221213112420_add_operator.up.sql
Normal file
4
backend/migrations/20221213112420_add_operator.up.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE workspace_invite ADD COLUMN operator BOOLEAN NOT NULL DEFAULT false;
|
||||
ALTER TABLE workspace_settings ADD COLUMN auto_invite_operator BOOLEAN DEFAULT false;
|
||||
ALTER TABLE completed_job ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT 'missing@email.xyz';
|
||||
@@ -1,5 +1,19 @@
|
||||
{
|
||||
"db": "PostgreSQL",
|
||||
"006f03e979abdf8055b1c598bc9806337216a6abf74db4eb64b0acb918a0de08": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3"
|
||||
},
|
||||
"0355b53b1d45955ca56b2829372ce9c656d7f0ad7b8d0709161047f0d8cdc4f4": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -176,20 +190,6 @@
|
||||
},
|
||||
"query": "SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2"
|
||||
},
|
||||
"0a76ed47629cac693ba7f169a1229b62bd900bc007a63fbae3fa7374ba66df65": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, $3)"
|
||||
},
|
||||
"0a9a191273c735c41d56ea46a39ffca075a0550eada87df7162c5037164ad6bf": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -204,20 +204,20 @@
|
||||
},
|
||||
"query": "\n INSERT INTO capture\n (workspace_id, path, created_by)\n VALUES ($1, $2, $3)\n ON CONFLICT (workspace_id, path)\n DO UPDATE SET created_at = now()\n "
|
||||
},
|
||||
"0ae9160591ae00117d20a616cfe07e38f0c32953c7e881e916c389255190b72d": {
|
||||
"0c5faa1e3233ca15240f80c9ba805f839d9f1f7450d1ea1f2b8e9faf8533892d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin)\n VALUES ($1, $2, $3, $4)"
|
||||
"query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3 AND operator = $4"
|
||||
},
|
||||
"0d6412bc3ebb1d58bdd9cbcef774dacf9016fa402af5c1b4e339b9a3d7163d5e": {
|
||||
"describe": {
|
||||
@@ -404,6 +404,11 @@
|
||||
"name": "auto_invite_domain",
|
||||
"ordinal": 5,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "auto_invite_operator",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -412,6 +417,7 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
@@ -460,9 +466,15 @@
|
||||
"name": "is_admin",
|
||||
"ordinal": 2,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "operator",
|
||||
"ordinal": 3,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
@@ -841,33 +853,6 @@
|
||||
},
|
||||
"query": "SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 "
|
||||
},
|
||||
"2bf44d998d7acd17ec6d98f81395f8bdac49f58880fbbb9350bf0142cd2efdc7": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM workspace_invite WHERE\n workspace_id = $1 AND email = $2 AND is_admin = $3"
|
||||
},
|
||||
"2c6c2358fc24d92db179306a0f18a83d71bf15d8dc76502c451634dff35715a6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ($1, $2, false)\n ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"2e11e3ef361c41e6e055dd4805cb9d5e45eaa486e35fc5f01dae311189d6800f": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -933,6 +918,22 @@
|
||||
},
|
||||
"query": "UPDATE schedule SET enabled = false, error = $1 WHERE workspace_id = $2 AND path = $3"
|
||||
},
|
||||
"31a6e8a73fa568e28b8ca6e35a38b663e0b07d15a212b81883ad9c327d99a7c4": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin, operator)\n VALUES ($1, $2, $3, $4, $5)"
|
||||
},
|
||||
"355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1174,27 +1175,6 @@
|
||||
},
|
||||
"query": "UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3"
|
||||
},
|
||||
"4ad5fa2f08236507aad911a95697e84fc0c3a274ba0e928da28c4d146cf8f1a8": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "is_admin",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2"
|
||||
},
|
||||
"4de3ba6930b91c639f9f3939dab7b3ab35aa792484e4ccb8084c336a2ec60f45": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1467,6 +1447,11 @@
|
||||
"name": "auto_invite_domain",
|
||||
"ordinal": 5,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "auto_invite_operator",
|
||||
"ordinal": 6,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -1475,6 +1460,7 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
@@ -1656,6 +1642,18 @@
|
||||
},
|
||||
"query": "DELETE FROM group_ WHERE name = $1 AND workspace_id = $2"
|
||||
},
|
||||
"5c377fffc224a06f693c125f4c13b0a9ccfc217190ba6cf78246294bbc6c93bc": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL WHERE workspace_id = $1"
|
||||
},
|
||||
"5fee34ce48f01bb55707227548ea4d1e77ba75abfae412a902b1d8d6a1963725": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2079,6 +2077,72 @@
|
||||
},
|
||||
"query": "SELECT email, login_type::text, verified, super_admin, name, company from password LIMIT $1 OFFSET $2"
|
||||
},
|
||||
"77c3915246e37ae158d0c600e2402b93ce4fb1179d034daf231e00797e214207": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies"
|
||||
]
|
||||
},
|
||||
"name": "job_kind"
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
"Numeric",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)"
|
||||
},
|
||||
"7a511ce8dbbf761423b527672ab02156aba5594623dc269992b34398673ca387": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2111,19 +2175,6 @@
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n VALUES ('demo', $1, false)\n ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"7b0a6bf71751668eea56923f70e198c36386a82b8a8abbf4f55caeb0618478fe": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET auto_invite_domain = $1 WHERE workspace_id = $2"
|
||||
},
|
||||
"7b1239ad6460e8f5fb41bfe12f662a779528784ec8cf3f6dcce5545ab90bf234": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2182,6 +2233,20 @@
|
||||
},
|
||||
"query": "SELECT name FROM group_ WHERE workspace_id = $1 ORDER BY name desc"
|
||||
},
|
||||
"7fcffd77d0957cff4dff7aa822f7bcab9ec10563738e47ab02657baa9b29179d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, false, $3)\n ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"808ff76fdb74482d5d3201417c8a2470e2867cb08b9a46d244653eb366d8ee5e": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2705,70 +2770,26 @@
|
||||
},
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1"
|
||||
},
|
||||
"90d477b39e9d2f439d378c21041af8bc7720cc21ae150a62ecaa6235142aea05": {
|
||||
"924e1d5cbb682db0d6121dd911f38cf8948d0e59ca47428daa4f43fce686ba80": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"columns": [
|
||||
{
|
||||
"name": "is_admin",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies"
|
||||
]
|
||||
},
|
||||
"name": "job_kind"
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
"Numeric"
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)"
|
||||
"query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2 AND disabled = false"
|
||||
},
|
||||
"9251404312374679a4be2f9def508fcadce446405e91d95100c3397ec26e5882": {
|
||||
"describe": {
|
||||
@@ -2792,6 +2813,20 @@
|
||||
},
|
||||
"query": "INSERT INTO app_version\n (flow_id, value, created_by)\n VALUES ($1, $2, $3) RETURNING id"
|
||||
},
|
||||
"930ad84a4db26fa6d2c8447d447099e944a0ea7b1266b4d02cee620fe3d761a5": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2 WHERE workspace_id = $3"
|
||||
},
|
||||
"9490a4388f43e45e32911f1129e623f2d73ce2da7a948fe134bea1c87cdbefd1": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2949,6 +2984,32 @@
|
||||
},
|
||||
"query": "DELETE FROM token WHERE token = $1 RETURNING email"
|
||||
},
|
||||
"9db64c9ff790d8c833c1e831a87803c32103ce9de68cc9c08d6f56cc988d7e37": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "auto_invite_operator",
|
||||
"ordinal": 1,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT workspace_id, auto_invite_operator FROM workspace_settings WHERE auto_invite_domain = $1"
|
||||
},
|
||||
"9def921a0b697e4cbcb6ad88196ff6f66358f3b819aaf4e279596ccfc433fa92": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3375,9 +3436,15 @@
|
||||
"name": "is_admin",
|
||||
"ordinal": 2,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "operator",
|
||||
"ordinal": 3,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
@@ -3597,6 +3664,21 @@
|
||||
},
|
||||
"query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE"
|
||||
},
|
||||
"be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n VALUES ($1, $2, $3, $4)"
|
||||
},
|
||||
"bf1d8e043338867e1da1ed236ff6c85a566d5fd58d4b0d5c3a10454513811ba3": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -3641,6 +3723,20 @@
|
||||
},
|
||||
"query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6, dependency_job = NULL WHERE path = $7 AND workspace_id = $8"
|
||||
},
|
||||
"c1149f74ef0cca4a48be80f523d339d78e8e20e2bfacac9ad1b6b75c5ae370fb": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE usr SET operator = $1 WHERE username = $2 AND workspace_id = $3"
|
||||
},
|
||||
"c2849e67b9fea0dc46e6d7000f5a0c9dab89ae80a183d9255f8fdb356b4bc61c": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3734,26 +3830,6 @@
|
||||
},
|
||||
"query": "\n INSERT INTO resume_job\n (id, resume_id, job, flow, value, approver)\n VALUES ($1, $2, $3, $4, $5, $6)\n "
|
||||
},
|
||||
"c5429cd91368fcdc4da65ef6d84b32b2726f5ea938bc53bf6ed63465bd0eb0f2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT workspace_id FROM workspace_settings WHERE auto_invite_domain = $1"
|
||||
},
|
||||
"c59dd666b9a316c027e8c319b80ccbab3a220d93b64357981bd4a03324dad1d0": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3883,18 +3959,6 @@
|
||||
},
|
||||
"query": "UPDATE queue SET running = false WHERE last_ping < now() - ($1 || ' seconds')::interval AND running = true AND job_kind != $2 AND same_worker = false RETURNING id, workspace_id, last_ping"
|
||||
},
|
||||
"ccb05bf4c0a0f297f75498a684673ae7e4f6ed47f7a8f5d7154e0e0350865d41": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE workspace_settings SET auto_invite_domain = NULL WHERE workspace_id = $1"
|
||||
},
|
||||
"d0308abac80575038203b60bb66d3b39b586939da0421a595e47c7a759616431": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -3978,6 +4042,20 @@
|
||||
},
|
||||
"query": "SELECT app.id, app.path, app.summary, app.versions, app.policy,\n app.extra_perms, app_version.value, \n app_version.created_at, app_version.created_by from app, app_version \n WHERE app.path = $1 AND app.workspace_id = $2 AND app_version.id = app.versions[array_upper(app.versions, 1)]"
|
||||
},
|
||||
"d4bf68b4c2bfd11db84787e41e92c13612fda7352e841f3cdef45ea762588f04": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin, operator)\n SELECT $1::text, email, false, $3 FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING"
|
||||
},
|
||||
"d4eb7aea60894b65498144b9bf522beba612f36368d62fe4e94b5b9e26349d32": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4425,6 +4503,33 @@
|
||||
},
|
||||
"query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name, slack_email)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3, slack_email = $4"
|
||||
},
|
||||
"ec85a425f88044c6ed4f8fcea223c28eb9fb8c16c89a52d4c4552bd149badafa": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "is_admin",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "operator",
|
||||
"ordinal": 1,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator"
|
||||
},
|
||||
"ed400dbf4a1fa597bc984240cfe9069369a8a265b127116187d78c0ba93ae076": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4823,18 +4928,5 @@
|
||||
}
|
||||
},
|
||||
"query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3"
|
||||
},
|
||||
"fceeeaa8d3c97273c8e77b3badd9e7cf82deead59c0d0619e0c303ea665f7d72": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO workspace_invite\n (workspace_id, email, is_admin)\n SELECT $1::text, email, false FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (\n SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email\n )\n ON CONFLICT DO NOTHING"
|
||||
}
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
gmail.com
|
||||
gmail.com
|
||||
hotmail.com
|
||||
live.com
|
||||
yahoo.com
|
||||
protonmail.com
|
||||
yandex.com
|
||||
@@ -638,9 +638,12 @@ paths:
|
||||
type: string
|
||||
is_admin:
|
||||
type: boolean
|
||||
operator:
|
||||
type: boolean
|
||||
required:
|
||||
- email
|
||||
- is_admin
|
||||
- operator
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
@@ -669,9 +672,12 @@ paths:
|
||||
type: string
|
||||
is_admin:
|
||||
type: boolean
|
||||
operator:
|
||||
type: boolean
|
||||
required:
|
||||
- email
|
||||
- is_admin
|
||||
- operator
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
@@ -781,6 +787,8 @@ paths:
|
||||
type: string
|
||||
auto_invite_domain:
|
||||
type: string
|
||||
auto_invite_operator:
|
||||
type: boolean
|
||||
|
||||
/w/{workspace}/workspaces/edit_slack_command:
|
||||
post:
|
||||
@@ -825,10 +833,8 @@ paths:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
set:
|
||||
operator:
|
||||
type: boolean
|
||||
required:
|
||||
- set
|
||||
|
||||
responses:
|
||||
"200":
|
||||
@@ -4183,6 +4189,8 @@ components:
|
||||
enum: [python3, deno, go, bash]
|
||||
is_skipped:
|
||||
type: boolean
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- created_by
|
||||
@@ -4195,6 +4203,7 @@ components:
|
||||
- permissioned_as
|
||||
- is_flow_step
|
||||
- is_skipped
|
||||
- email
|
||||
|
||||
Job:
|
||||
allOf:
|
||||
@@ -4245,12 +4254,8 @@ components:
|
||||
Usage:
|
||||
type: object
|
||||
properties:
|
||||
duration_ms:
|
||||
type: integer
|
||||
jobs:
|
||||
type: integer
|
||||
flows:
|
||||
type: integer
|
||||
executions:
|
||||
type: number
|
||||
|
||||
Login:
|
||||
type: object
|
||||
@@ -4282,6 +4287,10 @@ components:
|
||||
properties:
|
||||
is_admin:
|
||||
type: boolean
|
||||
operator:
|
||||
type: boolean
|
||||
disabled:
|
||||
type: boolean
|
||||
|
||||
TruncatedToken:
|
||||
type: object
|
||||
@@ -4858,10 +4867,13 @@ components:
|
||||
type: string
|
||||
is_admin:
|
||||
type: boolean
|
||||
operator:
|
||||
type: boolean
|
||||
required:
|
||||
- workspace_id
|
||||
- email
|
||||
- is_admin
|
||||
- operator
|
||||
|
||||
GlobalUserInfo:
|
||||
type: object
|
||||
|
||||
@@ -238,6 +238,7 @@ pub struct CompletedJob {
|
||||
pub is_flow_step: bool,
|
||||
pub language: Option<ScriptLang>,
|
||||
pub is_skipped: bool,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -400,6 +401,7 @@ async fn list_jobs(
|
||||
"is_flow_step",
|
||||
"language",
|
||||
"false as is_skipped",
|
||||
"email",
|
||||
],
|
||||
);
|
||||
let sqlc = list_completed_jobs_query(
|
||||
@@ -432,6 +434,7 @@ async fn list_jobs(
|
||||
"is_flow_step",
|
||||
"language",
|
||||
"is_skipped",
|
||||
"email",
|
||||
],
|
||||
);
|
||||
let sql = format!(
|
||||
@@ -807,6 +810,7 @@ struct UnifiedJob {
|
||||
is_flow_step: bool,
|
||||
language: Option<ScriptLang>,
|
||||
is_skipped: bool,
|
||||
email: String,
|
||||
}
|
||||
|
||||
impl From<UnifiedJob> for Job {
|
||||
@@ -839,6 +843,7 @@ impl From<UnifiedJob> for Job {
|
||||
is_flow_step: uj.is_flow_step,
|
||||
language: uj.language,
|
||||
is_skipped: uj.is_skipped,
|
||||
email: uj.email,
|
||||
}),
|
||||
"QueuedJob" => Job::QueuedJob(QueuedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
@@ -868,7 +873,7 @@ impl From<UnifiedJob> for Job {
|
||||
language: uj.language,
|
||||
same_worker: false,
|
||||
pre_run_error: None,
|
||||
email: "".to_string(),
|
||||
email: uj.email,
|
||||
}),
|
||||
t => panic!("job type {} not valid", t),
|
||||
}
|
||||
@@ -1392,6 +1397,7 @@ async fn list_completed_jobs(
|
||||
"is_flow_step",
|
||||
"language",
|
||||
"is_skipped",
|
||||
"email",
|
||||
],
|
||||
)
|
||||
.sql()?;
|
||||
|
||||
@@ -123,7 +123,7 @@ impl AuthCache {
|
||||
let is_admin = super_admin
|
||||
|| sqlx::query_scalar!(
|
||||
"SELECT is_admin FROM usr where username = $1 AND \
|
||||
workspace_id = $2",
|
||||
workspace_id = $2 AND disabled = false",
|
||||
name,
|
||||
&w_id.as_ref().unwrap()
|
||||
)
|
||||
@@ -162,7 +162,7 @@ impl AuthCache {
|
||||
if w_id.is_some() {
|
||||
let row_o = sqlx::query_as::<_, (String, bool)>(
|
||||
"SELECT username, is_admin FROM usr where email = $1 AND \
|
||||
workspace_id = $2",
|
||||
workspace_id = $2 AND disabled = false",
|
||||
)
|
||||
.bind(&email)
|
||||
.bind(&w_id.as_ref().unwrap())
|
||||
@@ -368,9 +368,7 @@ pub struct User {
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
pub struct Usage {
|
||||
pub duration_ms: i64,
|
||||
pub jobs: i64,
|
||||
pub flows: i64,
|
||||
pub executions: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -409,6 +407,7 @@ pub struct WorkspaceInvite {
|
||||
pub workspace_id: String,
|
||||
pub email: String,
|
||||
pub is_admin: bool,
|
||||
pub operator: bool,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
@@ -465,7 +464,8 @@ pub struct EditUser {
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditWorkspaceUser {
|
||||
pub is_admin: Option<bool>,
|
||||
pub enabled: Option<bool>,
|
||||
pub operator: Option<bool>,
|
||||
pub disabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -542,14 +542,12 @@ async fn list_users(
|
||||
SELECT usr.*, usage.*
|
||||
FROM usr
|
||||
, LATERAL (
|
||||
SELECT COALESCE(SUM(duration_ms), 0) duration_ms
|
||||
, COALESCE(SUM(job_kind IN ('flow', 'flowpreview') ::int), 0) flows
|
||||
, COALESCE(SUM(job_kind NOT IN ('flow', 'flowpreview') ::int), 0) jobs
|
||||
SELECT COALESCE(SUM(duration_ms + 1000)/1000 , 0) executions
|
||||
FROM completed_job
|
||||
WHERE workspace_id = usr.workspace_id
|
||||
AND created_by = usr.username
|
||||
AND parent_job IS NULL
|
||||
AND now() - '2 week'::interval < created_at
|
||||
AND job_kind NOT IN ('flow', 'flowpreview')
|
||||
AND email = usr.email
|
||||
AND now() - '5 week'::interval < created_at
|
||||
) usage
|
||||
WHERE workspace_id = $1
|
||||
",
|
||||
@@ -863,31 +861,40 @@ async fn accept_invite(
|
||||
}
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let is_admin = sqlx::query_scalar!(
|
||||
"DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin",
|
||||
let r = sqlx::query!(
|
||||
"DELETE FROM workspace_invite WHERE workspace_id = $1 AND email = $2 RETURNING is_admin, operator",
|
||||
nu.workspace_id,
|
||||
email,
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some(is_admin) = is_admin {
|
||||
tx = add_user_to_workspace(&nu.workspace_id, &email, &nu.username, is_admin, tx).await?;
|
||||
let is_some = r.is_some();
|
||||
if let Some(r) = r {
|
||||
tx = add_user_to_workspace(
|
||||
&nu.workspace_id,
|
||||
&email,
|
||||
&nu.username,
|
||||
r.is_admin,
|
||||
r.operator,
|
||||
tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&nu.username,
|
||||
"users.accept_invite",
|
||||
ActionKind::Create,
|
||||
&nu.workspace_id,
|
||||
Some(&email),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&nu.username,
|
||||
"users.accept_invite",
|
||||
ActionKind::Create,
|
||||
&nu.workspace_id,
|
||||
Some(&email),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
if is_admin.is_some() {
|
||||
if is_some {
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!(
|
||||
@@ -905,6 +912,7 @@ async fn add_user_to_workspace<'c>(
|
||||
email: &str,
|
||||
username: &str,
|
||||
is_admin: bool,
|
||||
operator: bool,
|
||||
mut tx: sqlx::Transaction<'c, sqlx::Postgres>,
|
||||
) -> error::Result<sqlx::Transaction<'c, sqlx::Postgres>> {
|
||||
let already_exists_username = sqlx::query_scalar!(
|
||||
@@ -941,12 +949,13 @@ async fn add_user_to_workspace<'c>(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO usr
|
||||
(workspace_id, email, username, is_admin)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
(workspace_id, email, username, is_admin, operator)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
&w_id,
|
||||
email,
|
||||
username,
|
||||
is_admin
|
||||
is_admin,
|
||||
operator
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -993,6 +1002,28 @@ async fn update_workspace_user(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(a) = eu.operator {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET operator = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
&username_to_update,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(a) = eu.disabled {
|
||||
sqlx::query_scalar!(
|
||||
"UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3",
|
||||
a,
|
||||
&username_to_update,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&username,
|
||||
@@ -1118,15 +1149,15 @@ async fn create_user(
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&email,
|
||||
"users.update",
|
||||
ActionKind::Update,
|
||||
"users.add_global",
|
||||
ActionKind::Create,
|
||||
"global",
|
||||
Some(&nu.email),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
invite_user_to_all_auto_invite_worspaces(&db, &email).await?;
|
||||
invite_user_to_all_auto_invite_worspaces(&db, &nu.email).await?;
|
||||
|
||||
Ok((StatusCode::CREATED, format!("email {} created", nu.email)))
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ pub struct WorkspaceSettings {
|
||||
pub slack_command_script: Option<String>,
|
||||
pub slack_email: String,
|
||||
pub auto_invite_domain: Option<String>,
|
||||
pub auto_invite_operator: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Debug)]
|
||||
@@ -101,7 +102,7 @@ struct EditCommandScript {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditAutoInvite {
|
||||
set: bool,
|
||||
operator: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -145,6 +146,7 @@ struct ValidateUsername {
|
||||
pub struct NewWorkspaceInvite {
|
||||
pub email: String,
|
||||
pub is_admin: bool,
|
||||
pub operator: bool,
|
||||
}
|
||||
|
||||
async fn list_pending_invites(
|
||||
@@ -277,7 +279,7 @@ async fn edit_auto_invite(
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if ea.set {
|
||||
if let Some(operator) = ea.operator {
|
||||
if BANNED_DOMAINS.contains(domain) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Domain {} is not allowed",
|
||||
@@ -286,8 +288,9 @@ async fn edit_auto_invite(
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET auto_invite_domain = $1 WHERE workspace_id = $2",
|
||||
"UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2 WHERE workspace_id = $3",
|
||||
domain,
|
||||
operator,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
@@ -295,20 +298,21 @@ async fn edit_auto_invite(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin)
|
||||
SELECT $1::text, email, false FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (
|
||||
(workspace_id, email, is_admin, operator)
|
||||
SELECT $1::text, email, false, $3 FROM password WHERE email LIKE CONCAT('%', $2::text) AND NOT EXISTS (
|
||||
SELECT 1 FROM usr WHERE workspace_id = $1::text AND email = password.email
|
||||
)
|
||||
ON CONFLICT DO NOTHING",
|
||||
&w_id,
|
||||
&domain
|
||||
&domain,
|
||||
operator
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE workspace_settings SET auto_invite_domain = NULL WHERE workspace_id = $1",
|
||||
&w_id
|
||||
"UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL WHERE workspace_id = $1",
|
||||
&w_id,
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -320,7 +324,7 @@ async fn edit_auto_invite(
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&authed.email),
|
||||
Some([("set", &ea.set.to_string()[..])].into()),
|
||||
Some([("operator", &format!("{:?}", ea.operator)[..])].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -533,20 +537,21 @@ async fn delete_workspace(
|
||||
pub async fn invite_user_to_all_auto_invite_worspaces(db: &DB, email: &str) -> Result<()> {
|
||||
let mut tx = db.begin().await?;
|
||||
let domain = email.split('@').last().unwrap();
|
||||
let workspaces = sqlx::query_scalar!(
|
||||
"SELECT workspace_id FROM workspace_settings WHERE auto_invite_domain = $1",
|
||||
let workspaces = sqlx::query!(
|
||||
"SELECT workspace_id, auto_invite_operator FROM workspace_settings WHERE auto_invite_domain = $1",
|
||||
domain
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
for w in workspaces {
|
||||
for r in workspaces {
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin)
|
||||
VALUES ($1, $2, false)
|
||||
(workspace_id, email, is_admin, operator)
|
||||
VALUES ($1, $2, false, $3)
|
||||
ON CONFLICT DO NOTHING",
|
||||
w,
|
||||
r.workspace_id,
|
||||
email,
|
||||
r.auto_invite_operator
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -567,11 +572,12 @@ async fn invite_user(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace_invite
|
||||
(workspace_id, email, is_admin)
|
||||
VALUES ($1, $2, $3)",
|
||||
(workspace_id, email, is_admin, operator)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
&w_id,
|
||||
nu.email,
|
||||
nu.is_admin
|
||||
nu.is_admin,
|
||||
nu.operator
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -596,10 +602,11 @@ async fn delete_invite(
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM workspace_invite WHERE
|
||||
workspace_id = $1 AND email = $2 AND is_admin = $3",
|
||||
workspace_id = $1 AND email = $2 AND is_admin = $3 AND operator = $4",
|
||||
&w_id,
|
||||
nu.email,
|
||||
nu.is_admin
|
||||
nu.is_admin,
|
||||
nu.operator
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
@@ -126,9 +126,10 @@ pub async fn add_completed_job(
|
||||
, raw_flow
|
||||
, is_flow_step
|
||||
, is_skipped
|
||||
, language )
|
||||
, language
|
||||
, email )
|
||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, EXTRACT(milliseconds FROM (now() - $6))), $7, $8, $9,\
|
||||
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)
|
||||
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27)
|
||||
ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)",
|
||||
queued_job.workspace_id,
|
||||
queued_job.id,
|
||||
@@ -155,7 +156,8 @@ pub async fn add_completed_job(
|
||||
queued_job.is_flow_step,
|
||||
skipped,
|
||||
queued_job.language: ScriptLang,
|
||||
duration: Option<i64>
|
||||
duration: Option<i64>,
|
||||
queued_job.email
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
encodeState,
|
||||
formatCron,
|
||||
@@ -186,80 +186,84 @@
|
||||
let flowViewer: Drawer
|
||||
</script>
|
||||
|
||||
<UnsavedConfirmationModal />
|
||||
{#if !$userStore?.operator}
|
||||
<UnsavedConfirmationModal />
|
||||
|
||||
<Drawer bind:this={flowViewer} size="75%">
|
||||
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
|
||||
<div class="overflow-hidden h-full w-full">
|
||||
<FlowGraphViewer flow={$flowStore} />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Drawer bind:this={flowViewer} size="75%">
|
||||
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
|
||||
<div class="overflow-hidden h-full w-full">
|
||||
<FlowGraphViewer flow={$flowStore} />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-col flex-1 h-screen">
|
||||
<!-- Nav between steps-->
|
||||
<div
|
||||
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
<FlowImportExportMenu />
|
||||
<Button
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: faEye }}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="sm"
|
||||
on:click={flowViewer.openDrawer}
|
||||
>
|
||||
View Graph
|
||||
</Button>
|
||||
</div>
|
||||
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('settings')
|
||||
document.getElementById('path')?.focus()
|
||||
}}
|
||||
>
|
||||
{$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('settings')
|
||||
document.getElementById('flow-summary')?.focus()
|
||||
}}
|
||||
>
|
||||
<div class="max-w-[10em] !truncate">
|
||||
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-2">
|
||||
<FlowPreviewButtons />
|
||||
<div class="center-center">
|
||||
<div class="flex flex-col flex-1 h-screen">
|
||||
<!-- Nav between steps-->
|
||||
<div
|
||||
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
<FlowImportExportMenu />
|
||||
<Button
|
||||
disabled={pathError != ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: faEye }}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="sm"
|
||||
on:click={saveFlow}>Save</Button
|
||||
on:click={flowViewer.openDrawer}
|
||||
>
|
||||
View Graph
|
||||
</Button>
|
||||
</div>
|
||||
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('settings')
|
||||
document.getElementById('path')?.focus()
|
||||
}}
|
||||
>
|
||||
{$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('settings')
|
||||
document.getElementById('flow-summary')?.focus()
|
||||
}}
|
||||
>
|
||||
<div class="max-w-[10em] !truncate">
|
||||
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-2">
|
||||
<FlowPreviewButtons />
|
||||
<div class="center-center">
|
||||
<Button
|
||||
disabled={pathError != ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
size="sm"
|
||||
on:click={saveFlow}>Save</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- metadata -->
|
||||
{#if $flowStateStore}
|
||||
<FlowEditor {initialPath} {loading} />
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- metadata -->
|
||||
{#if $flowStateStore}
|
||||
<FlowEditor {initialPath} {loading} />
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
Flow Builder not available to operators
|
||||
{/if}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
company
|
||||
}
|
||||
})
|
||||
sendUserToast(`Successfully added ${email}. Welcome to them!`)
|
||||
sendUserToast(`Added ${email}`)
|
||||
dispatch('new')
|
||||
}
|
||||
</script>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="flex flex-row space-x-1">
|
||||
<input type="email" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
|
||||
|
||||
<Toggle class="mx-2" bind:checked={is_super_admin} options={{ right: 'admin' }} />
|
||||
<Toggle class="mx-2" bind:checked={is_super_admin} options={{ right: 'superadmin' }} />
|
||||
<input on:keyup={handleKeyUp} type="password" placeholder="password" bind:value={password} />
|
||||
<input type="text" on:keyup={handleKeyUp} placeholder="name" bind:value={name} />
|
||||
<input type="text" on:keyup={handleKeyUp} placeholder="company" bind:value={company} />
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { Button } from './common'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Button, ToggleButton, ToggleButtonGroup } from './common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -24,18 +24,33 @@
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
email,
|
||||
is_admin
|
||||
is_admin: selected == 'admin',
|
||||
operator: selected == 'operator'
|
||||
}
|
||||
})
|
||||
sendUserToast(`Successfully invited ${email}. Welcome to them!`)
|
||||
sendUserToast(`Invited ${email}`)
|
||||
dispatch('new')
|
||||
}
|
||||
|
||||
let selected: 'operator' | 'author' | 'admin' = 'author'
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<input type="email" on:keyup={handleKeyUp} placeholder="email" bind:value={email} class="mr-4" />
|
||||
|
||||
<Toggle bind:checked={is_admin} options={{ right: 'admin' }} />
|
||||
<ToggleButtonGroup bind:selected>
|
||||
<ToggleButton position="left" value="operator" size="sm"
|
||||
>Operator <Tooltip
|
||||
>An operator can only execute and view scripts/flows/apps from your workspace, and only
|
||||
those that he has visibility on</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<ToggleButton position="center" value="author" size="sm"
|
||||
>Author <Tooltip
|
||||
>An Author can execute and view scripts/flows/apps, but he can also create new ones</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<ToggleButton position="right" value="admin" size="sm">Admin</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
path: resourceToEdit.path,
|
||||
requestBody: { path, value: args, description }
|
||||
})
|
||||
sendUserToast(`Successfully updated resource at ${path}`)
|
||||
sendUserToast(`Updated resource at ${path}`)
|
||||
dispatch('refresh')
|
||||
drawer.closeDrawer?.()
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { page } from '$app/stores'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode, isInitialCode } from '$lib/script_helpers'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
export let script: Script
|
||||
export let initialPath: string = ''
|
||||
export let template: 'pgsql'| 'mysql' | 'script' = 'script'
|
||||
export let template: 'pgsql' | 'mysql' | 'script' = 'script'
|
||||
export let initialArgs: Record<string, any> = {}
|
||||
|
||||
const langs: [string, SupportedLanguage][] = [
|
||||
@@ -91,228 +91,232 @@
|
||||
$: kind = script.kind as 'script' | 'trigger' | 'approval' | undefined
|
||||
</script>
|
||||
|
||||
<UnsavedConfirmationModal />
|
||||
<div class="flex flex-col h-screen">
|
||||
<!-- Nav between steps-->
|
||||
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
|
||||
<div
|
||||
class="justify-between flex flex-row w-full items-center overflow-x-auto scrollbar-hidden px-2"
|
||||
>
|
||||
<div class="flex flex-row py-1">
|
||||
<Breadcrumb
|
||||
items={['Metadata', 'Code', 'Advanced']}
|
||||
selectedIndex={step}
|
||||
on:select={(e) => changeStep(e.detail.index + 1)}
|
||||
disabled={pathError != ''}
|
||||
>
|
||||
<svelte:fragment slot="separator">/</svelte:fragment>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
{#if !$userStore?.operator}
|
||||
<UnsavedConfirmationModal />
|
||||
<div class="flex flex-col h-screen">
|
||||
<!-- Nav between steps-->
|
||||
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
|
||||
<div
|
||||
class="justify-between flex flex-row w-full items-center overflow-x-auto scrollbar-hidden px-2"
|
||||
>
|
||||
<div class="flex flex-row py-1">
|
||||
<Breadcrumb
|
||||
items={['Metadata', 'Code', 'Advanced']}
|
||||
selectedIndex={step}
|
||||
on:select={(e) => changeStep(e.detail.index + 1)}
|
||||
disabled={pathError != ''}
|
||||
>
|
||||
<svelte:fragment slot="separator">/</svelte:fragment>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
await changeStep(1)
|
||||
setTimeout(() => pathC?.focus(), 100)
|
||||
}}
|
||||
>
|
||||
{script.path}
|
||||
</Button>
|
||||
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
await changeStep(1)
|
||||
setTimeout(() => pathC?.focus(), 100)
|
||||
}}
|
||||
>
|
||||
{script.path}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
await changeStep(1)
|
||||
setTimeout(() => summaryC?.focus(), 100)
|
||||
}}
|
||||
>
|
||||
<div class="max-w-[10em] !truncate">
|
||||
{script.summary == '' || !script.summary ? 'No summary' : script.summary}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-2">
|
||||
<Button
|
||||
variant="border"
|
||||
size="sm"
|
||||
btnClasses={step == 1 ? 'hidden sm:invisible' : ''}
|
||||
on:click={() => changeStep(step - 1)}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={step == 1 ? 'contained' : 'border'}
|
||||
btnClasses={step == 3 ? 'invisible' : ''}
|
||||
disabled={step === 1 && pathError !== ''}
|
||||
on:click={() => changeStep(step + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={step == 1 ? 'border' : 'contained'}
|
||||
disabled={step === 1 && pathError !== ''}
|
||||
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={editScript}>Save</Button
|
||||
>
|
||||
<Button
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
await changeStep(1)
|
||||
setTimeout(() => summaryC?.focus(), 100)
|
||||
}}
|
||||
>
|
||||
<div class="max-w-[10em] !truncate">
|
||||
{script.summary == '' || !script.summary ? 'No summary' : script.summary}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-2">
|
||||
<Button
|
||||
variant="border"
|
||||
size="sm"
|
||||
btnClasses={step == 1 ? 'hidden sm:invisible' : ''}
|
||||
on:click={() => changeStep(step - 1)}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={step == 1 ? 'contained' : 'border'}
|
||||
btnClasses={step == 3 ? 'invisible' : ''}
|
||||
disabled={step === 1 && pathError !== ''}
|
||||
on:click={() => changeStep(step + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={step == 1 ? 'border' : 'contained'}
|
||||
disabled={step === 1 && pathError !== ''}
|
||||
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={editScript}>Save</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- metadata -->
|
||||
{#if step === 1}
|
||||
<CenteredPage>
|
||||
<div class="space-y-6">
|
||||
<h2 class="border-b pb-1 mt-4">Path & Summary</h2>
|
||||
<Path
|
||||
bind:this={pathC}
|
||||
bind:error={pathError}
|
||||
bind:path={script.path}
|
||||
{initialPath}
|
||||
on:enter={() => changeStep(2)}
|
||||
namePlaceholder="my_script"
|
||||
kind="script"
|
||||
/>
|
||||
<label class="block ">
|
||||
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={summaryC}
|
||||
bind:value={script.summary}
|
||||
placeholder="A very short summary of the script displayed when the script is listed"
|
||||
rows="1"
|
||||
<!-- metadata -->
|
||||
{#if step === 1}
|
||||
<CenteredPage>
|
||||
<div class="space-y-6">
|
||||
<h2 class="border-b pb-1 mt-4">Path & Summary</h2>
|
||||
<Path
|
||||
bind:this={pathC}
|
||||
bind:error={pathError}
|
||||
bind:path={script.path}
|
||||
{initialPath}
|
||||
on:enter={() => changeStep(2)}
|
||||
namePlaceholder="my_script"
|
||||
kind="script"
|
||||
/>
|
||||
</label>
|
||||
<h2 class="border-b pb-1 mt-4">Language</h2>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each langs as [label, lang]}
|
||||
{@const isPicked = script.language == lang && template == 'script'}
|
||||
<label class="block ">
|
||||
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={summaryC}
|
||||
bind:value={script.summary}
|
||||
placeholder="A very short summary of the script displayed when the script is listed"
|
||||
rows="1"
|
||||
/>
|
||||
</label>
|
||||
<h2 class="border-b pb-1 mt-4">Language</h2>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each langs as [label, lang]}
|
||||
{@const isPicked = script.language == lang && template == 'script'}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = lang
|
||||
template = 'script'
|
||||
initContent(lang, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
|
||||
</Button>
|
||||
{/each}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
color={template == 'pgsql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = lang
|
||||
template = 'script'
|
||||
initContent(lang, script.kind, template)
|
||||
script.language = Script.language.DENO
|
||||
template = 'pgsql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
|
||||
</Button>
|
||||
{/each}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={template == 'pgsql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'pgsql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={template == 'mysql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'mysql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
|
||||
</Button>
|
||||
</div>
|
||||
<h2 class="border-b pb-1 mt-4">Advanced</h2>
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewScriptKind = !viewScriptKind)}
|
||||
>
|
||||
Tag this script as having a specific purpose inside flows
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewScriptKind}
|
||||
<div class="max-w-lg">
|
||||
<RadioButton
|
||||
label="Script Type"
|
||||
options={[
|
||||
['Common Script', Script.kind.SCRIPT],
|
||||
[
|
||||
{
|
||||
title: 'Trigger Script',
|
||||
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
|
||||
},
|
||||
Script.kind.TRIGGER
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Error Handler',
|
||||
desc: `Handle errors for flows after all retries attempts have been exhausted`
|
||||
},
|
||||
Script.kind.FAILURE
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Approval Script',
|
||||
desc: `Send notification externally to ask for approval to continue a flow`
|
||||
},
|
||||
Script.kind.APPROVAL
|
||||
]
|
||||
|
||||
// ['Command Handler', Script.kind.COMMAND]
|
||||
]}
|
||||
on:change={(e) => {
|
||||
if (isInitialCode(script.content)) {
|
||||
template = 'script'
|
||||
initContent(script.language, e.detail, template)
|
||||
}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={template == 'mysql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'mysql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
bind:value={script.kind}
|
||||
>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
|
||||
</Button>
|
||||
</div>
|
||||
<h2 class="border-b pb-1 mt-4">Advanced</h2>
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewScriptKind = !viewScriptKind)}
|
||||
>
|
||||
Tag this script as having a specific purpose inside flows
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewScriptKind}
|
||||
<div class="max-w-lg">
|
||||
<RadioButton
|
||||
label="Script Type"
|
||||
options={[
|
||||
['Common Script', Script.kind.SCRIPT],
|
||||
[
|
||||
{
|
||||
title: 'Trigger Script',
|
||||
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
|
||||
},
|
||||
Script.kind.TRIGGER
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Error Handler',
|
||||
desc: `Handle errors for flows after all retries attempts have been exhausted`
|
||||
},
|
||||
Script.kind.FAILURE
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Approval Script',
|
||||
desc: `Send notification externally to ask for approval to continue a flow`
|
||||
},
|
||||
Script.kind.APPROVAL
|
||||
]
|
||||
|
||||
// ['Command Handler', Script.kind.COMMAND]
|
||||
]}
|
||||
on:change={(e) => {
|
||||
if (isInitialCode(script.content)) {
|
||||
template = 'script'
|
||||
initContent(script.language, e.detail, template)
|
||||
}
|
||||
}}
|
||||
bind:value={script.kind}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ml-3">
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="ml-3">
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CenteredPage>
|
||||
{:else if step === 2}
|
||||
<ScriptEditor
|
||||
bind:schema={script.schema}
|
||||
path={script.path}
|
||||
bind:code={script.content}
|
||||
lang={script.language}
|
||||
{initialArgs}
|
||||
{kind}
|
||||
/>
|
||||
{:else if step === 3}
|
||||
<CenteredPage>
|
||||
<ScriptSchema
|
||||
bind:summary={script.summary}
|
||||
bind:description={script.description}
|
||||
</CenteredPage>
|
||||
{:else if step === 2}
|
||||
<ScriptEditor
|
||||
bind:schema={script.schema}
|
||||
path={script.path}
|
||||
bind:code={script.content}
|
||||
lang={script.language}
|
||||
{initialArgs}
|
||||
{kind}
|
||||
/>
|
||||
</CenteredPage>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if step === 3}
|
||||
<CenteredPage>
|
||||
<ScriptSchema
|
||||
bind:summary={script.summary}
|
||||
bind:description={script.description}
|
||||
bind:schema={script.schema}
|
||||
/>
|
||||
</CenteredPage>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
Script Builder not available to operators
|
||||
{/if}
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
let users: GlobalUserInfo[] = []
|
||||
let filteredUsers: GlobalUserInfo[] = []
|
||||
|
||||
let deleteConfirmedCallback: (() => void) | undefined = undefined
|
||||
$: openConfirmation = Boolean(deleteConfirmedCallback)
|
||||
|
||||
async function loadVersion(): Promise<void> {
|
||||
version = await SettingsService.backendVersion()
|
||||
}
|
||||
@@ -47,39 +44,13 @@
|
||||
|
||||
<Drawer bind:this={drawer} on:open={listUsers} size="900px">
|
||||
<DrawerContent overflow_y={false} title="Superadmin Settings" on:close={drawer.closeDrawer}>
|
||||
<ConfirmationModal
|
||||
open={openConfirmation}
|
||||
title="Remove user"
|
||||
confirmationText="Remove"
|
||||
on:canceled={() => {
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
if (deleteConfirmedCallback) {
|
||||
deleteConfirmedCallback()
|
||||
}
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>Are you sure you want to remove this user?</span>
|
||||
<Alert type="info" title="Bypass confirmation">
|
||||
<div>
|
||||
You can press
|
||||
<Badge color="dark-gray">SHIFT</Badge>
|
||||
while removing a variable to bypass confirmation.
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div>
|
||||
<div class="text-xs pt-1 text-gray-500 ">
|
||||
Windmill {version}
|
||||
</div>
|
||||
|
||||
<PageHeader title="All users" primary={false} />
|
||||
<PageHeader title="All global users" primary={false} />
|
||||
|
||||
<div class="pb-1" />
|
||||
<InviteGlobalUser on:new={listUsers} />
|
||||
@@ -91,21 +62,23 @@
|
||||
<TableCustom>
|
||||
<tr slot="header-row" class="sticky top-0 bg-white border-b">
|
||||
<th>email</th>
|
||||
<th>superadmin</th>
|
||||
<th>auth</th>
|
||||
<th>name</th>
|
||||
<th>company</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body" class="overflow-y-auto h-full max-h-full">
|
||||
{#if filteredUsers && users}
|
||||
{#each filteredUsers as { email, super_admin, login_type, name, company } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{super_admin ? 'yes' : ''}</td>
|
||||
<td>{login_type}</td>
|
||||
<td><span class="break-words">{name ?? ''}</span></td>
|
||||
<td><span class="break-words">{company ?? ''}</span></td>
|
||||
<td
|
||||
>{#if super_admin}<Badge>Superadmin</Badge>{/if}</td
|
||||
>
|
||||
<td>
|
||||
<div class="flex flex-row gap-x-1">
|
||||
<button
|
||||
@@ -119,18 +92,15 @@
|
||||
})
|
||||
sendUserToast('User updated')
|
||||
listUsers()
|
||||
}}>{super_admin ? 'demote' : 'promote'}</button
|
||||
}}>{super_admin ? 'non-superadmin' : 'superadmin'}</button
|
||||
>
|
||||
|
|
||||
<button
|
||||
class="text-red-500"
|
||||
on:click={async () => {
|
||||
deleteConfirmedCallback = async () => {
|
||||
await UserService.globalUserDelete({ email })
|
||||
sendUserToast(`User ${email} removed`)
|
||||
await listUsers()
|
||||
}
|
||||
openConfirmation = false
|
||||
await UserService.globalUserDelete({ email })
|
||||
sendUserToast(`User ${email} removed`)
|
||||
listUsers()
|
||||
}}>remove</button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
description: variable.description
|
||||
}
|
||||
})
|
||||
sendUserToast(`Successfully created variable ${path}`)
|
||||
sendUserToast(`Created variable ${path}`)
|
||||
dispatch('create')
|
||||
drawer.closeDrawer()
|
||||
}
|
||||
@@ -115,7 +115,7 @@
|
||||
description: getV.description != variable.description ? variable.description : undefined
|
||||
}
|
||||
})
|
||||
sendUserToast(`Successfully updated variable at ${initialPath}`)
|
||||
sendUserToast(`Updated variable ${initialPath}`)
|
||||
dispatch('create')
|
||||
drawer.closeDrawer()
|
||||
} catch (err) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import ContextPanel from './contextPanel/ContextPanel.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import AppPreview from './AppPreview.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
export let app: App
|
||||
export let path: string
|
||||
@@ -67,63 +68,67 @@
|
||||
$: width = $breakpoint === 'sm' ? 'w-[640px]' : 'w-full '
|
||||
</script>
|
||||
|
||||
{#if initialMode !== 'preview'}
|
||||
<AppEditorHeader bind:title={$appStore.title} bind:mode={$mode} bind:breakpoint={$breakpoint} />
|
||||
{/if}
|
||||
{#if !$userStore?.operator}
|
||||
{#if initialMode !== 'preview'}
|
||||
<AppEditorHeader bind:title={$appStore.title} bind:mode={$mode} bind:breakpoint={$breakpoint} />
|
||||
{/if}
|
||||
|
||||
{#if previewing}
|
||||
<AppPreview app={$appStore} />
|
||||
{#if previewing}
|
||||
<AppPreview app={$appStore} />
|
||||
{:else}
|
||||
<SplitPanesWrapper class="max-w-full overflow-hidden">
|
||||
<Pane size={20} minSize={15} maxSize={40}>
|
||||
<ContextPanel appPath={path} />
|
||||
</Pane>
|
||||
<Pane size={60}>
|
||||
<div class="p-4 bg-gray-100 min-h-full w-full relative">
|
||||
{#if $appStore.grid}
|
||||
<div class={classNames('mx-auto h-full', width)}>
|
||||
<GridEditor />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $connectingInput.opened}
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={30} minSize={25} maxSize={40}>
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
<Tab value="insert" size="xs">
|
||||
<div class="m-1 flex flex-row gap-2">
|
||||
<Icon data={faPlus} />
|
||||
<span>Insert</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="settings" size="xs">
|
||||
<div class="m-1 flex flex-row gap-2">
|
||||
<Icon data={faSliders} />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
{#each $appStore.grid as gridItem (gridItem.id)}
|
||||
{#if gridItem.data.id === $selectedComponent}
|
||||
<ComponentPanel bind:component={gridItem.data} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{#if $selectedComponent === undefined}
|
||||
<div class="p-4 text-sm">No component selected.</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
<TabContent value="insert">
|
||||
<ComponentList />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</Pane>
|
||||
</SplitPanesWrapper>
|
||||
{/if}
|
||||
{:else}
|
||||
<SplitPanesWrapper class="max-w-full overflow-hidden">
|
||||
<Pane size={20} minSize={15} maxSize={40}>
|
||||
<ContextPanel appPath={path} />
|
||||
</Pane>
|
||||
<Pane size={60}>
|
||||
<div class="p-4 bg-gray-100 min-h-full w-full relative">
|
||||
{#if $appStore.grid}
|
||||
<div class={classNames('mx-auto h-full', width)}>
|
||||
<GridEditor />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $connectingInput.opened}
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full bg-black border-2 bg-opacity-25 z-1 flex justify-center items-center"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={30} minSize={25} maxSize={40}>
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
<Tab value="insert" size="xs">
|
||||
<div class="m-1 flex flex-row gap-2">
|
||||
<Icon data={faPlus} />
|
||||
<span>Insert</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="settings" size="xs">
|
||||
<div class="m-1 flex flex-row gap-2">
|
||||
<Icon data={faSliders} />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
{#each $appStore.grid as gridItem (gridItem.id)}
|
||||
{#if gridItem.data.id === $selectedComponent}
|
||||
<ComponentPanel bind:component={gridItem.data} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{#if $selectedComponent === undefined}
|
||||
<div class="p-4 text-sm">No component selected.</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
<TabContent value="insert">
|
||||
<ComponentList />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</Pane>
|
||||
</SplitPanesWrapper>
|
||||
App editor not available to operators
|
||||
{/if}
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
sendUserToast('Saved successfully.')
|
||||
sendUserToast('Saved')
|
||||
})
|
||||
.catch(() => {
|
||||
sendUserToast('Error during saving. Please try again later.', true)
|
||||
sendUserToast('Error during saving. Please try again later', true)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.save = false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import { AppService, type ListableApp } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { faEdit, faEye, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
@@ -32,18 +32,20 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/apps/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{#if !$userStore?.operator}
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/apps/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
href="/apps/get/{path}"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type ShareModal from '$lib/components/ShareModal.svelte'
|
||||
|
||||
import { FlowService, type Flow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import {
|
||||
faArchive,
|
||||
@@ -34,7 +34,7 @@
|
||||
try {
|
||||
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
|
||||
dispatch('change')
|
||||
sendUserToast(`Successfully archived flow ${path}`)
|
||||
sendUserToast(`Archived flow ${path}`)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not archive this flow ${err.body}`, true)
|
||||
}
|
||||
@@ -56,30 +56,32 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/flows/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/flows/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{#if !$userStore?.operator}
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/flows/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/flows/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type ShareModal from '$lib/components/ShareModal.svelte'
|
||||
|
||||
import { ScriptService, type Script } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { capitalize, sendUserToast } from '$lib/utils'
|
||||
import {
|
||||
faArchive,
|
||||
@@ -44,7 +44,7 @@
|
||||
async function archiveScript(path: string): Promise<void> {
|
||||
await ScriptService.archiveScriptByPath({ workspace: $workspaceStore!, path })
|
||||
dispatch('change')
|
||||
sendUserToast(`Successfully archived script ${path}`)
|
||||
sendUserToast(`Archived script ${path}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -74,30 +74,32 @@
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/scripts/edit/{hash}?step=2"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/scripts/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{#if !$userStore?.operator}
|
||||
{#if canWrite}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/scripts/edit/{hash}?step=2"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
href="/scripts/add?template={path}"
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
@@ -128,6 +130,7 @@
|
||||
icon: faEye,
|
||||
href: `/scripts/get/${hash}`
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(script.path, script.extra_perms, $userStore) &&
|
||||
script.workspace_id == $workspaceStore,
|
||||
script.workspace_id == $workspaceStore &&
|
||||
!$userStore?.operator,
|
||||
...script
|
||||
}
|
||||
})
|
||||
@@ -66,7 +67,10 @@
|
||||
async function loadFlows(): Promise<void> {
|
||||
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
|
||||
return {
|
||||
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
|
||||
canWrite:
|
||||
canWrite(x.path, x.extra_perms, $userStore) &&
|
||||
x.workspace_id == $workspaceStore &&
|
||||
!$userStore?.operator,
|
||||
...x
|
||||
}
|
||||
})
|
||||
@@ -77,7 +81,9 @@
|
||||
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((app: ListableApp) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(app.path!, app.extra_perms!, $userStore) && app.workspace_id == $workspaceStore,
|
||||
canWrite(app.path!, app.extra_perms!, $userStore) &&
|
||||
app.workspace_id == $workspaceStore &&
|
||||
!$userStore?.operator,
|
||||
...app
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,21 +18,21 @@
|
||||
const mainMenuLinks = [
|
||||
{ label: 'Home', href: '/', icon: faHomeAlt },
|
||||
{ label: 'Runs', href: '/runs', icon: faPlay },
|
||||
{ label: 'Variables', href: '/variables', icon: faDollarSign },
|
||||
{ label: 'Resources', href: '/resources', icon: faCubes }
|
||||
{ label: 'Variables', href: '/variables', icon: faDollarSign, disabled: $userStore?.operator },
|
||||
{ label: 'Resources', href: '/resources', icon: faCubes, disabled: $userStore?.operator }
|
||||
]
|
||||
|
||||
$: secondaryMenuLinks = [
|
||||
{ label: 'Schedules', href: '/schedules', icon: faCalendar },
|
||||
{ label: 'Groups', href: '/groups', icon: faUsersCog },
|
||||
{ label: 'Audit Logs', href: '/audit_logs', icon: faEye },
|
||||
{ label: 'Schedules', href: '/schedules', icon: faCalendar, disabled: $userStore?.operator },
|
||||
{ label: 'Groups', href: '/groups', icon: faUsersCog, disabled: $userStore?.operator },
|
||||
{ label: 'Audit Logs', href: '/audit_logs', icon: faEye, disabled: $userStore?.operator },
|
||||
{
|
||||
label: 'Workspace',
|
||||
href: '/workspace_settings',
|
||||
icon: faCog,
|
||||
disabled: !($userStore?.is_admin || $superadmin)
|
||||
},
|
||||
{ label: 'Workers', href: '/workers', icon: faRobot }
|
||||
{ label: 'Workers', href: '/workers', icon: faRobot, disabled: $userStore?.operator }
|
||||
]
|
||||
|
||||
const thirdMenuLinks = [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { userStore, usersWorkspaceStore, superadmin, usageStore } from '$lib/stores'
|
||||
import { classNames, isCloudHosted } from '$lib/utils'
|
||||
import { faCrown, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faCrown, faPlay, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import Icon from 'svelte-awesome'
|
||||
@@ -31,6 +31,8 @@
|
||||
{$userStore?.username ?? ($superadmin ? $superadmin : '___')}
|
||||
{#if $userStore?.is_admin || $superadmin}
|
||||
<Icon data={faCrown} scale={0.6} />
|
||||
{:else if $userStore?.operator}
|
||||
<Icon class="ml-2" data={faPlay} scale={0.8} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -42,7 +44,15 @@
|
||||
<p class="text-sm font-medium text-gray-900 truncate" role="none">
|
||||
{$usersWorkspaceStore?.email}
|
||||
</p>
|
||||
<span class="text-xs text-gray-600">
|
||||
{#if $userStore?.is_admin}
|
||||
Admin of this workspace <Icon data={faCrown} scale={0.6} />
|
||||
{:else if $userStore?.operator}
|
||||
Operator in this workspace <Icon class="ml-2" data={faPlay} scale={0.8} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
on:click={() => dispatch('user-settings')}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface UserExt {
|
||||
username: string;
|
||||
is_admin: boolean;
|
||||
is_super_admin: boolean;
|
||||
operator: boolean;
|
||||
created_at: string;
|
||||
groups: string[];
|
||||
pgroups: string[];
|
||||
|
||||
@@ -157,25 +157,27 @@
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/edit/{path}"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/add?template={flow.path}"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Use as template/Fork
|
||||
</Button>
|
||||
{#if !$userStore?.operator}
|
||||
<Button
|
||||
href="/flows/edit/{path}"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
href="/flows/add?template={flow.path}"
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Use as template/Fork
|
||||
</Button>
|
||||
{/if}
|
||||
<Button href="/runs/{flow.path}" color="blue" size="xs" startIcon={{ icon: faList }}>
|
||||
View runs
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, type OpenFlow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { Alert, Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import CreateActionsFlow from '$lib/components/flows/CreateActionsFlow.svelte'
|
||||
@@ -130,32 +130,36 @@
|
||||
{/if}
|
||||
<PageHeader title="Home">
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
<CreateApp />
|
||||
<CreateActionsScript />
|
||||
<CreateActionsFlow />
|
||||
{#if !$userStore?.operator}
|
||||
<CreateApp />
|
||||
<CreateActionsScript />
|
||||
<CreateActionsFlow />
|
||||
{/if}
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab size="md" value="workspace">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size={18} />
|
||||
Workspace
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="md" value="hubscripts">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Globe2 size={18} />
|
||||
Hub Scripts
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="md" value="hubflows">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Globe2 size={18} />
|
||||
Hub Flows
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{#if !$userStore?.operator}
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab size="md" value="workspace">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size={18} />
|
||||
Workspace
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="md" value="hubscripts">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Globe2 size={18} />
|
||||
Hub Scripts
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="md" value="hubflows">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Globe2 size={18} />
|
||||
Hub Flows
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{/if}
|
||||
<div class="my-2" />
|
||||
<div class="flex flex-col gap-y-16">
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -193,14 +193,16 @@
|
||||
size="xs"
|
||||
startIcon={{ icon: faRefresh }}>Run again</Button
|
||||
>
|
||||
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
|
||||
<Button
|
||||
disabled={not_same_workspace}
|
||||
href={editHref}
|
||||
color="blue"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}>Edit</Button
|
||||
>
|
||||
{#if !$userStore?.operator}
|
||||
{#if canWrite(job?.script_path ?? '', {}, $userStore)}
|
||||
<Button
|
||||
disabled={not_same_workspace}
|
||||
href={editHref}
|
||||
color="blue"
|
||||
size="xs"
|
||||
startIcon={{ icon: faEdit }}>Edit</Button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
disabled={not_same_workspace}
|
||||
|
||||
@@ -157,24 +157,26 @@
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
href={`/scripts/edit/${script.hash}?step=2`}
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faEdit }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
{#if !topHash}
|
||||
{#if !$userStore?.operator}
|
||||
<Button
|
||||
href={`/scripts/add?template=${script.path}`}
|
||||
variant="border"
|
||||
href={`/scripts/edit/${script.hash}?step=2`}
|
||||
color="blue"
|
||||
size="md"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
startIcon={{ icon: faEdit }}
|
||||
disabled={!can_write}
|
||||
>
|
||||
Fork
|
||||
Edit
|
||||
</Button>
|
||||
{#if !topHash}
|
||||
<Button
|
||||
href={`/scripts/add?template=${script.path}`}
|
||||
variant="border"
|
||||
size="md"
|
||||
startIcon={{ icon: faCodeFork }}
|
||||
>
|
||||
Fork
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,8 +69,7 @@
|
||||
requestBody: args,
|
||||
scheduledFor
|
||||
})
|
||||
sendUserToast(`Job <a href='/run/${run}?workspace=${$workspaceStore}'>${run}</a> started`)
|
||||
goto('/run/' + run + '?workspace=' + $workspaceStore)
|
||||
await goto('/run/' + run + '?workspace=' + $workspaceStore)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not create job: ${err.body}`, true)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import CenteredModal from '$lib/components/CenteredModal.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
const rd = $page.url.searchParams.get('rd')
|
||||
|
||||
@@ -45,10 +46,10 @@
|
||||
if (auto_invite) {
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: id,
|
||||
requestBody: { set: true }
|
||||
requestBody: { operator: operatorOnly }
|
||||
})
|
||||
}
|
||||
sendUserToast(`Successfully created workspace id: ${id}`)
|
||||
sendUserToast(`Created workspace id: ${id}`)
|
||||
|
||||
usersWorkspaceStore.set(await WorkspaceService.listUserWorkspaces())
|
||||
workspaceStore.set(id)
|
||||
@@ -94,6 +95,7 @@
|
||||
$: domain = $usersWorkspaceStore?.email.split('@')[1]
|
||||
|
||||
let auto_invite = false
|
||||
let operatorOnly = false
|
||||
</script>
|
||||
|
||||
<CenteredModal title="New Workspace">
|
||||
@@ -120,6 +122,16 @@
|
||||
bind:checked={auto_invite}
|
||||
options={{ right: `Auto invite users with the same domain (${domain})` }}
|
||||
/>
|
||||
{#if auto_invite}
|
||||
<Toggle
|
||||
bind:checked={operatorOnly}
|
||||
options={{ right: `Auto invited users to join as operators` }}
|
||||
/>
|
||||
<Tooltip
|
||||
>An operator can only execute and view scripts/flows/apps from your workspace, and only those
|
||||
that he has visibility on</Tooltip
|
||||
>
|
||||
{/if}
|
||||
{#if !isDomainAllowed}
|
||||
<div class="text-gray-600 text-sm mb-4 mt-2">{domain} domain not allowed for auto-invite</div>
|
||||
{/if}
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
<span class="font-mono font-semibold">{invite.workspace_id}</span>
|
||||
{#if invite.is_admin}
|
||||
<span class="text-sm">as an admin</span>
|
||||
{:else if invite.operator}
|
||||
<span class="text-sm">as an operator</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end items-center flex-col sm:flex-row gap-1">
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { faScroll, faBarsStaggered } from '@fortawesome/free-solid-svg-icons'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton/ToggleButton.svelte'
|
||||
|
||||
let users: User[] | undefined = undefined
|
||||
let invites: WorkspaceInvite[] = []
|
||||
@@ -38,6 +41,7 @@
|
||||
let team_name: string | undefined
|
||||
let auto_invite_domain: string | undefined
|
||||
let itemKind: 'flow' | 'script' = 'flow'
|
||||
let operatorOnly: boolean | undefined = undefined
|
||||
|
||||
// function getDropDownItems(username: string): DropdownItem[] {
|
||||
// return [
|
||||
@@ -76,6 +80,7 @@
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
team_name = settings.slack_name
|
||||
auto_invite_domain = settings.auto_invite_domain
|
||||
operatorOnly = settings.auto_invite_operator
|
||||
scriptPath = (settings.slack_command_script ?? '').split('/').slice(1).join('/')
|
||||
initialPath = scriptPath
|
||||
}
|
||||
@@ -104,6 +109,34 @@
|
||||
loadSettings()
|
||||
}
|
||||
}
|
||||
|
||||
$: operatorOnly != undefined && auto_invite_domain && setOperatorOnly()
|
||||
|
||||
async function removeAllInvitesFromDomain() {
|
||||
await Promise.all(
|
||||
invites
|
||||
.filter((x) => x.email.endsWith('@' + auto_invite_domain ?? ''))
|
||||
.map(({ email, is_admin, operator }) =>
|
||||
WorkspaceService.deleteInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: {
|
||||
email,
|
||||
is_admin,
|
||||
operator
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
async function setOperatorOnly() {
|
||||
await removeAllInvitesFromDomain()
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { operator: operatorOnly }
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
@@ -127,47 +160,94 @@
|
||||
<tr slot="header-row">
|
||||
<th>email</th>
|
||||
<th>username</th>
|
||||
<th>role</th>
|
||||
<th colspan="3">jobs & flows (<abbr title="past two weeks">2w</abbr>)</th>
|
||||
<th
|
||||
>executions (<abbr title="past 5 weeks">5w</abbr>) <Tooltip
|
||||
>An execution is calculated as 1 for any runs of scripts + 1 for each seconds above
|
||||
the first one</Tooltip
|
||||
>
|
||||
</th>
|
||||
<th />
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if filteredUsers}
|
||||
{#each filteredUsers as { email, username, is_admin, usage } (email)}
|
||||
{#each filteredUsers as { email, username, is_admin, operator, usage, disabled } (email)}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{username}</td>
|
||||
<td>{is_admin ? 'admin' : 'user'}</td>
|
||||
<td>{usage?.jobs}</td>
|
||||
<td>{usage?.flows}</td>
|
||||
<td>{msToSec(usage?.duration_ms)}s</td>
|
||||
<td class="whitespace-nowrap"
|
||||
><button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
await UserService.deleteUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username
|
||||
})
|
||||
sendUserToast('User removed')
|
||||
listUsers()
|
||||
}}>remove</button
|
||||
>
|
||||
-
|
||||
<button
|
||||
class="text-blue-500"
|
||||
on:click={async () => {
|
||||
await UserService.updateUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username,
|
||||
requestBody: {
|
||||
is_admin: !is_admin
|
||||
}
|
||||
})
|
||||
sendUserToast('User updated')
|
||||
listUsers()
|
||||
}}>{is_admin ? 'demote' : 'promote'}</button
|
||||
<td>{usage?.executions}</td>
|
||||
<td
|
||||
><div class="flex gap-1"
|
||||
>{#if disabled}
|
||||
<Badge color="red">disabled</Badge>
|
||||
{/if}</div
|
||||
></td
|
||||
>
|
||||
<td>
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={is_admin ? 'admin' : operator ? 'operator' : 'author'}
|
||||
on:selected={async (e) => {
|
||||
const body =
|
||||
e.detail == 'admin'
|
||||
? { is_admin: true, operator: false }
|
||||
: e.detail == 'operator'
|
||||
? { is_admin: false, operator: true }
|
||||
: { is_admin: false, operator: false }
|
||||
await UserService.updateUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username,
|
||||
requestBody: body
|
||||
})
|
||||
listUsers()
|
||||
}}
|
||||
>
|
||||
<ToggleButton position="left" value="operator" size="xs"
|
||||
>Operator <Tooltip
|
||||
>An operator can only execute and view scripts/flows/apps from your
|
||||
workspace, and only those that he has visibility on</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<ToggleButton position="center" value="author" size="xs"
|
||||
>Author <Tooltip
|
||||
>An Author can execute and view scripts/flows/apps, but he can also create
|
||||
new ones</Tooltip
|
||||
></ToggleButton
|
||||
>
|
||||
<ToggleButton position="right" value="admin" size="xs">Admin</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="text-blue-500"
|
||||
on:click={async () => {
|
||||
await UserService.updateUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username,
|
||||
requestBody: {
|
||||
disabled: !disabled
|
||||
}
|
||||
})
|
||||
listUsers()
|
||||
}}>{disabled ? 'enable' : 'disable'}</button
|
||||
>
|
||||
|
|
||||
<button
|
||||
class="text-red-500"
|
||||
on:click={async () => {
|
||||
await UserService.deleteUser({
|
||||
workspace: $workspaceStore ?? '',
|
||||
username
|
||||
})
|
||||
sendUserToast('User removed')
|
||||
listUsers()
|
||||
}}>remove</button
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
@@ -196,10 +276,12 @@
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each invites as { email, is_admin }}
|
||||
{#each invites as { email, is_admin, operator }}
|
||||
<tr class="border">
|
||||
<td>{email}</td>
|
||||
<td>{is_admin ? 'admin' : 'user'}</td>
|
||||
<td
|
||||
>{#if operator}<Badge>operator</Badge>{:else if is_admin}<Badge>admin</Badge>{/if}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
@@ -208,11 +290,12 @@
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: {
|
||||
email,
|
||||
is_admin
|
||||
is_admin,
|
||||
operator
|
||||
}
|
||||
})
|
||||
listInvites()
|
||||
}}>remove</button
|
||||
}}>cancel</button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
@@ -224,29 +307,42 @@
|
||||
<PageHeader title="Auto Invite" primary={false} />
|
||||
<div class="flex gap-2">
|
||||
{#if auto_invite_domain != domain}
|
||||
<Button
|
||||
disabled={!allowedAutoDomain}
|
||||
on:click={async () => {
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { set: true }
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
}}>Set auto-invite to {domain}</Button
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
disabled={!allowedAutoDomain}
|
||||
on:click={async () => {
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { operator: false }
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
}}>Set auto-invite to {domain}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if auto_invite_domain}
|
||||
<Button
|
||||
on:click={async () => {
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { set: false }
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
}}>Unset auto-invite from {auto_invite_domain}</Button
|
||||
>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<Toggle
|
||||
bind:checked={operatorOnly}
|
||||
options={{
|
||||
right: `Auto-invited users to join as operators`
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
on:click={async () => {
|
||||
await removeAllInvitesFromDomain()
|
||||
await WorkspaceService.editAutoInvite({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { operator: undefined }
|
||||
})
|
||||
loadSettings()
|
||||
listInvites()
|
||||
}}>Unset auto-invite from {auto_invite_domain} domain</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !allowedAutoDomain}
|
||||
@@ -334,7 +430,7 @@
|
||||
btnClasses="mt-2"
|
||||
on:click={async () => {
|
||||
await WorkspaceService.deleteWorkspace({ workspace: $workspaceStore ?? '' })
|
||||
sendUserToast(`Successfully deleted workspace ${$workspaceStore}`)
|
||||
sendUserToast(`Deleted workspace ${$workspaceStore}`)
|
||||
workspaceStore.set(undefined)
|
||||
usersWorkspaceStore.set(undefined)
|
||||
goto('/user/workspaces')
|
||||
|
||||
Reference in New Issue
Block a user