Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94e0c38191 | |||
| 6cc3700d29 | |||
| bb313fd9dc | |||
| 5ef8621db7 | |||
| 6cd348ca28 | |||
| 3183f9e909 | |||
| ff7004412b | |||
| f07bcc4660 | |||
| d773e13aab | |||
| dc0d128892 | |||
| 124c4ca46f | |||
| 5d281d9b6c | |||
| 5b37bb5b11 | |||
| fd1da01a3f | |||
| 6a447369f8 | |||
| 01d877f7ed | |||
| 73505d1ed8 | |||
| 766191899c | |||
| 38e8b4086d | |||
| ce047d1bb0 | |||
| 4e38d2ff43 | |||
| e19639c9be |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-09-08 - 5.3.0 - feat(web)
|
||||||
|
Add deployments API typings and web UI improvements: services & deployments management with CRUD and actions
|
||||||
|
|
||||||
|
- Add deployment request interfaces (ts_interfaces/requests/deployment.ts) to define typed API for create/read/update/delete/scale/restart operations.
|
||||||
|
- Extend web app state (ts_web/appstate.ts) to include typed services and deployments, and add actions for create/update/delete of services and deployments.
|
||||||
|
- Enhance web views (ts_web/elements/*): CloudlyViewServices and CloudlyViewDeployments now include richer display, styling, and UI actions (create, edit, deploy, restart, stop, delete).
|
||||||
|
- Fix subscription variable naming in several web components (subecription -> subscription) and improve table display functions to handle missing data safely.
|
||||||
|
- Add .claude/settings.local.json (tooling/permissions) used for local development/test tooling.
|
||||||
|
|
||||||
## 2025-09-07 - 5.2.0 - feat(settings)
|
## 2025-09-07 - 5.2.0 - feat(settings)
|
||||||
Add runtime settings management, node & baremetal managers, and settings UI
|
Add runtime settings management, node & baremetal managers, and settings UI
|
||||||
|
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/cloudly",
|
"name": "@serve.zone/cloudly",
|
||||||
"version": "5.2.0",
|
"version": "5.3.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@git.zone/tsbundle": "^2.5.1",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsdoc": "^1.5.2",
|
"@git.zone/tsdoc": "^1.5.2",
|
||||||
"@git.zone/tspublish": "^1.10.3",
|
"@git.zone/tspublish": "^1.10.3",
|
||||||
"@git.zone/tstest": "^2.3.6",
|
"@git.zone/tstest": "^2.3.8",
|
||||||
"@git.zone/tswatch": "^2.2.1",
|
"@git.zone/tswatch": "^2.2.1",
|
||||||
"@types/node": "^22.0.0"
|
"@types/node": "^22.0.0"
|
||||||
},
|
},
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"@apiclient.xyz/docker": "^1.3.5",
|
"@apiclient.xyz/docker": "^1.3.5",
|
||||||
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
||||||
"@apiclient.xyz/slack": "^3.0.9",
|
"@apiclient.xyz/slack": "^3.0.9",
|
||||||
"@design.estate/dees-catalog": "^1.11.2",
|
"@design.estate/dees-catalog": "^1.11.3",
|
||||||
"@design.estate/dees-domtools": "^2.3.3",
|
"@design.estate/dees-domtools": "^2.3.3",
|
||||||
"@design.estate/dees-element": "^2.1.2",
|
"@design.estate/dees-element": "^2.1.2",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"@push.rocks/smartexpect": "^2.5.0",
|
"@push.rocks/smartexpect": "^2.5.0",
|
||||||
"@push.rocks/smartfile": "^11.2.7",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjson": "^5.0.19",
|
"@push.rocks/smartjson": "^5.2.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.1.9",
|
"@push.rocks/smartlog": "^3.1.9",
|
||||||
"@push.rocks/smartlog-destination-clickhouse": "^1.0.13",
|
"@push.rocks/smartlog-destination-clickhouse": "^1.0.13",
|
||||||
@@ -67,9 +67,9 @@
|
|||||||
"@push.rocks/smartrequest": "^4.3.1",
|
"@push.rocks/smartrequest": "^4.3.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartssh": "^2.0.1",
|
"@push.rocks/smartssh": "^2.0.1",
|
||||||
"@push.rocks/smartstate": "^2.0.26",
|
"@push.rocks/smartstate": "^2.0.27",
|
||||||
"@push.rocks/smartstream": "^3.2.5",
|
"@push.rocks/smartstream": "^3.2.5",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.1.0",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/taskbuffer": "^3.4.0",
|
"@push.rocks/taskbuffer": "^3.4.0",
|
||||||
"@push.rocks/webjwt": "^1.0.9",
|
"@push.rocks/webjwt": "^1.0.9",
|
||||||
|
|||||||
273
pnpm-lock.yaml
generated
273
pnpm-lock.yaml
generated
@@ -33,8 +33,8 @@ importers:
|
|||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
'@design.estate/dees-catalog':
|
'@design.estate/dees-catalog':
|
||||||
specifier: ^1.11.2
|
specifier: ^1.11.3
|
||||||
version: 1.11.2(@tiptap/pm@2.26.1)
|
version: 1.11.3(@tiptap/pm@2.26.1)
|
||||||
'@design.estate/dees-domtools':
|
'@design.estate/dees-domtools':
|
||||||
specifier: ^2.3.3
|
specifier: ^2.3.3
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
@@ -87,8 +87,8 @@ importers:
|
|||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
'@push.rocks/smartjson':
|
'@push.rocks/smartjson':
|
||||||
specifier: ^5.0.19
|
specifier: ^5.2.0
|
||||||
version: 5.0.20
|
version: 5.2.0
|
||||||
'@push.rocks/smartjwt':
|
'@push.rocks/smartjwt':
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -117,14 +117,14 @@ importers:
|
|||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@push.rocks/smartstate':
|
'@push.rocks/smartstate':
|
||||||
specifier: ^2.0.26
|
specifier: ^2.0.27
|
||||||
version: 2.0.26
|
version: 2.0.27
|
||||||
'@push.rocks/smartstream':
|
'@push.rocks/smartstream':
|
||||||
specifier: ^3.2.5
|
specifier: ^3.2.5
|
||||||
version: 3.2.5
|
version: 3.2.5
|
||||||
'@push.rocks/smartstring':
|
'@push.rocks/smartstring':
|
||||||
specifier: ^4.0.15
|
specifier: ^4.1.0
|
||||||
version: 4.0.15
|
version: 4.1.0
|
||||||
'@push.rocks/smartunique':
|
'@push.rocks/smartunique':
|
||||||
specifier: ^3.0.9
|
specifier: ^3.0.9
|
||||||
version: 3.0.9
|
version: 3.0.9
|
||||||
@@ -151,8 +151,8 @@ importers:
|
|||||||
specifier: ^1.10.3
|
specifier: ^1.10.3
|
||||||
version: 1.10.3
|
version: 1.10.3
|
||||||
'@git.zone/tstest':
|
'@git.zone/tstest':
|
||||||
specifier: ^2.3.6
|
specifier: ^2.3.8
|
||||||
version: 2.3.6(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2)
|
version: 2.3.8(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2)
|
||||||
'@git.zone/tswatch':
|
'@git.zone/tswatch':
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
@@ -476,8 +476,8 @@ packages:
|
|||||||
'@dabh/diagnostics@2.0.3':
|
'@dabh/diagnostics@2.0.3':
|
||||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||||
|
|
||||||
'@design.estate/dees-catalog@1.11.2':
|
'@design.estate/dees-catalog@1.11.3':
|
||||||
resolution: {integrity: sha512-gMK+wDKXDBPzfWmaJySotjjp5A9rwk2PQANQF8V6Q52xUfKKUv7gHj4eju+pN6qkUA5OUzdCDplUeUCrA8i37w==}
|
resolution: {integrity: sha512-gXGi6PlaHY4+lXHo17p+R/L6/QaqtN/3JFzTUXPl4J0fVKqVrEp22+lf7uvgAhs4WpV1Vd/c9yoyQ6JmrNSj4g==}
|
||||||
|
|
||||||
'@design.estate/dees-comms@1.0.27':
|
'@design.estate/dees-comms@1.0.27':
|
||||||
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
||||||
@@ -846,8 +846,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==}
|
resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tstest@2.3.6':
|
'@git.zone/tstest@2.3.8':
|
||||||
resolution: {integrity: sha512-2dcVM1WvQj9FoLPRWbLgBCWnDK0auI2c2vJxUzrLe0bi/ci50yrXxyKb2FIToQ+kOVe234Yb6jhNyp/d/zyHMQ==}
|
resolution: {integrity: sha512-rt7rpR2UwzHXjpqquEvWG4LfzGOGeI6lcR2YyO8pc7lqjhH+xsuaWPUQ5IwFl4Vw4VnR9ZoHBCqkjvxF8ow1wQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tswatch@2.2.1':
|
'@git.zone/tswatch@2.2.1':
|
||||||
@@ -1200,68 +1200,68 @@ packages:
|
|||||||
'@mongodb-js/saslprep@1.3.0':
|
'@mongodb-js/saslprep@1.3.0':
|
||||||
resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==}
|
resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==}
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.78':
|
'@napi-rs/canvas-android-arm64@0.1.79':
|
||||||
resolution: {integrity: sha512-N1ikxztjrRmh8xxlG5kYm1RuNr8ZW1EINEDQsLhhuy7t0pWI/e7SH91uFVLZKCMDyjel1tyWV93b5fdCAi7ggw==}
|
resolution: {integrity: sha512-ih6ZIztNDEXl7axvC4swOwLFrM9lOyJa9VAMq7xIBtEZhR/8IVDa0ZTup2fZEiTCmnjmXolzv7uDviHkOTEMKQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.78':
|
'@napi-rs/canvas-darwin-arm64@0.1.79':
|
||||||
resolution: {integrity: sha512-FA3aCU3G5yGc74BSmnLJTObnZRV+HW+JBTrsU+0WVVaNyVKlb5nMvYAQuieQlRVemsAA2ek2c6nYtHh6u6bwFw==}
|
resolution: {integrity: sha512-REMz1Fac2VlOYJDg+JjmQWSJc459cCgVom6GvKwWkDqzSjvG9BSo72MDmQY3uhb7r49Xuz5gTFcLYTfNcm4MoA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.78':
|
'@napi-rs/canvas-darwin-x64@0.1.79':
|
||||||
resolution: {integrity: sha512-xVij69o9t/frixCDEoyWoVDKgE3ksLGdmE2nvBWVGmoLu94MWUlv2y4Qzf5oozBmydG5Dcm4pRHFBM7YWa1i6g==}
|
resolution: {integrity: sha512-uQxLg6Bll7zv/ljp/YIeiUFWfV9C/ESv+2ioUh60hIAypuhtg6hhtWE/KnoW7G48wQls5VUStvEnJbnJ7bPKlA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.78':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.79':
|
||||||
resolution: {integrity: sha512-aSEXrLcIpBtXpOSnLhTg4jPsjJEnK7Je9KqUdAWjc7T8O4iYlxWxrXFIF8rV8J79h5jNdScgZpAUWYnEcutR3g==}
|
resolution: {integrity: sha512-X37B//TVIipL/3RyvyfNlbQK2uyIaK3PJ2bH7ZeU+jpkaYprBsV15GCN/LHTYAi6R0F/c53zK3aSFNKkGHM/Og==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.79':
|
||||||
resolution: {integrity: sha512-dlEPRX1hLGKaY3UtGa1dtkA1uGgFITn2mDnfI6YsLlYyLJQNqHx87D1YTACI4zFCUuLr/EzQDzuX+vnp9YveVg==}
|
resolution: {integrity: sha512-+T1fuau1heabE6zGXiqZBGPH5fTIQF+xEu/u4fuugxEiChRYlhnPjkw26MBi8ePg/jmzxLfJEij6LMJQ4AQa2A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.78':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.79':
|
||||||
resolution: {integrity: sha512-TsCfjOPZtm5Q/NO1EZHR5pwDPSPjPEttvnv44GL32Zn1uvudssjTLbvaG1jHq81Qxm16GTXEiYLmx4jOLZQYlg==}
|
resolution: {integrity: sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
|
||||||
resolution: {integrity: sha512-+cpTTb0GDshEow/5Fy8TpNyzaPsYb3clQIjgWRmzRcuteLU+CHEU/vpYvAcSo7JxHYPJd8fjSr+qqh+nI5AtmA==}
|
resolution: {integrity: sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.79':
|
||||||
resolution: {integrity: sha512-wxRcvKfvYBgtrO0Uy8OmwvjlnTcHpY45LLwkwVNIWHPqHAsyoTyG/JBSfJ0p5tWRzMOPDCDqdhpIO4LOgXjeyg==}
|
resolution: {integrity: sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.78':
|
'@napi-rs/canvas-linux-x64-musl@0.1.79':
|
||||||
resolution: {integrity: sha512-vQFOGwC9QDP0kXlhb2LU1QRw/humXgcbVp8mXlyBqzc/a0eijlLF9wzyarHC1EywpymtS63TAj8PHZnhTYN6hg==}
|
resolution: {integrity: sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.78':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.79':
|
||||||
resolution: {integrity: sha512-/eKlTZBtGUgpRKalzOzRr6h7KVSuziESWXgBcBnXggZmimwIJWPJlEcbrx5Tcwj8rPuZiANXQOG9pPgy9Q4LTQ==}
|
resolution: {integrity: sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.78':
|
'@napi-rs/canvas@0.1.79':
|
||||||
resolution: {integrity: sha512-YaBHJvT+T1DoP16puvWM6w46Lq3VhwKIJ8th5m1iEJyGh7mibk5dT7flBvMQ1EH1LYmMzXJ+OUhu+8wQ9I6u7g==}
|
resolution: {integrity: sha512-0SkvRRjyxY35eniEsQsjPYUMWunKlAWvionJOzJJADZF5ZDf/sL+ncJbMTV5LUiHg1iHOvVjWcuDOx/GNXr/lA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.3':
|
'@napi-rs/wasm-runtime@1.0.3':
|
||||||
@@ -1440,6 +1440,9 @@ packages:
|
|||||||
'@push.rocks/smartdns@6.2.2':
|
'@push.rocks/smartdns@6.2.2':
|
||||||
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
|
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
|
||||||
|
|
||||||
|
'@push.rocks/smartdns@7.6.1':
|
||||||
|
resolution: {integrity: sha512-nnP5+A2GOt0WsHrYhtKERmjdEHUchc+QbCCBEqlyeQTn+mNfx2WZvKVI1DFRJt8lamvzxP6Hr/BSe3WHdh4Snw==}
|
||||||
|
|
||||||
'@push.rocks/smartenv@5.0.12':
|
'@push.rocks/smartenv@5.0.12':
|
||||||
resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==}
|
resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==}
|
||||||
|
|
||||||
@@ -1479,6 +1482,9 @@ packages:
|
|||||||
'@push.rocks/smarthash@3.2.3':
|
'@push.rocks/smarthash@3.2.3':
|
||||||
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
|
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
|
||||||
|
|
||||||
|
'@push.rocks/smarthash@3.2.6':
|
||||||
|
resolution: {integrity: sha512-Mq/WNX0Tjjes3X1gHd/ZBwOOKSrAG/Z3Xoc0OcCm3P20WKpniihkMpsnlE7wGjvpHLi/ZRe/XkB3KC3d5r9X4g==}
|
||||||
|
|
||||||
'@push.rocks/smarti18n@1.0.4':
|
'@push.rocks/smarti18n@1.0.4':
|
||||||
resolution: {integrity: sha512-bHIi9Iuzp2cbux9q79ZK5jOQYPsYJ9zDDS4p/xEPQH31gr0mcFRosLSQb1kvDQDVmUhI0ADlQMqr2ui9zEXQHA==}
|
resolution: {integrity: sha512-bHIi9Iuzp2cbux9q79ZK5jOQYPsYJ9zDDS4p/xEPQH31gr0mcFRosLSQb1kvDQDVmUhI0ADlQMqr2ui9zEXQHA==}
|
||||||
|
|
||||||
@@ -1488,8 +1494,8 @@ packages:
|
|||||||
'@push.rocks/smartjimp@1.2.0':
|
'@push.rocks/smartjimp@1.2.0':
|
||||||
resolution: {integrity: sha512-SPz8p2ZuphNqIXK/UDsNFrnpJn/jr6FbuBSMQc0V2v2ffQIF32ZqktKQpXpitiqD1K5JEYS56JAhlYHgrAu7yw==}
|
resolution: {integrity: sha512-SPz8p2ZuphNqIXK/UDsNFrnpJn/jr6FbuBSMQc0V2v2ffQIF32ZqktKQpXpitiqD1K5JEYS56JAhlYHgrAu7yw==}
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.2.0':
|
||||||
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
resolution: {integrity: sha512-710e8UwovRfPgUtaBHcd6unaODUjV5fjxtGcGCqtaTcmvOV6VpasdVfT66xMDzQmWH2E9ZfHDJeso9HdDQzNQA==}
|
||||||
|
|
||||||
'@push.rocks/smartjwt@2.2.1':
|
'@push.rocks/smartjwt@2.2.1':
|
||||||
resolution: {integrity: sha512-Xwau9o8u7kLfSGi5v+kiyGB/hiDPclZjVEuj69J0LszO9nOh4OexYizKIOgOzKQMqnYQ03Dy35KqP9pdEjccbQ==}
|
resolution: {integrity: sha512-Xwau9o8u7kLfSGi5v+kiyGB/hiDPclZjVEuj69J0LszO9nOh4OexYizKIOgOzKQMqnYQ03Dy35KqP9pdEjccbQ==}
|
||||||
@@ -1530,6 +1536,9 @@ packages:
|
|||||||
'@push.rocks/smartnetwork@4.1.2':
|
'@push.rocks/smartnetwork@4.1.2':
|
||||||
resolution: {integrity: sha512-TjucG72ooHgzAUpNu2LAv4iFoettmZq2aEWhhzIa7AKcOvt4yxsk3Vl73guhKRohTfhdRauPcH5OHISLUHJbYA==}
|
resolution: {integrity: sha512-TjucG72ooHgzAUpNu2LAv4iFoettmZq2aEWhhzIa7AKcOvt4yxsk3Vl73guhKRohTfhdRauPcH5OHISLUHJbYA==}
|
||||||
|
|
||||||
|
'@push.rocks/smartnetwork@4.4.0':
|
||||||
|
resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==}
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
||||||
|
|
||||||
@@ -1599,8 +1608,8 @@ packages:
|
|||||||
'@push.rocks/smartssh@2.0.1':
|
'@push.rocks/smartssh@2.0.1':
|
||||||
resolution: {integrity: sha512-S+NFu1PYjsuExTUTQybXsT4r+mjqKydpAOfFvosF5ATO0EBD+nJc1TVx49dzAPO4/gs71Bxe3eLZWwZFwdKfsg==}
|
resolution: {integrity: sha512-S+NFu1PYjsuExTUTQybXsT4r+mjqKydpAOfFvosF5ATO0EBD+nJc1TVx49dzAPO4/gs71Bxe3eLZWwZFwdKfsg==}
|
||||||
|
|
||||||
'@push.rocks/smartstate@2.0.26':
|
'@push.rocks/smartstate@2.0.27':
|
||||||
resolution: {integrity: sha512-lMcf0ZWWs9jej9wjapuonuIZiQNiD9NcAcvRDFXq7GtQf/HUyr6zr5K1XxGZaCIGyYrbYnBHBpNU+8DBoarHrA==}
|
resolution: {integrity: sha512-q4UKir7GV3hakJWXQR4DoA4tUVwT5GRkJ/MtanHYF0wZLHfS19+nGmyO9y974zk3eT9hmy3+Lq5cKtU2W6+Y3w==}
|
||||||
|
|
||||||
'@push.rocks/smartstream@2.0.8':
|
'@push.rocks/smartstream@2.0.8':
|
||||||
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
|
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
|
||||||
@@ -1608,8 +1617,8 @@ packages:
|
|||||||
'@push.rocks/smartstream@3.2.5':
|
'@push.rocks/smartstream@3.2.5':
|
||||||
resolution: {integrity: sha512-PLGGIFDy8JLNVUnnntMSIYN4W081YSbNC7Y/sWpvUT8PAXtbEXXUiDFgK5o3gcI0ptpKQxHAwxhzNlPj0sbFVg==}
|
resolution: {integrity: sha512-PLGGIFDy8JLNVUnnntMSIYN4W081YSbNC7Y/sWpvUT8PAXtbEXXUiDFgK5o3gcI0ptpKQxHAwxhzNlPj0sbFVg==}
|
||||||
|
|
||||||
'@push.rocks/smartstring@4.0.15':
|
'@push.rocks/smartstring@4.1.0':
|
||||||
resolution: {integrity: sha512-NTNeOjWyg+aHtBTiQEyXamr7oTvYZ3wS1fudHo9ua7CLrykpK+i+RxFyJaLg1zB5x9xQF3NLEQecB14HPFX8Cg==}
|
resolution: {integrity: sha512-Q4py/Nm3KTDhQ9EiC75yBtSTLR0KLMwhKM+8gGcutgKotZT6wJ3gncjmtD8LKFfNhb4lSaFMgPJgLrCHTOH6Iw==}
|
||||||
|
|
||||||
'@push.rocks/smarttime@4.0.8':
|
'@push.rocks/smarttime@4.0.8':
|
||||||
resolution: {integrity: sha512-He+1ebBowVd8rW+VHZMFmz407xVMQf/JbyKr3s1ozoIlJS1AhZpDvlkzyqLV2tNMP1/cEBeo25ImJN2x1pksBA==}
|
resolution: {integrity: sha512-He+1ebBowVd8rW+VHZMFmz407xVMQf/JbyKr3s1ozoIlJS1AhZpDvlkzyqLV2tNMP1/cEBeo25ImJN2x1pksBA==}
|
||||||
@@ -5271,6 +5280,12 @@ packages:
|
|||||||
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
systeminformation@5.27.8:
|
||||||
|
resolution: {integrity: sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tar-fs@3.1.0:
|
tar-fs@3.1.0:
|
||||||
resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==}
|
resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==}
|
||||||
|
|
||||||
@@ -5718,7 +5733,7 @@ snapshots:
|
|||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartfeed': 1.0.11
|
'@push.rocks/smartfeed': 1.0.11
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.9
|
'@push.rocks/smartlog': 3.1.9
|
||||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
@@ -5757,10 +5772,10 @@ snapshots:
|
|||||||
'@api.global/typedrequest': 3.1.10
|
'@api.global/typedrequest': 3.1.10
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/isohash': 2.0.1
|
'@push.rocks/isohash': 2.0.1
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartsocket': 2.0.27
|
'@push.rocks/smartsocket': 2.0.27
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarturl': 3.1.0
|
'@push.rocks/smarturl': 3.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
@@ -5776,7 +5791,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.1.9
|
'@push.rocks/smartlog': 3.1.9
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 2.1.0
|
'@push.rocks/smartrequest': 2.1.0
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@tsclass/tsclass': 9.2.0
|
'@tsclass/tsclass': 9.2.0
|
||||||
cloudflare: 4.5.0
|
cloudflare: 4.5.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -5788,14 +5803,14 @@ snapshots:
|
|||||||
'@push.rocks/smartarchive': 4.2.2
|
'@push.rocks/smartarchive': 4.2.2
|
||||||
'@push.rocks/smartbucket': 3.3.10
|
'@push.rocks/smartbucket': 3.3.10
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.9
|
'@push.rocks/smartlog': 3.1.9
|
||||||
'@push.rocks/smartnetwork': 4.1.2
|
'@push.rocks/smartnetwork': 4.1.2
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 4.3.1
|
'@push.rocks/smartrequest': 4.3.1
|
||||||
'@push.rocks/smartstream': 3.2.5
|
'@push.rocks/smartstream': 3.2.5
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/smartversion': 3.0.5
|
'@push.rocks/smartversion': 3.0.5
|
||||||
'@tsclass/tsclass': 9.2.0
|
'@tsclass/tsclass': 9.2.0
|
||||||
@@ -6684,7 +6699,7 @@ snapshots:
|
|||||||
enabled: 2.0.0
|
enabled: 2.0.0
|
||||||
kuler: 2.0.0
|
kuler: 2.0.0
|
||||||
|
|
||||||
'@design.estate/dees-catalog@1.11.2(@tiptap/pm@2.26.1)':
|
'@design.estate/dees-catalog@1.11.3(@tiptap/pm@2.26.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@design.estate/dees-domtools': 2.3.3
|
'@design.estate/dees-domtools': 2.3.3
|
||||||
'@design.estate/dees-element': 2.1.2
|
'@design.estate/dees-element': 2.1.2
|
||||||
@@ -6695,7 +6710,7 @@ snapshots:
|
|||||||
'@fortawesome/free-solid-svg-icons': 7.0.1
|
'@fortawesome/free-solid-svg-icons': 7.0.1
|
||||||
'@push.rocks/smarti18n': 1.0.4
|
'@push.rocks/smarti18n': 1.0.4
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
|
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
|
||||||
'@tiptap/extension-link': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)
|
'@tiptap/extension-link': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)
|
||||||
'@tiptap/extension-text-align': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))
|
'@tiptap/extension-text-align': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))
|
||||||
@@ -6732,13 +6747,13 @@ snapshots:
|
|||||||
'@design.estate/dees-comms': 1.0.27
|
'@design.estate/dees-comms': 1.0.27
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartmarkdown': 3.0.3
|
'@push.rocks/smartmarkdown': 3.0.3
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrouter': 1.3.3
|
'@push.rocks/smartrouter': 1.3.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartstate': 2.0.26
|
'@push.rocks/smartstate': 2.0.27
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarturl': 3.1.0
|
'@push.rocks/smarturl': 3.1.0
|
||||||
'@push.rocks/webrequest': 3.0.37
|
'@push.rocks/webrequest': 3.0.37
|
||||||
'@push.rocks/websetup': 3.0.19
|
'@push.rocks/websetup': 3.0.19
|
||||||
@@ -7068,7 +7083,7 @@ snapshots:
|
|||||||
'@push.rocks/smartshell': 3.0.6
|
'@push.rocks/smartshell': 3.0.6
|
||||||
tsx: 4.19.2
|
tsx: 4.19.2
|
||||||
|
|
||||||
'@git.zone/tstest@2.3.6(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2)':
|
'@git.zone/tstest@2.3.8(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.79
|
'@api.global/typedserver': 3.0.79
|
||||||
'@git.zone/tsbundle': 2.5.1
|
'@git.zone/tsbundle': 2.5.1
|
||||||
@@ -7082,9 +7097,10 @@ snapshots:
|
|||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartexpect': 2.5.0
|
'@push.rocks/smartexpect': 2.5.0
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.9
|
'@push.rocks/smartlog': 3.1.9
|
||||||
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)
|
||||||
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 4.3.1
|
'@push.rocks/smartrequest': 4.3.1
|
||||||
@@ -7578,48 +7594,48 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sparse-bitfield: 3.0.3
|
sparse-bitfield: 3.0.3
|
||||||
|
|
||||||
'@napi-rs/canvas-android-arm64@0.1.78':
|
'@napi-rs/canvas-android-arm64@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-arm64@0.1.78':
|
'@napi-rs/canvas-darwin-arm64@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-darwin-x64@0.1.78':
|
'@napi-rs/canvas-darwin-x64@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.78':
|
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-arm64-gnu@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-arm64-musl@0.1.78':
|
'@napi-rs/canvas-linux-arm64-musl@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.78':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.78':
|
'@napi-rs/canvas-linux-x64-musl@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-x64-msvc@0.1.78':
|
'@napi-rs/canvas-win32-x64-msvc@0.1.79':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/canvas@0.1.78':
|
'@napi-rs/canvas@0.1.79':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas-android-arm64': 0.1.78
|
'@napi-rs/canvas-android-arm64': 0.1.79
|
||||||
'@napi-rs/canvas-darwin-arm64': 0.1.78
|
'@napi-rs/canvas-darwin-arm64': 0.1.79
|
||||||
'@napi-rs/canvas-darwin-x64': 0.1.78
|
'@napi-rs/canvas-darwin-x64': 0.1.79
|
||||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.78
|
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.79
|
||||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.78
|
'@napi-rs/canvas-linux-arm64-gnu': 0.1.79
|
||||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.78
|
'@napi-rs/canvas-linux-arm64-musl': 0.1.79
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.78
|
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.79
|
||||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.78
|
'@napi-rs/canvas-linux-x64-gnu': 0.1.79
|
||||||
'@napi-rs/canvas-linux-x64-musl': 0.1.78
|
'@napi-rs/canvas-linux-x64-musl': 0.1.79
|
||||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.78
|
'@napi-rs/canvas-win32-x64-msvc': 0.1.79
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.3':
|
'@napi-rs/wasm-runtime@1.0.3':
|
||||||
@@ -7804,10 +7820,10 @@ snapshots:
|
|||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartexit': 1.0.23
|
'@push.rocks/smartexit': 1.0.23
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartpath': 5.1.0
|
'@push.rocks/smartpath': 5.1.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.4.0
|
'@push.rocks/taskbuffer': 3.4.0
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 4.4.4
|
||||||
@@ -7826,10 +7842,10 @@ snapshots:
|
|||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartexit': 1.0.23
|
'@push.rocks/smartexit': 1.0.23
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.4.0
|
'@push.rocks/taskbuffer': 3.4.0
|
||||||
'@tsclass/tsclass': 9.2.0
|
'@tsclass/tsclass': 9.2.0
|
||||||
@@ -7878,7 +7894,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.9
|
'@push.rocks/smartlog': 3.1.9
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -7896,7 +7912,7 @@ snapshots:
|
|||||||
'@push.rocks/smartfile': 10.0.41
|
'@push.rocks/smartfile': 10.0.41
|
||||||
'@push.rocks/smartpath': 5.1.0
|
'@push.rocks/smartpath': 5.1.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
|
|
||||||
'@push.rocks/qenv@6.1.3':
|
'@push.rocks/qenv@6.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7919,7 +7935,7 @@ snapshots:
|
|||||||
'@push.rocks/smartnetwork': 4.1.2
|
'@push.rocks/smartnetwork': 4.1.2
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 2.1.0
|
'@push.rocks/smartrequest': 2.1.0
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@tsclass/tsclass': 9.2.0
|
'@tsclass/tsclass': 9.2.0
|
||||||
@@ -7928,7 +7944,6 @@ snapshots:
|
|||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
|
||||||
- encoding
|
- encoding
|
||||||
- gcp-metadata
|
- gcp-metadata
|
||||||
- kerberos
|
- kerberos
|
||||||
@@ -8005,7 +8020,7 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartstream': 3.2.5
|
'@push.rocks/smartstream': 3.2.5
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@tsclass/tsclass': 9.2.0
|
'@tsclass/tsclass': 9.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -8075,7 +8090,7 @@ snapshots:
|
|||||||
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.4.0
|
'@push.rocks/taskbuffer': 3.4.0
|
||||||
@@ -8115,6 +8130,22 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@push.rocks/smartdns@7.6.1':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartenv': 5.0.13
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrequest': 2.1.0
|
||||||
|
'@tsclass/tsclass': 9.2.0
|
||||||
|
'@types/dns-packet': 5.6.5
|
||||||
|
'@types/elliptic': 6.4.18
|
||||||
|
acme-client: 5.4.0
|
||||||
|
dns-packet: 5.6.1
|
||||||
|
elliptic: 6.6.1
|
||||||
|
minimatch: 10.0.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@push.rocks/smartenv@5.0.12':
|
'@push.rocks/smartenv@5.0.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -8155,7 +8186,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile-interfaces': 1.0.7
|
'@push.rocks/smartfile-interfaces': 1.0.7
|
||||||
'@push.rocks/smarthash': 3.0.4
|
'@push.rocks/smarthash': 3.0.4
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartmime': 1.0.6
|
'@push.rocks/smartmime': 1.0.6
|
||||||
'@push.rocks/smartpath': 5.1.0
|
'@push.rocks/smartpath': 5.1.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -8174,7 +8205,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile-interfaces': 1.0.7
|
'@push.rocks/smartfile-interfaces': 1.0.7
|
||||||
'@push.rocks/smarthash': 3.2.3
|
'@push.rocks/smarthash': 3.2.3
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartmime': 2.0.4
|
'@push.rocks/smartmime': 2.0.4
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -8193,7 +8224,7 @@ snapshots:
|
|||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
'@types/diff': 8.0.0
|
'@types/diff': 8.0.0
|
||||||
diff: 8.0.2
|
diff: 8.0.2
|
||||||
@@ -8206,7 +8237,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smarthash@3.0.4':
|
'@push.rocks/smarthash@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@types/through2': 2.0.41
|
'@types/through2': 2.0.41
|
||||||
through2: 4.0.2
|
through2: 4.0.2
|
||||||
@@ -8214,7 +8245,15 @@ snapshots:
|
|||||||
'@push.rocks/smarthash@3.2.3':
|
'@push.rocks/smarthash@3.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@types/through2': 2.0.41
|
||||||
|
through2: 4.0.2
|
||||||
|
|
||||||
|
'@push.rocks/smarthash@3.2.6':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartenv': 5.0.13
|
||||||
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@types/through2': 2.0.41
|
'@types/through2': 2.0.41
|
||||||
through2: 4.0.2
|
through2: 4.0.2
|
||||||
@@ -8244,10 +8283,10 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartenv': 5.0.12
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
fast-json-stable-stringify: 2.1.0
|
fast-json-stable-stringify: 2.1.0
|
||||||
lodash.clonedeep: 4.5.0
|
lodash.clonedeep: 4.5.0
|
||||||
|
|
||||||
@@ -8255,7 +8294,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartguard': 3.1.0
|
'@push.rocks/smartguard': 3.1.0
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 4.4.4
|
||||||
'@types/jsonwebtoken': 9.0.7
|
'@types/jsonwebtoken': 9.0.7
|
||||||
jsonwebtoken: 9.0.2
|
jsonwebtoken: 9.0.2
|
||||||
@@ -8350,12 +8389,23 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartping': 1.0.8
|
'@push.rocks/smartping': 1.0.8
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@types/default-gateway': 7.2.2
|
'@types/default-gateway': 7.2.2
|
||||||
isopen: 1.3.0
|
isopen: 1.3.0
|
||||||
public-ip: 7.0.1
|
public-ip: 7.0.1
|
||||||
systeminformation: 5.27.7
|
systeminformation: 5.27.7
|
||||||
|
|
||||||
|
'@push.rocks/smartnetwork@4.4.0':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdns': 7.6.1
|
||||||
|
'@push.rocks/smartping': 1.0.8
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartstring': 4.1.0
|
||||||
|
isopen: 1.3.0
|
||||||
|
systeminformation: 5.27.8
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
@@ -8405,7 +8455,7 @@ snapshots:
|
|||||||
'@push.rocks/smartbuffer': 3.0.5
|
'@push.rocks/smartbuffer': 3.0.5
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartnetwork': 4.1.2
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.2)
|
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.2)
|
||||||
@@ -8553,7 +8603,7 @@ snapshots:
|
|||||||
'@push.rocks/lik': 6.1.0
|
'@push.rocks/lik': 6.1.0
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartenv': 5.0.12
|
'@push.rocks/smartenv': 5.0.12
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.9
|
'@push.rocks/smartlog': 3.1.9
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -8584,16 +8634,16 @@ snapshots:
|
|||||||
'@push.rocks/smartpath': 5.1.0
|
'@push.rocks/smartpath': 5.1.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartshell': 3.0.6
|
'@push.rocks/smartshell': 3.0.6
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@types/fs-extra': 11.0.4
|
'@types/fs-extra': 11.0.4
|
||||||
fs-extra: 11.2.0
|
fs-extra: 11.2.0
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
|
|
||||||
'@push.rocks/smartstate@2.0.26':
|
'@push.rocks/smartstate@2.0.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smarthash': 3.2.3
|
'@push.rocks/smarthash': 3.2.6
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
@@ -8614,16 +8664,9 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
|
||||||
'@push.rocks/smartstring@4.0.15':
|
'@push.rocks/smartstring@4.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/isounique': 1.0.5
|
'@push.rocks/isounique': 1.0.5
|
||||||
'@push.rocks/smartenv': 5.0.12
|
|
||||||
'@types/randomatic': 3.1.5
|
|
||||||
crypto-random-string: 5.0.0
|
|
||||||
js-base64: 3.7.7
|
|
||||||
randomatic: 3.1.1
|
|
||||||
strip-indent: 4.0.0
|
|
||||||
url: 0.11.4
|
|
||||||
|
|
||||||
'@push.rocks/smarttime@4.0.8':
|
'@push.rocks/smarttime@4.0.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8687,13 +8730,13 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/webjwt@1.0.9':
|
'@push.rocks/webjwt@1.0.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.1.0
|
||||||
|
|
||||||
'@push.rocks/webrequest@3.0.37':
|
'@push.rocks/webrequest@3.0.37':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartenv': 5.0.12
|
'@push.rocks/smartenv': 5.0.12
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
|
|
||||||
@@ -8708,7 +8751,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/lik': 6.1.0
|
'@push.rocks/lik': 6.1.0
|
||||||
'@push.rocks/smartenv': 5.0.12
|
'@push.rocks/smartenv': 5.0.12
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@tempfix/idb': 8.0.3
|
'@tempfix/idb': 8.0.3
|
||||||
@@ -12345,7 +12388,7 @@ snapshots:
|
|||||||
|
|
||||||
pdfjs-dist@4.10.38:
|
pdfjs-dist@4.10.38:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@napi-rs/canvas': 0.1.78
|
'@napi-rs/canvas': 0.1.79
|
||||||
|
|
||||||
peek-readable@4.1.0: {}
|
peek-readable@4.1.0: {}
|
||||||
|
|
||||||
@@ -13114,6 +13157,8 @@ snapshots:
|
|||||||
|
|
||||||
systeminformation@5.27.7: {}
|
systeminformation@5.27.7: {}
|
||||||
|
|
||||||
|
systeminformation@5.27.8: {}
|
||||||
|
|
||||||
tar-fs@3.1.0:
|
tar-fs@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pump: 3.0.3
|
pump: 3.0.3
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/cloudly',
|
name: '@serve.zone/cloudly',
|
||||||
version: '5.2.0',
|
version: '5.3.0',
|
||||||
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
|
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ import { MongodbConnector } from './connector.mongodb/connector.js';
|
|||||||
// processes
|
// processes
|
||||||
import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js';
|
import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js';
|
||||||
import { ClusterManager } from './manager.cluster/classes.clustermanager.js';
|
import { ClusterManager } from './manager.cluster/classes.clustermanager.js';
|
||||||
import { CloudlyTaskmanager } from './manager.task/taskmanager.js';
|
import { CloudlyTaskManager } from './manager.task/classes.taskmanager.js';
|
||||||
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js';
|
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js';
|
||||||
import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js';
|
import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js';
|
||||||
import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js';
|
import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js';
|
||||||
import { ExternalApiManager } from './manager.status/statusmanager.js';
|
import { ExternalApiManager } from './manager.status/statusmanager.js';
|
||||||
import { ExternalRegistryManager } from './manager.externalregistry/index.js';
|
import { ExternalRegistryManager } from './manager.externalregistry/index.js';
|
||||||
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
||||||
|
import { ServiceManager } from './manager.service/classes.servicemanager.js';
|
||||||
|
import { DeploymentManager } from './manager.deployment/classes.deploymentmanager.js';
|
||||||
|
import { DnsManager } from './manager.dns/classes.dnsmanager.js';
|
||||||
|
import { DomainManager } from './manager.domain/classes.domainmanager.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
||||||
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
|
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
|
||||||
@@ -60,7 +64,11 @@ export class Cloudly {
|
|||||||
public externalApiManager: ExternalApiManager;
|
public externalApiManager: ExternalApiManager;
|
||||||
public externalRegistryManager: ExternalRegistryManager;
|
public externalRegistryManager: ExternalRegistryManager;
|
||||||
public imageManager: ImageManager;
|
public imageManager: ImageManager;
|
||||||
public taskManager: CloudlyTaskmanager;
|
public serviceManager: ServiceManager;
|
||||||
|
public deploymentManager: DeploymentManager;
|
||||||
|
public dnsManager: DnsManager;
|
||||||
|
public domainManager: DomainManager;
|
||||||
|
public taskManager: CloudlyTaskManager;
|
||||||
public nodeManager: CloudlyNodeManager;
|
public nodeManager: CloudlyNodeManager;
|
||||||
public baremetalManager: CloudlyBaremetalManager;
|
public baremetalManager: CloudlyBaremetalManager;
|
||||||
|
|
||||||
@@ -89,7 +97,11 @@ export class Cloudly {
|
|||||||
this.externalApiManager = new ExternalApiManager(this);
|
this.externalApiManager = new ExternalApiManager(this);
|
||||||
this.externalRegistryManager = new ExternalRegistryManager(this);
|
this.externalRegistryManager = new ExternalRegistryManager(this);
|
||||||
this.imageManager = new ImageManager(this);
|
this.imageManager = new ImageManager(this);
|
||||||
this.taskManager = new CloudlyTaskmanager(this);
|
this.serviceManager = new ServiceManager(this);
|
||||||
|
this.deploymentManager = new DeploymentManager(this);
|
||||||
|
this.dnsManager = new DnsManager(this);
|
||||||
|
this.domainManager = new DomainManager(this);
|
||||||
|
this.taskManager = new CloudlyTaskManager(this);
|
||||||
this.secretManager = new CloudlySecretManager(this);
|
this.secretManager = new CloudlySecretManager(this);
|
||||||
this.nodeManager = new CloudlyNodeManager(this);
|
this.nodeManager = new CloudlyNodeManager(this);
|
||||||
this.baremetalManager = new CloudlyBaremetalManager(this);
|
this.baremetalManager = new CloudlyBaremetalManager(this);
|
||||||
@@ -114,6 +126,9 @@ export class Cloudly {
|
|||||||
await this.secretManager.start();
|
await this.secretManager.start();
|
||||||
await this.nodeManager.start();
|
await this.nodeManager.start();
|
||||||
await this.baremetalManager.start();
|
await this.baremetalManager.start();
|
||||||
|
await this.serviceManager.start();
|
||||||
|
await this.deploymentManager.start();
|
||||||
|
await this.taskManager.init();
|
||||||
|
|
||||||
await this.cloudflareConnector.init();
|
await this.cloudflareConnector.init();
|
||||||
await this.letsencryptConnector.init();
|
await this.letsencryptConnector.init();
|
||||||
@@ -123,6 +138,7 @@ export class Cloudly {
|
|||||||
|
|
||||||
// start the managers
|
// start the managers
|
||||||
this.imageManager.start();
|
this.imageManager.start();
|
||||||
|
this.externalRegistryManager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,5 +149,9 @@ export class Cloudly {
|
|||||||
await this.letsencryptConnector.stop();
|
await this.letsencryptConnector.stop();
|
||||||
await this.mongodbConnector.stop();
|
await this.mongodbConnector.stop();
|
||||||
await this.secretManager.stop();
|
await this.secretManager.stop();
|
||||||
|
await this.serviceManager.stop();
|
||||||
|
await this.deploymentManager.stop();
|
||||||
|
await this.taskManager.stop();
|
||||||
|
await this.externalRegistryManager.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
98
ts/manager.deployment/classes.deployment.ts
Normal file
98
ts/manager.deployment/classes.deployment.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
@plugins.smartdata.managed()
|
||||||
|
export class Deployment extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
Deployment,
|
||||||
|
plugins.servezoneInterfaces.data.IDeployment
|
||||||
|
> {
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
public id: string = plugins.smartunique.uniSimple('deployment');
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public serviceId: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public nodeId: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public containerId?: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public usedImageId: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public version: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public deployedAt: number;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public deploymentLog: string[] = [];
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public resourceUsage?: {
|
||||||
|
cpuUsagePercent: number;
|
||||||
|
memoryUsedMB: number;
|
||||||
|
lastUpdated: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async createDeployment(
|
||||||
|
deploymentData: Partial<plugins.servezoneInterfaces.data.IDeployment>
|
||||||
|
): Promise<Deployment> {
|
||||||
|
const deployment = new Deployment();
|
||||||
|
if (deploymentData.serviceId) deployment.serviceId = deploymentData.serviceId;
|
||||||
|
if (deploymentData.nodeId) deployment.nodeId = deploymentData.nodeId;
|
||||||
|
if (deploymentData.containerId) deployment.containerId = deploymentData.containerId;
|
||||||
|
if (deploymentData.usedImageId) deployment.usedImageId = deploymentData.usedImageId;
|
||||||
|
if (deploymentData.version) deployment.version = deploymentData.version;
|
||||||
|
if (deploymentData.deployedAt) deployment.deployedAt = deploymentData.deployedAt;
|
||||||
|
if (deploymentData.deploymentLog) deployment.deploymentLog = deploymentData.deploymentLog;
|
||||||
|
if (deploymentData.status) deployment.status = deploymentData.status;
|
||||||
|
if (deploymentData.healthStatus) deployment.healthStatus = deploymentData.healthStatus;
|
||||||
|
if (deploymentData.resourceUsage) deployment.resourceUsage = deploymentData.resourceUsage;
|
||||||
|
|
||||||
|
await deployment.save();
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateHealthStatus(healthStatus: 'healthy' | 'unhealthy' | 'unknown') {
|
||||||
|
this.healthStatus = healthStatus;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateResourceUsage(cpuUsagePercent: number, memoryUsedMB: number) {
|
||||||
|
this.resourceUsage = {
|
||||||
|
cpuUsagePercent,
|
||||||
|
memoryUsedMB,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addLogEntry(entry: string) {
|
||||||
|
this.deploymentLog.push(entry);
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createSavableObject(): Promise<plugins.servezoneInterfaces.data.IDeployment> {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
serviceId: this.serviceId,
|
||||||
|
nodeId: this.nodeId,
|
||||||
|
containerId: this.containerId,
|
||||||
|
usedImageId: this.usedImageId,
|
||||||
|
version: this.version,
|
||||||
|
deployedAt: this.deployedAt,
|
||||||
|
deploymentLog: this.deploymentLog,
|
||||||
|
status: this.status,
|
||||||
|
healthStatus: this.healthStatus,
|
||||||
|
resourceUsage: this.resourceUsage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
324
ts/manager.deployment/classes.deploymentmanager.ts
Normal file
324
ts/manager.deployment/classes.deploymentmanager.ts
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Deployment } from './classes.deployment.js';
|
||||||
|
|
||||||
|
export class DeploymentManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CDeployment = plugins.smartdata.setDefaultManagerForDoc(this, Deployment);
|
||||||
|
|
||||||
|
constructor(cloudlyRef: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
|
||||||
|
// Connect typedrouter to main router
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
// Get all deployments
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(
|
||||||
|
'getDeployments',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployments = await this.CDeployment.getInstances({});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deployments: await Promise.all(
|
||||||
|
deployments.map((deployment) => deployment.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get deployment by ID
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentById>(
|
||||||
|
'getDeploymentById',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployment = await this.CDeployment.getInstance({
|
||||||
|
id: reqArg.deploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
throw new Error('Deployment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deployment: await deployment.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get deployments by service
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentsByService>(
|
||||||
|
'getDeploymentsByService',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployments = await this.CDeployment.getInstances({
|
||||||
|
serviceId: reqArg.serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deployments: await Promise.all(
|
||||||
|
deployments.map((deployment) => deployment.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get deployments by node
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentsByNode>(
|
||||||
|
'getDeploymentsByNode',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployments = await this.CDeployment.getInstances({
|
||||||
|
nodeId: reqArg.nodeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
deployments: await Promise.all(
|
||||||
|
deployments.map((deployment) => deployment.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create deployment
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(
|
||||||
|
'createDeployment',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployment = await Deployment.createDeployment(reqArg.deploymentData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deployment: await deployment.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update deployment
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(
|
||||||
|
'updateDeployment',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployment = await this.CDeployment.getInstance({
|
||||||
|
id: reqArg.deploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
throw new Error('Deployment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (reqArg.deploymentData.status !== undefined) {
|
||||||
|
deployment.status = reqArg.deploymentData.status;
|
||||||
|
}
|
||||||
|
if (reqArg.deploymentData.healthStatus !== undefined) {
|
||||||
|
deployment.healthStatus = reqArg.deploymentData.healthStatus;
|
||||||
|
}
|
||||||
|
if (reqArg.deploymentData.containerId !== undefined) {
|
||||||
|
deployment.containerId = reqArg.deploymentData.containerId;
|
||||||
|
}
|
||||||
|
if (reqArg.deploymentData.resourceUsage !== undefined) {
|
||||||
|
deployment.resourceUsage = reqArg.deploymentData.resourceUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deployment.save();
|
||||||
|
|
||||||
|
return {
|
||||||
|
deployment: await deployment.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete deployment
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(
|
||||||
|
'deleteDeploymentById',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployment = await this.CDeployment.getInstance({
|
||||||
|
id: reqArg.deploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
throw new Error('Deployment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceId = deployment.serviceId;
|
||||||
|
await deployment.delete();
|
||||||
|
|
||||||
|
// Check if this was the last deployment for the service
|
||||||
|
const remainingDeployments = await this.getDeploymentsForService(serviceId);
|
||||||
|
if (remainingDeployments.length === 0) {
|
||||||
|
// Deactivate DNS entries if no more deployments exist
|
||||||
|
await this.cloudlyRef.dnsManager.deactivateServiceDnsEntries(serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restart deployment
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_RestartDeployment>(
|
||||||
|
'restartDeployment',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deployment = await this.CDeployment.getInstance({
|
||||||
|
id: reqArg.deploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
throw new Error('Deployment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement actual restart logic with Docker/container runtime
|
||||||
|
deployment.status = 'starting';
|
||||||
|
await deployment.save();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
deployment: await deployment.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Scale deployment
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_ScaleDeployment>(
|
||||||
|
'scaleDeployment',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO: Implement scaling logic
|
||||||
|
// This would create/delete deployment instances based on replicas count
|
||||||
|
|
||||||
|
const deployment = await this.CDeployment.getInstance({
|
||||||
|
id: reqArg.deploymentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
throw new Error('Deployment not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
deployment: await deployment.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all deployments
|
||||||
|
*/
|
||||||
|
public async getAllDeployments(): Promise<Deployment[]> {
|
||||||
|
return await this.CDeployment.getInstances({});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get deployments for a specific service
|
||||||
|
*/
|
||||||
|
public async getDeploymentsForService(serviceId: string): Promise<Deployment[]> {
|
||||||
|
return await this.CDeployment.getInstances({
|
||||||
|
serviceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get deployments for a specific node
|
||||||
|
*/
|
||||||
|
public async getDeploymentsForNode(nodeId: string): Promise<Deployment[]> {
|
||||||
|
return await this.CDeployment.getInstances({
|
||||||
|
nodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new deployment
|
||||||
|
*/
|
||||||
|
public async createDeployment(
|
||||||
|
serviceId: string,
|
||||||
|
nodeId: string,
|
||||||
|
version: string = 'latest'
|
||||||
|
): Promise<Deployment> {
|
||||||
|
const deployment = await Deployment.createDeployment({
|
||||||
|
serviceId,
|
||||||
|
nodeId,
|
||||||
|
version,
|
||||||
|
status: 'scheduled',
|
||||||
|
deployedAt: Date.now(),
|
||||||
|
deploymentLog: [`Deployment created at ${new Date().toISOString()}`],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate DNS entries for the service
|
||||||
|
await this.cloudlyRef.dnsManager.activateServiceDnsEntries(serviceId);
|
||||||
|
|
||||||
|
// Get the node's IP address and update DNS entries
|
||||||
|
const node = await this.cloudlyRef.nodeManager.CClusterNode.getInstance({
|
||||||
|
id: nodeId,
|
||||||
|
});
|
||||||
|
if (node && node.data.publicIp) {
|
||||||
|
await this.cloudlyRef.dnsManager.updateServiceDnsEntriesIp(serviceId, node.data.publicIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
// DeploymentManager is ready - handlers are already registered in constructor
|
||||||
|
console.log('DeploymentManager started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
// Cleanup if needed
|
||||||
|
console.log('DeploymentManager stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
149
ts/manager.dns/classes.dnsentry.ts
Normal file
149
ts/manager.dns/classes.dnsentry.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DnsManager } from './classes.dnsmanager.js';
|
||||||
|
|
||||||
|
@plugins.smartdata.managed()
|
||||||
|
export class DnsEntry extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
DnsEntry,
|
||||||
|
plugins.servezoneInterfaces.data.IDnsEntry,
|
||||||
|
DnsManager
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async getDnsEntryById(dnsEntryIdArg: string) {
|
||||||
|
const dnsEntry = await this.getInstance({
|
||||||
|
id: dnsEntryIdArg,
|
||||||
|
});
|
||||||
|
return dnsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDnsEntries(filterArg?: { zone?: string }) {
|
||||||
|
const filter: any = {};
|
||||||
|
if (filterArg?.zone) {
|
||||||
|
filter['data.zone'] = filterArg.zone;
|
||||||
|
}
|
||||||
|
const dnsEntries = await this.getInstances(filter);
|
||||||
|
return dnsEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDnsZones() {
|
||||||
|
const dnsEntries = await this.getInstances({});
|
||||||
|
const zones = new Set<string>();
|
||||||
|
for (const entry of dnsEntries) {
|
||||||
|
if (entry.data.zone) {
|
||||||
|
zones.add(entry.data.zone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(zones).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createDnsEntry(dnsEntryDataArg: plugins.servezoneInterfaces.data.IDnsEntry['data']) {
|
||||||
|
const dnsEntry = new DnsEntry();
|
||||||
|
dnsEntry.id = await DnsEntry.getNewId();
|
||||||
|
dnsEntry.data = {
|
||||||
|
...dnsEntryDataArg,
|
||||||
|
ttl: dnsEntryDataArg.ttl || 3600, // Default TTL: 1 hour
|
||||||
|
active: dnsEntryDataArg.active !== false, // Default to active
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
await dnsEntry.save();
|
||||||
|
return dnsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async updateDnsEntry(
|
||||||
|
dnsEntryIdArg: string,
|
||||||
|
dnsEntryDataArg: Partial<plugins.servezoneInterfaces.data.IDnsEntry['data']>
|
||||||
|
) {
|
||||||
|
const dnsEntry = await this.getInstance({
|
||||||
|
id: dnsEntryIdArg,
|
||||||
|
});
|
||||||
|
if (!dnsEntry) {
|
||||||
|
throw new Error(`DNS entry with id ${dnsEntryIdArg} not found`);
|
||||||
|
}
|
||||||
|
Object.assign(dnsEntry.data, dnsEntryDataArg, {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
await dnsEntry.save();
|
||||||
|
return dnsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteDnsEntry(dnsEntryIdArg: string) {
|
||||||
|
const dnsEntry = await this.getInstance({
|
||||||
|
id: dnsEntryIdArg,
|
||||||
|
});
|
||||||
|
if (!dnsEntry) {
|
||||||
|
throw new Error(`DNS entry with id ${dnsEntryIdArg} not found`);
|
||||||
|
}
|
||||||
|
await dnsEntry.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IDnsEntry['data'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the DNS entry data
|
||||||
|
*/
|
||||||
|
public validateData(): boolean {
|
||||||
|
const { type, name, value, zone } = this.data;
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!type || !name || !value || !zone) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-specific validation
|
||||||
|
switch (type) {
|
||||||
|
case 'A':
|
||||||
|
// Validate IPv4 address
|
||||||
|
return /^(\d{1,3}\.){3}\d{1,3}$/.test(value);
|
||||||
|
case 'AAAA':
|
||||||
|
// Validate IPv6 address (simplified)
|
||||||
|
return /^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/.test(value);
|
||||||
|
case 'MX':
|
||||||
|
// MX records must have priority
|
||||||
|
return this.data.priority !== undefined && this.data.priority >= 0;
|
||||||
|
case 'SRV':
|
||||||
|
// SRV records must have priority, weight, and port
|
||||||
|
return (
|
||||||
|
this.data.priority !== undefined &&
|
||||||
|
this.data.weight !== undefined &&
|
||||||
|
this.data.port !== undefined
|
||||||
|
);
|
||||||
|
case 'CNAME':
|
||||||
|
case 'NS':
|
||||||
|
case 'PTR':
|
||||||
|
// These should point to valid domain names
|
||||||
|
return /^[a-zA-Z0-9.-]+$/.test(value);
|
||||||
|
case 'TXT':
|
||||||
|
case 'CAA':
|
||||||
|
case 'SOA':
|
||||||
|
// These can contain any text
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a formatted string representation of the DNS entry
|
||||||
|
*/
|
||||||
|
public toFormattedString(): string {
|
||||||
|
const { type, name, value, ttl, priority } = this.data;
|
||||||
|
let result = `${name} ${ttl} IN ${type}`;
|
||||||
|
|
||||||
|
if (priority !== undefined) {
|
||||||
|
result += ` ${priority}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'SRV' && this.data.weight !== undefined && this.data.port !== undefined) {
|
||||||
|
result += ` ${this.data.weight} ${this.data.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += ` ${value}`;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
267
ts/manager.dns/classes.dnsmanager.ts
Normal file
267
ts/manager.dns/classes.dnsmanager.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DnsEntry } from './classes.dnsentry.js';
|
||||||
|
|
||||||
|
export class DnsManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CDnsEntry = plugins.smartdata.setDefaultManagerForDoc(this, DnsEntry);
|
||||||
|
|
||||||
|
constructor(cloudlyRef: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
// Get all DNS entries
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(
|
||||||
|
'getDnsEntries',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dnsEntries = await this.CDnsEntry.getDnsEntries(
|
||||||
|
reqArg.zone ? { zone: reqArg.zone } : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntries: await Promise.all(
|
||||||
|
dnsEntries.map((entry) => entry.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get DNS entry by ID
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntryById>(
|
||||||
|
'getDnsEntryById',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dnsEntry = await this.CDnsEntry.getDnsEntryById(reqArg.dnsEntryId);
|
||||||
|
if (!dnsEntry) {
|
||||||
|
throw new Error(`DNS entry with id ${reqArg.dnsEntryId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntry: await dnsEntry.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create DNS entry
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(
|
||||||
|
'createDnsEntry',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Validate domain exists and is activated if domainId is provided
|
||||||
|
if (reqArg.dnsEntryData.domainId) {
|
||||||
|
const domain = await this.cloudlyRef.domainManager.CDomain.getDomainById(reqArg.dnsEntryData.domainId);
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${reqArg.dnsEntryData.domainId} not found`);
|
||||||
|
}
|
||||||
|
if ((domain.data as any).activationState !== 'activated') {
|
||||||
|
throw new Error(`Domain ${domain.data.name} is not activated; DNS changes are not allowed.`);
|
||||||
|
}
|
||||||
|
// Set the zone from the domain name
|
||||||
|
reqArg.dnsEntryData.zone = domain.data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnsEntry = await this.CDnsEntry.createDnsEntry(reqArg.dnsEntryData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntry: await dnsEntry.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update DNS entry
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(
|
||||||
|
'updateDnsEntry',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Validate domain exists and is activated if domainId is provided
|
||||||
|
if (reqArg.dnsEntryData.domainId) {
|
||||||
|
const domain = await this.cloudlyRef.domainManager.CDomain.getDomainById(reqArg.dnsEntryData.domainId);
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${reqArg.dnsEntryData.domainId} not found`);
|
||||||
|
}
|
||||||
|
if ((domain.data as any).activationState !== 'activated') {
|
||||||
|
throw new Error(`Domain ${domain.data.name} is not activated; DNS changes are not allowed.`);
|
||||||
|
}
|
||||||
|
// Set the zone from the domain name
|
||||||
|
reqArg.dnsEntryData.zone = domain.data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnsEntry = await this.CDnsEntry.updateDnsEntry(
|
||||||
|
reqArg.dnsEntryId,
|
||||||
|
reqArg.dnsEntryData
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsEntry: await dnsEntry.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete DNS entry
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(
|
||||||
|
'deleteDnsEntry',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const success = await this.CDnsEntry.deleteDnsEntry(reqArg.dnsEntryId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get DNS zones
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsZones>(
|
||||||
|
'getDnsZones',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const zones = await this.CDnsEntry.getDnsZones();
|
||||||
|
|
||||||
|
return {
|
||||||
|
zones,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a DNS entry for a service
|
||||||
|
* @param dnsEntryData The DNS entry data
|
||||||
|
*/
|
||||||
|
public async createServiceDnsEntry(dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data']) {
|
||||||
|
// If domainId is provided, get the domain and set the zone
|
||||||
|
if (dnsEntryData.domainId) {
|
||||||
|
const domain = await this.cloudlyRef.domainManager.CDomain.getInstance({
|
||||||
|
id: dnsEntryData.domainId,
|
||||||
|
});
|
||||||
|
if (domain) {
|
||||||
|
dnsEntryData.zone = domain.data.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the DNS entry
|
||||||
|
const dnsEntry = await this.CDnsEntry.createDnsEntry(dnsEntryData);
|
||||||
|
return dnsEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate DNS entries for a service when it's deployed
|
||||||
|
* @param serviceId The service ID
|
||||||
|
*/
|
||||||
|
public async activateServiceDnsEntries(serviceId: string) {
|
||||||
|
const dnsEntries = await this.CDnsEntry.getInstances({
|
||||||
|
'data.sourceServiceId': serviceId,
|
||||||
|
'data.sourceType': 'service',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of dnsEntries) {
|
||||||
|
entry.data.active = true;
|
||||||
|
entry.data.updatedAt = Date.now();
|
||||||
|
await entry.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate DNS entries for a service when it's undeployed
|
||||||
|
* @param serviceId The service ID
|
||||||
|
*/
|
||||||
|
public async deactivateServiceDnsEntries(serviceId: string) {
|
||||||
|
const dnsEntries = await this.CDnsEntry.getInstances({
|
||||||
|
'data.sourceServiceId': serviceId,
|
||||||
|
'data.sourceType': 'service',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of dnsEntries) {
|
||||||
|
entry.data.active = false;
|
||||||
|
entry.data.updatedAt = Date.now();
|
||||||
|
await entry.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all DNS entries for a service
|
||||||
|
* @param serviceId The service ID
|
||||||
|
*/
|
||||||
|
public async removeServiceDnsEntries(serviceId: string) {
|
||||||
|
const dnsEntries = await this.CDnsEntry.getInstances({
|
||||||
|
'data.sourceServiceId': serviceId,
|
||||||
|
'data.sourceType': 'service',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of dnsEntries) {
|
||||||
|
await entry.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update DNS entry values when deployment happens
|
||||||
|
* @param serviceId The service ID
|
||||||
|
* @param ipAddress The IP address to set for the DNS entries
|
||||||
|
*/
|
||||||
|
public async updateServiceDnsEntriesIp(serviceId: string, ipAddress: string) {
|
||||||
|
const dnsEntries = await this.CDnsEntry.getInstances({
|
||||||
|
'data.sourceServiceId': serviceId,
|
||||||
|
'data.sourceType': 'service',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of dnsEntries) {
|
||||||
|
if (entry.data.type === 'A' || entry.data.type === 'AAAA') {
|
||||||
|
entry.data.value = ipAddress;
|
||||||
|
entry.data.updatedAt = Date.now();
|
||||||
|
await entry.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the DNS manager
|
||||||
|
*/
|
||||||
|
public async init() {
|
||||||
|
console.log('DNS Manager initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the DNS manager
|
||||||
|
*/
|
||||||
|
public async stop() {
|
||||||
|
console.log('DNS Manager stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
211
ts/manager.domain/classes.domain.ts
Normal file
211
ts/manager.domain/classes.domain.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DomainManager } from './classes.domainmanager.js';
|
||||||
|
|
||||||
|
@plugins.smartdata.managed()
|
||||||
|
export class Domain extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
Domain,
|
||||||
|
plugins.servezoneInterfaces.data.IDomain,
|
||||||
|
DomainManager
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async getDomainById(domainIdArg: string) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
id: domainIdArg,
|
||||||
|
});
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDomainByName(domainNameArg: string) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
'data.name': domainNameArg,
|
||||||
|
});
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDomains() {
|
||||||
|
const domains = await this.getInstances({});
|
||||||
|
return domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createDomain(domainDataArg: plugins.servezoneInterfaces.data.IDomain['data']) {
|
||||||
|
const domain = new Domain();
|
||||||
|
domain.id = await Domain.getNewId();
|
||||||
|
domain.data = {
|
||||||
|
...domainDataArg,
|
||||||
|
status: domainDataArg.status || 'pending',
|
||||||
|
verificationStatus: domainDataArg.verificationStatus || 'pending',
|
||||||
|
nameservers: domainDataArg.nameservers || [],
|
||||||
|
autoRenew: domainDataArg.autoRenew !== false,
|
||||||
|
activationState: domainDataArg.activationState || 'available',
|
||||||
|
syncSource: domainDataArg.syncSource ?? null,
|
||||||
|
lastSyncAt: domainDataArg.lastSyncAt,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
await domain.save();
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async updateDomain(
|
||||||
|
domainIdArg: string,
|
||||||
|
domainDataArg: Partial<plugins.servezoneInterfaces.data.IDomain['data']>
|
||||||
|
) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
id: domainIdArg,
|
||||||
|
});
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${domainIdArg} not found`);
|
||||||
|
}
|
||||||
|
// Merge updates and respect incoming activationState when provided
|
||||||
|
Object.assign(domain.data, domainDataArg);
|
||||||
|
domain.data.updatedAt = Date.now();
|
||||||
|
// Ensure activationState has a sensible default if still missing
|
||||||
|
if (!domain.data.activationState) {
|
||||||
|
(domain.data as any).activationState = 'available';
|
||||||
|
}
|
||||||
|
await domain.save();
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteDomain(domainIdArg: string) {
|
||||||
|
const domain = await this.getInstance({
|
||||||
|
id: domainIdArg,
|
||||||
|
});
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${domainIdArg} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are DNS entries for this domain
|
||||||
|
const dnsManager = domain.manager.cloudlyRef.dnsManager;
|
||||||
|
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
|
||||||
|
'data.zone': domain.data.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dnsEntries.length > 0) {
|
||||||
|
console.log(`Warning: Deleting domain ${domain.data.name} with ${dnsEntries.length} DNS entries`);
|
||||||
|
// Optionally delete associated DNS entries
|
||||||
|
for (const dnsEntry of dnsEntries) {
|
||||||
|
await dnsEntry.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await domain.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IDomain['data'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify domain ownership
|
||||||
|
*/
|
||||||
|
public async verifyDomain(methodArg?: 'dns' | 'http' | 'email' | 'manual') {
|
||||||
|
const method = methodArg || this.data.verificationMethod || 'dns';
|
||||||
|
|
||||||
|
// Generate verification token if not exists
|
||||||
|
if (!this.data.verificationToken) {
|
||||||
|
this.data.verificationToken = plugins.smartunique.shortId();
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
let verificationResult = {
|
||||||
|
success: false,
|
||||||
|
message: '',
|
||||||
|
details: {} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'dns':
|
||||||
|
// Check for TXT record with verification token
|
||||||
|
verificationResult = await this.verifyViaDns();
|
||||||
|
break;
|
||||||
|
case 'http':
|
||||||
|
// Check for file at well-known URL
|
||||||
|
verificationResult = await this.verifyViaHttp();
|
||||||
|
break;
|
||||||
|
case 'email':
|
||||||
|
// Send verification email
|
||||||
|
verificationResult = await this.verifyViaEmail();
|
||||||
|
break;
|
||||||
|
case 'manual':
|
||||||
|
// Manual verification
|
||||||
|
verificationResult.success = true;
|
||||||
|
verificationResult.message = 'Manually verified';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update verification status
|
||||||
|
if (verificationResult.success) {
|
||||||
|
this.data.verificationStatus = 'verified';
|
||||||
|
this.data.lastVerificationAt = Date.now();
|
||||||
|
this.data.verificationMethod = method;
|
||||||
|
} else {
|
||||||
|
this.data.verificationStatus = 'failed';
|
||||||
|
this.data.lastVerificationAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
return verificationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyViaDns(): Promise<{ success: boolean; message: string; details: any }> {
|
||||||
|
// TODO: Implement DNS verification
|
||||||
|
// Look for TXT record _cloudly-verify.{domain} with value {verificationToken}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'DNS verification not yet implemented',
|
||||||
|
details: {
|
||||||
|
expectedRecord: `_cloudly-verify.${this.data.name}`,
|
||||||
|
expectedValue: this.data.verificationToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyViaHttp(): Promise<{ success: boolean; message: string; details: any }> {
|
||||||
|
// TODO: Implement HTTP verification
|
||||||
|
// Check for file at http://{domain}/.well-known/cloudly-verify.txt
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'HTTP verification not yet implemented',
|
||||||
|
details: {
|
||||||
|
expectedUrl: `http://${this.data.name}/.well-known/cloudly-verify.txt`,
|
||||||
|
expectedContent: this.data.verificationToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async verifyViaEmail(): Promise<{ success: boolean; message: string; details: any }> {
|
||||||
|
// TODO: Implement email verification
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Email verification not yet implemented',
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if domain is expiring soon
|
||||||
|
*/
|
||||||
|
public isExpiringSoon(daysThreshold: number = 30): boolean {
|
||||||
|
if (!this.data.expiresAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const daysUntilExpiry = (this.data.expiresAt - Date.now()) / (1000 * 60 * 60 * 24);
|
||||||
|
return daysUntilExpiry <= daysThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all DNS entries for this domain
|
||||||
|
*/
|
||||||
|
public async getDnsEntries() {
|
||||||
|
const dnsManager = this.manager.cloudlyRef.dnsManager;
|
||||||
|
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
|
||||||
|
'data.zone': this.data.name,
|
||||||
|
});
|
||||||
|
return dnsEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
188
ts/manager.domain/classes.domainmanager.ts
Normal file
188
ts/manager.domain/classes.domainmanager.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Domain } from './classes.domain.js';
|
||||||
|
|
||||||
|
export class DomainManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CDomain = plugins.smartdata.setDefaultManagerForDoc(this, Domain);
|
||||||
|
|
||||||
|
constructor(cloudlyRef: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
// Get all domains
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(
|
||||||
|
'getDomains',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domains = await this.CDomain.getDomains();
|
||||||
|
|
||||||
|
return {
|
||||||
|
domains: await Promise.all(
|
||||||
|
domains.map((domain) => domain.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get domain by ID
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomainById>(
|
||||||
|
'getDomainById',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domain = await this.CDomain.getDomainById(reqArg.domainId);
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${reqArg.domainId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(
|
||||||
|
'createDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if domain already exists
|
||||||
|
const existingDomain = await this.CDomain.getDomainByName(reqArg.domainData.name);
|
||||||
|
if (existingDomain) {
|
||||||
|
throw new Error(`Domain ${reqArg.domainData.name} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = await this.CDomain.createDomain(reqArg.domainData);
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(
|
||||||
|
'updateDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domain = await this.CDomain.updateDomain(
|
||||||
|
reqArg.domainId,
|
||||||
|
reqArg.domainData
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(
|
||||||
|
'deleteDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const success = await this.CDomain.deleteDomain(reqArg.domainId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify domain
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(
|
||||||
|
'verifyDomain',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const domain = await this.CDomain.getDomainById(reqArg.domainId);
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error(`Domain with id ${reqArg.domainId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationResult = await domain.verifyDomain(reqArg.verificationMethod);
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: await domain.createSavableObject(),
|
||||||
|
verificationResult,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the domain manager
|
||||||
|
*/
|
||||||
|
public async init() {
|
||||||
|
console.log('Domain Manager initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the domain manager
|
||||||
|
*/
|
||||||
|
public async stop() {
|
||||||
|
console.log('Domain Manager stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active domains
|
||||||
|
*/
|
||||||
|
public async getActiveDomains() {
|
||||||
|
const domains = await this.CDomain.getInstances({
|
||||||
|
'data.status': 'active',
|
||||||
|
});
|
||||||
|
return domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get domains that are expiring soon
|
||||||
|
*/
|
||||||
|
public async getExpiringDomains(daysThreshold: number = 30) {
|
||||||
|
const domains = await this.CDomain.getDomains();
|
||||||
|
return domains.filter(domain => domain.isExpiringSoon(daysThreshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a domain name is available (not in our system)
|
||||||
|
*/
|
||||||
|
public async isDomainAvailable(domainName: string): Promise<boolean> {
|
||||||
|
const existingDomain = await this.CDomain.getDomainByName(domainName);
|
||||||
|
return !existingDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import * as paths from '../paths.js';
|
|||||||
import type { Cloudly } from 'ts/classes.cloudly.js';
|
import type { Cloudly } from 'ts/classes.cloudly.js';
|
||||||
import type { ExternalRegistryManager } from './classes.externalregistrymanager.js';
|
import type { ExternalRegistryManager } from './classes.externalregistrymanager.js';
|
||||||
|
|
||||||
|
@plugins.smartdata.managed()
|
||||||
export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> {
|
export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> {
|
||||||
// STATIC
|
// STATIC
|
||||||
public static async getRegistryById(registryIdArg: string) {
|
public static async getRegistryById(registryIdArg: string) {
|
||||||
@@ -17,14 +18,92 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
|
|||||||
return externalRegistries;
|
return externalRegistries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getDefaultRegistry(type: 'docker' | 'npm' = 'docker') {
|
||||||
|
const defaultRegistry = await this.getInstance({
|
||||||
|
'data.type': type,
|
||||||
|
'data.isDefault': true,
|
||||||
|
});
|
||||||
|
return defaultRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) {
|
public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) {
|
||||||
const externalRegistry = new ExternalRegistry();
|
const externalRegistry = new ExternalRegistry();
|
||||||
externalRegistry.id = await ExternalRegistry.getNewId();
|
externalRegistry.id = await this.getNewId();
|
||||||
Object.assign(externalRegistry, registryDataArg);
|
externalRegistry.data = {
|
||||||
|
type: registryDataArg.type || 'docker',
|
||||||
|
name: registryDataArg.name || '',
|
||||||
|
url: registryDataArg.url || '',
|
||||||
|
username: registryDataArg.username,
|
||||||
|
password: registryDataArg.password,
|
||||||
|
description: registryDataArg.description,
|
||||||
|
isDefault: registryDataArg.isDefault || false,
|
||||||
|
authType: registryDataArg.authType || (registryDataArg.username || registryDataArg.password ? 'basic' : 'none'),
|
||||||
|
insecure: registryDataArg.insecure || false,
|
||||||
|
namespace: registryDataArg.namespace,
|
||||||
|
proxy: registryDataArg.proxy,
|
||||||
|
config: registryDataArg.config,
|
||||||
|
status: 'unverified',
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is set as default, unset other defaults of the same type
|
||||||
|
if (externalRegistry.data.isDefault) {
|
||||||
|
const existingDefaults = await this.getInstances({
|
||||||
|
'data.type': externalRegistry.data.type,
|
||||||
|
'data.isDefault': true,
|
||||||
|
});
|
||||||
|
for (const existingDefault of existingDefaults) {
|
||||||
|
existingDefault.data.isDefault = false;
|
||||||
|
await existingDefault.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await externalRegistry.save();
|
await externalRegistry.save();
|
||||||
return externalRegistry;
|
return externalRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async updateExternalRegistry(
|
||||||
|
registryIdArg: string,
|
||||||
|
registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>
|
||||||
|
) {
|
||||||
|
const externalRegistry = await this.getRegistryById(registryIdArg);
|
||||||
|
if (!externalRegistry) {
|
||||||
|
throw new Error(`Registry with id ${registryIdArg} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If setting as default, unset other defaults of the same type
|
||||||
|
if (registryDataArg.isDefault && !externalRegistry.data.isDefault) {
|
||||||
|
const existingDefaults = await this.getInstances({
|
||||||
|
'data.type': externalRegistry.data.type,
|
||||||
|
'data.isDefault': true,
|
||||||
|
});
|
||||||
|
for (const existingDefault of existingDefaults) {
|
||||||
|
if (existingDefault.id !== registryIdArg) {
|
||||||
|
existingDefault.data.isDefault = false;
|
||||||
|
await existingDefault.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
Object.assign(externalRegistry.data, registryDataArg, {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await externalRegistry.save();
|
||||||
|
return externalRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteExternalRegistry(registryIdArg: string) {
|
||||||
|
const externalRegistry = await this.getRegistryById(registryIdArg);
|
||||||
|
if (!externalRegistry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await externalRegistry.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
@@ -37,4 +116,91 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the registry connection
|
||||||
|
*/
|
||||||
|
public async verifyConnection(): Promise<{ success: boolean; message?: string }> {
|
||||||
|
try {
|
||||||
|
// For Docker registries, try to access the v2 API
|
||||||
|
if (this.data.type === 'docker') {
|
||||||
|
const registryUrl = this.data.url.replace(/\/$/, ''); // Remove trailing slash
|
||||||
|
|
||||||
|
// Build headers based on auth type
|
||||||
|
const headers: any = {};
|
||||||
|
if (this.data.authType === 'basic' && this.data.username && this.data.password) {
|
||||||
|
headers['Authorization'] = 'Basic ' + Buffer.from(`${this.data.username}:${this.data.password}`).toString('base64');
|
||||||
|
} else if (this.data.authType === 'token' && this.data.password) {
|
||||||
|
// For token auth, password field contains the token
|
||||||
|
headers['Authorization'] = `Bearer ${this.data.password}`;
|
||||||
|
}
|
||||||
|
// For 'none' auth type or missing credentials, no auth header is added
|
||||||
|
|
||||||
|
// Try to access the Docker Registry v2 API
|
||||||
|
const response = await fetch(`${registryUrl}/v2/`, {
|
||||||
|
headers,
|
||||||
|
// Allow insecure if configured
|
||||||
|
...(this.data.insecure ? { rejectUnauthorized: false } : {}),
|
||||||
|
}).catch(err => {
|
||||||
|
throw new Error(`Failed to connect: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
// 200 means successful (either public or authenticated)
|
||||||
|
this.data.status = 'active';
|
||||||
|
this.data.lastVerified = Date.now();
|
||||||
|
this.data.lastError = undefined;
|
||||||
|
await this.save();
|
||||||
|
return { success: true, message: 'Registry connection successful' };
|
||||||
|
} else if (response.status === 401 && this.data.authType === 'none') {
|
||||||
|
// 401 with no auth means registry exists but needs auth
|
||||||
|
throw new Error('Registry requires authentication');
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
throw new Error('Authentication failed - check credentials');
|
||||||
|
} else {
|
||||||
|
throw new Error(`Registry returned status ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For npm registries, implement npm-specific verification
|
||||||
|
if (this.data.type === 'npm') {
|
||||||
|
// TODO: Implement npm registry verification
|
||||||
|
this.data.status = 'unverified';
|
||||||
|
return { success: false, message: 'NPM registry verification not yet implemented' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: 'Unknown registry type' };
|
||||||
|
} catch (error) {
|
||||||
|
this.data.status = 'error';
|
||||||
|
this.data.lastError = error.message;
|
||||||
|
await this.save();
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the full registry URL with namespace if applicable
|
||||||
|
*/
|
||||||
|
public getFullRegistryUrl(): string {
|
||||||
|
let url = this.data.url.replace(/\/$/, ''); // Remove trailing slash
|
||||||
|
if (this.data.namespace) {
|
||||||
|
url = `${url}/${this.data.namespace}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Docker auth config for this registry
|
||||||
|
*/
|
||||||
|
public getDockerAuthConfig() {
|
||||||
|
if (this.data.type !== 'docker') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: this.data.username,
|
||||||
|
password: this.data.password,
|
||||||
|
email: this.data.config?.dockerConfig?.email,
|
||||||
|
serveraddress: this.data.config?.dockerConfig?.serverAddress || this.data.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,24 +13,35 @@ export class ExternalRegistryManager {
|
|||||||
|
|
||||||
constructor(cloudlyRef: Cloudly) {
|
constructor(cloudlyRef: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRef;
|
this.cloudlyRef = cloudlyRef;
|
||||||
}
|
|
||||||
|
|
||||||
public async start() {
|
// Add typedrouter to cloudly's main router
|
||||||
// lets set up a typedrouter
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
this.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
|
|
||||||
|
// Get registry by ID
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>(
|
||||||
new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => {
|
new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => {
|
||||||
const registry = await ExternalRegistry.getRegistryById(dataArg.id);
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const registry = await this.CExternalRegistry.getRegistryById(dataArg.id);
|
||||||
|
if (!registry) {
|
||||||
|
throw new Error(`Registry with id ${dataArg.id} not found`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
registry: await registry.createSavableObject(),
|
registry: await registry.createSavableObject(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get all registries
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>(
|
||||||
new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => {
|
new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => {
|
||||||
const registries = await ExternalRegistry.getRegistries();
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const registries = await this.CExternalRegistry.getRegistries();
|
||||||
return {
|
return {
|
||||||
registries: await Promise.all(
|
registries: await Promise.all(
|
||||||
registries.map((registry) => registry.createSavableObject())
|
registries.map((registry) => registry.createSavableObject())
|
||||||
@@ -39,13 +50,81 @@ export class ExternalRegistryManager {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create registry
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>(
|
||||||
new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => {
|
new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => {
|
||||||
const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData);
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const registry = await this.CExternalRegistry.createExternalRegistry(dataArg.registryData);
|
||||||
return {
|
return {
|
||||||
registry: await registry.createSavableObject(),
|
registry: await registry.createSavableObject(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update registry
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>(
|
||||||
|
new plugins.typedrequest.TypedHandler('updateExternalRegistry', async (dataArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const registry = await this.CExternalRegistry.updateExternalRegistry(
|
||||||
|
dataArg.registryId,
|
||||||
|
dataArg.registryData
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
resultRegistry: await registry.createSavableObject(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete registry
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>(
|
||||||
|
new plugins.typedrequest.TypedHandler('deleteExternalRegistryById', async (dataArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const success = await this.CExternalRegistry.deleteExternalRegistry(dataArg.registryId);
|
||||||
|
return {
|
||||||
|
ok: success,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify registry connection
|
||||||
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>(
|
||||||
|
new plugins.typedrequest.TypedHandler('verifyExternalRegistry', async (dataArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const registry = await this.CExternalRegistry.getRegistryById(dataArg.registryId);
|
||||||
|
if (!registry) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Registry with id ${dataArg.registryId} not found`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await registry.verifyConnection();
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
message: result.message,
|
||||||
|
registry: await registry.createSavableObject(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
console.log('External Registry Manager started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
console.log('External Registry Manager stopped');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { SecretBundle } from 'ts/manager.secret/classes.secretbundle.js';
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { ServiceManager } from './classes.servicemanager.js';
|
import { ServiceManager } from './classes.servicemanager.js';
|
||||||
|
|
||||||
|
@plugins.smartdata.managed()
|
||||||
export class Service extends plugins.smartdata.SmartDataDbDoc<
|
export class Service extends plugins.smartdata.SmartDataDbDoc<
|
||||||
Service,
|
Service,
|
||||||
plugins.servezoneInterfaces.data.IService,
|
plugins.servezoneInterfaces.data.IService,
|
||||||
@@ -25,6 +26,12 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
service.id = await Service.getNewId();
|
service.id = await Service.getNewId();
|
||||||
Object.assign(service, serviceDataArg);
|
Object.assign(service, serviceDataArg);
|
||||||
await service.save();
|
await service.save();
|
||||||
|
|
||||||
|
// Create DNS entries if service has web port and domains configured
|
||||||
|
if (service.data.ports?.web && service.data.domains?.length > 0) {
|
||||||
|
await service.createDnsEntries();
|
||||||
|
}
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,4 +61,40 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
}
|
}
|
||||||
return finalFlatObject;
|
return finalFlatObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates DNS entries for this service (in inactive state)
|
||||||
|
* These will be activated when the service is deployed
|
||||||
|
*/
|
||||||
|
public async createDnsEntries() {
|
||||||
|
const dnsManager = this.manager.cloudlyRef.dnsManager;
|
||||||
|
|
||||||
|
for (const domain of this.data.domains) {
|
||||||
|
const dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data'] = {
|
||||||
|
type: 'A', // Default to A record, could be made configurable
|
||||||
|
name: domain.name,
|
||||||
|
value: '0.0.0.0', // Placeholder, will be updated on deployment
|
||||||
|
ttl: 3600,
|
||||||
|
zone: '', // Will be set based on domainId
|
||||||
|
domainId: domain.domainId,
|
||||||
|
active: false, // Created as inactive
|
||||||
|
description: `Auto-generated DNS entry for service ${this.data.name}`,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
isAutoGenerated: true,
|
||||||
|
sourceServiceId: this.id,
|
||||||
|
sourceType: 'service',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the DNS entry
|
||||||
|
await dnsManager.createServiceDnsEntry(dnsEntryData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes DNS entries for this service
|
||||||
|
*/
|
||||||
|
public async removeDnsEntries() {
|
||||||
|
const dnsManager = this.manager.cloudlyRef.dnsManager;
|
||||||
|
await dnsManager.removeServiceDnsEntries(this.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export class ServiceManager {
|
|||||||
constructor(cloudlyRef: Cloudly) {
|
constructor(cloudlyRef: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRef;
|
this.cloudlyRef = cloudlyRef;
|
||||||
|
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
|
||||||
'getServices',
|
'getServices',
|
||||||
@@ -89,6 +91,10 @@ export class ServiceManager {
|
|||||||
const service = await Service.getInstance({
|
const service = await Service.getInstance({
|
||||||
id: dataArg.serviceId,
|
id: dataArg.serviceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Remove DNS entries before deleting the service
|
||||||
|
await service.removeDnsEntries();
|
||||||
|
|
||||||
await service.delete();
|
await service.delete();
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -97,4 +103,14 @@ export class ServiceManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
// ServiceManager is ready - handlers are already registered in constructor
|
||||||
|
console.log('ServiceManager started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
// Cleanup if needed
|
||||||
|
console.log('ServiceManager stopped');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
165
ts/manager.task/classes.taskexecution.ts
Normal file
165
ts/manager.task/classes.taskexecution.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { CloudlyTaskManager } from './classes.taskmanager.js';
|
||||||
|
|
||||||
|
@plugins.smartdata.managed()
|
||||||
|
export class TaskExecution extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
TaskExecution,
|
||||||
|
plugins.servezoneInterfaces.data.ITaskExecution,
|
||||||
|
CloudlyTaskManager
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async getTaskExecutionById(executionIdArg: string) {
|
||||||
|
const execution = await this.getInstance({
|
||||||
|
id: executionIdArg,
|
||||||
|
});
|
||||||
|
return execution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getTaskExecutions(filterArg?: {
|
||||||
|
taskName?: string;
|
||||||
|
status?: string;
|
||||||
|
startedAfter?: number;
|
||||||
|
startedBefore?: number;
|
||||||
|
}) {
|
||||||
|
const query: any = {};
|
||||||
|
|
||||||
|
if (filterArg?.taskName) {
|
||||||
|
query['data.taskName'] = filterArg.taskName;
|
||||||
|
}
|
||||||
|
if (filterArg?.status) {
|
||||||
|
query['data.status'] = filterArg.status;
|
||||||
|
}
|
||||||
|
if (filterArg?.startedAfter || filterArg?.startedBefore) {
|
||||||
|
query['data.startedAt'] = {};
|
||||||
|
if (filterArg.startedAfter) {
|
||||||
|
query['data.startedAt'].$gte = filterArg.startedAfter;
|
||||||
|
}
|
||||||
|
if (filterArg.startedBefore) {
|
||||||
|
query['data.startedAt'].$lte = filterArg.startedBefore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const executions = await this.getInstances(query);
|
||||||
|
return executions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createTaskExecution(
|
||||||
|
taskName: string,
|
||||||
|
triggeredBy: 'schedule' | 'manual' | 'system',
|
||||||
|
userId?: string
|
||||||
|
) {
|
||||||
|
const execution = new TaskExecution();
|
||||||
|
execution.id = await TaskExecution.getNewId();
|
||||||
|
execution.data = {
|
||||||
|
taskName,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
status: 'running',
|
||||||
|
triggeredBy,
|
||||||
|
userId,
|
||||||
|
logs: [],
|
||||||
|
};
|
||||||
|
await execution.save();
|
||||||
|
return execution;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.ITaskExecution['data'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a log entry to the execution
|
||||||
|
*/
|
||||||
|
public async addLog(message: string, severity: 'info' | 'warning' | 'error' | 'success' = 'info') {
|
||||||
|
this.data.logs.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
message,
|
||||||
|
severity,
|
||||||
|
});
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a metric for the execution
|
||||||
|
*/
|
||||||
|
public async setMetric(key: string, value: any) {
|
||||||
|
if (!this.data.metrics) {
|
||||||
|
this.data.metrics = {};
|
||||||
|
}
|
||||||
|
this.data.metrics[key] = value;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the execution as completed
|
||||||
|
*/
|
||||||
|
public async complete(result?: any) {
|
||||||
|
this.data.completedAt = Date.now();
|
||||||
|
this.data.duration = this.data.completedAt - this.data.startedAt;
|
||||||
|
this.data.status = 'completed';
|
||||||
|
if (result !== undefined) {
|
||||||
|
this.data.result = result;
|
||||||
|
}
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the execution as failed
|
||||||
|
*/
|
||||||
|
public async fail(error: Error | string) {
|
||||||
|
this.data.completedAt = Date.now();
|
||||||
|
this.data.duration = this.data.completedAt - this.data.startedAt;
|
||||||
|
this.data.status = 'failed';
|
||||||
|
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
this.data.error = {
|
||||||
|
message: error,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.data.error = {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
code: (error as any).code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the execution
|
||||||
|
*/
|
||||||
|
public async cancel() {
|
||||||
|
this.data.completedAt = Date.now();
|
||||||
|
this.data.duration = this.data.completedAt - this.data.startedAt;
|
||||||
|
this.data.status = 'cancelled';
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get running executions
|
||||||
|
*/
|
||||||
|
public static async getRunningExecutions() {
|
||||||
|
return await this.getInstances({
|
||||||
|
'data.status': 'running',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old executions
|
||||||
|
*/
|
||||||
|
public static async cleanupOldExecutions(olderThanDays: number = 30) {
|
||||||
|
const cutoffTime = Date.now() - (olderThanDays * 24 * 60 * 60 * 1000);
|
||||||
|
const oldExecutions = await this.getInstances({
|
||||||
|
'data.completedAt': { $lt: cutoffTime },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const execution of oldExecutions) {
|
||||||
|
await execution.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldExecutions.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
349
ts/manager.task/classes.taskmanager.ts
Normal file
349
ts/manager.task/classes.taskmanager.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import { TaskExecution } from './classes.taskexecution.js';
|
||||||
|
import { createPredefinedTasks } from './predefinedtasks.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
|
export interface ITaskInfo {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
|
||||||
|
schedule?: string; // Cron expression if scheduled
|
||||||
|
lastRun?: number;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CloudlyTaskManager {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
|
||||||
|
// TaskBuffer integration
|
||||||
|
private taskBufferManager = new plugins.taskbuffer.TaskManager();
|
||||||
|
private taskRegistry = new Map<string, plugins.taskbuffer.Task>();
|
||||||
|
private taskInfo = new Map<string, ITaskInfo>();
|
||||||
|
private currentExecutions = new Map<string, TaskExecution>();
|
||||||
|
private cancellationRequests = new Set<string>();
|
||||||
|
|
||||||
|
// Database connection helper
|
||||||
|
get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up TaskExecution document manager
|
||||||
|
public CTaskExecution = plugins.smartdata.setDefaultManagerForDoc(this, TaskExecution);
|
||||||
|
|
||||||
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
|
|
||||||
|
// Add router to main router
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
|
// Set up API endpoints
|
||||||
|
this.setupApiEndpoints();
|
||||||
|
|
||||||
|
// Register predefined tasks
|
||||||
|
createPredefinedTasks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a task with the manager
|
||||||
|
*/
|
||||||
|
public registerTask(
|
||||||
|
name: string,
|
||||||
|
task: plugins.taskbuffer.Task,
|
||||||
|
info: Omit<ITaskInfo, 'name' | 'lastRun'>
|
||||||
|
) {
|
||||||
|
this.taskRegistry.set(name, task);
|
||||||
|
this.taskInfo.set(name, {
|
||||||
|
name,
|
||||||
|
...info,
|
||||||
|
lastRun: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule if cron expression provided
|
||||||
|
if (info.schedule && info.enabled) {
|
||||||
|
this.scheduleTask(name, info.schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', `Registered task: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a task with tracking
|
||||||
|
*/
|
||||||
|
public async executeTask(
|
||||||
|
taskName: string,
|
||||||
|
triggeredBy: 'schedule' | 'manual' | 'system',
|
||||||
|
userId?: string
|
||||||
|
): Promise<TaskExecution> {
|
||||||
|
const task = this.taskRegistry.get(taskName);
|
||||||
|
const info = this.taskInfo.get(taskName);
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
throw new Error(`Task ${taskName} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info?.enabled && triggeredBy === 'schedule') {
|
||||||
|
logger.log('warn', `Skipping disabled scheduled task: ${taskName}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create execution record
|
||||||
|
const execution = await TaskExecution.createTaskExecution(taskName, triggeredBy, userId);
|
||||||
|
|
||||||
|
if (info?.description) {
|
||||||
|
execution.data.taskDescription = info.description;
|
||||||
|
}
|
||||||
|
if (info?.category) {
|
||||||
|
execution.data.category = info.category;
|
||||||
|
}
|
||||||
|
await execution.save();
|
||||||
|
|
||||||
|
// Store current execution for task to access
|
||||||
|
this.currentExecutions.set(taskName, execution);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution.addLog(`Starting task: ${taskName}`, 'info');
|
||||||
|
|
||||||
|
// Execute the task
|
||||||
|
const result = await task.trigger();
|
||||||
|
|
||||||
|
// If a cancellation was requested during execution, don't mark as completed
|
||||||
|
if (execution.data.status === 'cancelled' || this.cancellationRequests.has(execution.id)) {
|
||||||
|
await execution.addLog('Task cancelled during execution', 'warning');
|
||||||
|
} else {
|
||||||
|
// Task completed successfully
|
||||||
|
await execution.complete(result);
|
||||||
|
await execution.addLog(`Task completed successfully`, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last run time
|
||||||
|
if (info) {
|
||||||
|
info.lastRun = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// If already cancelled, don't mark as failed
|
||||||
|
if (execution.data.status === 'cancelled' || this.cancellationRequests.has(execution.id)) {
|
||||||
|
await execution.addLog('Task was cancelled', 'warning');
|
||||||
|
} else {
|
||||||
|
// Task failed
|
||||||
|
await execution.fail(error as any);
|
||||||
|
await execution.addLog(`Task failed: ${(error as any).message}`, 'error');
|
||||||
|
logger.log('error', `Task ${taskName} failed: ${(error as any).message}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up current execution
|
||||||
|
this.currentExecutions.delete(taskName);
|
||||||
|
this.cancellationRequests.delete(execution.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return execution;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current execution for a task (used by tasks to log)
|
||||||
|
*/
|
||||||
|
public getCurrentExecution(taskName: string): TaskExecution | undefined {
|
||||||
|
return this.currentExecutions.get(taskName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a task with cron expression
|
||||||
|
*/
|
||||||
|
public scheduleTask(taskName: string, cronExpression: string) {
|
||||||
|
const task = this.taskRegistry.get(taskName);
|
||||||
|
if (!task) {
|
||||||
|
throw new Error(`Task ${taskName} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap task execution with tracking
|
||||||
|
const wrappedTask = new plugins.taskbuffer.Task({
|
||||||
|
name: `${taskName}-scheduled`,
|
||||||
|
taskFunction: async () => {
|
||||||
|
await this.executeTask(taskName, 'schedule');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.taskBufferManager.addAndScheduleTask(wrappedTask, cronExpression);
|
||||||
|
logger.log('info', `Scheduled task ${taskName} with cron: ${cronExpression}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a running task
|
||||||
|
*/
|
||||||
|
public async cancelTask(executionId: string): Promise<boolean> {
|
||||||
|
const execution = await TaskExecution.getTaskExecutionById(executionId);
|
||||||
|
if (!execution || execution.data.status !== 'running') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await execution.cancel();
|
||||||
|
await execution.addLog('Task cancelled by user', 'warning');
|
||||||
|
// mark cancellation request so running task functions can react cooperatively
|
||||||
|
this.cancellationRequests.add(execution.id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if cancellation is requested for an execution
|
||||||
|
*/
|
||||||
|
public isCancellationRequested(executionId: string): boolean {
|
||||||
|
return this.cancellationRequests.has(executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered tasks
|
||||||
|
*/
|
||||||
|
public getAllTasks(): ITaskInfo[] {
|
||||||
|
return Array.from(this.taskInfo.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable a task
|
||||||
|
*/
|
||||||
|
public async setTaskEnabled(taskName: string, enabled: boolean) {
|
||||||
|
const info = this.taskInfo.get(taskName);
|
||||||
|
if (!info) {
|
||||||
|
throw new Error(`Task ${taskName} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
info.enabled = enabled;
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
// TODO: Remove from scheduler if disabled
|
||||||
|
logger.log('info', `Disabled task: ${taskName}`);
|
||||||
|
} else if (info.schedule) {
|
||||||
|
// Reschedule if enabled with schedule
|
||||||
|
this.scheduleTask(taskName, info.schedule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up API endpoints
|
||||||
|
*/
|
||||||
|
private setupApiEndpoints() {
|
||||||
|
// Get all tasks
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTasks>(
|
||||||
|
'getTasks',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tasks = this.getAllTasks();
|
||||||
|
|
||||||
|
return {
|
||||||
|
tasks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get task executions
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>(
|
||||||
|
'getTaskExecutions',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const executions = await TaskExecution.getTaskExecutions(reqArg.filter);
|
||||||
|
|
||||||
|
return {
|
||||||
|
executions: await Promise.all(
|
||||||
|
executions.map(e => e.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get task execution by ID
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>(
|
||||||
|
'getTaskExecutionById',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const execution = await TaskExecution.getTaskExecutionById(reqArg.executionId);
|
||||||
|
|
||||||
|
if (!execution) {
|
||||||
|
throw new Error('Task execution not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
execution: await execution.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger task manually
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>(
|
||||||
|
'triggerTask',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const execution = await this.executeTask(
|
||||||
|
reqArg.taskName,
|
||||||
|
'manual',
|
||||||
|
reqArg.userId
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
execution: await execution.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cancel task
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_CancelTask>(
|
||||||
|
'cancelTask',
|
||||||
|
async (reqArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(reqArg, [
|
||||||
|
this.cloudlyRef.authManager.validIdentityGuard,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const success = await this.cancelTask(reqArg.executionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the task manager
|
||||||
|
*/
|
||||||
|
public async init() {
|
||||||
|
logger.log('info', 'Task Manager initialized');
|
||||||
|
|
||||||
|
// Clean up old executions on startup
|
||||||
|
const deletedCount = await TaskExecution.cleanupOldExecutions(30);
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
logger.log('info', `Cleaned up ${deletedCount} old task executions`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the task manager
|
||||||
|
*/
|
||||||
|
public async stop() {
|
||||||
|
// Stop all scheduled tasks
|
||||||
|
await this.taskBufferManager.stop();
|
||||||
|
logger.log('info', 'Task Manager stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
566
ts/manager.task/predefinedtasks.ts
Normal file
566
ts/manager.task/predefinedtasks.ts
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { CloudlyTaskManager } from './classes.taskmanager.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and register all predefined tasks
|
||||||
|
*/
|
||||||
|
export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
||||||
|
|
||||||
|
// Cloudflare Domain Sync Task
|
||||||
|
const cfDomainSync = new plugins.taskbuffer.Task({
|
||||||
|
name: 'cloudflare-domain-sync',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('cloudflare-domain-sync');
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Starting Cloudflare domain sync…', 'info');
|
||||||
|
const cf = taskManager.cloudlyRef.cloudflareConnector?.cloudflare;
|
||||||
|
if (!cf) {
|
||||||
|
await execution?.addLog('Cloudflare not configured; skipping sync.', 'warning');
|
||||||
|
return { created: 0, updated: 0, totalZones: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const zones = await cf.convenience.listZones();
|
||||||
|
await execution?.setMetric('totalZones', zones.length);
|
||||||
|
await execution?.addLog(`Fetched ${zones.length} zones from Cloudflare`, 'info');
|
||||||
|
|
||||||
|
let created = 0;
|
||||||
|
let updated = 0;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (const zone of zones) {
|
||||||
|
// zone fields from Cloudflare typings
|
||||||
|
const zoneName = (zone as any).name as string;
|
||||||
|
const zoneId = (zone as any).id as string;
|
||||||
|
const zoneStatus = ((zone as any).status || 'active') as 'active'|'pending'|'suspended'|'transferred'|'expired';
|
||||||
|
const nameServers: string[] = (zone as any).name_servers || [];
|
||||||
|
|
||||||
|
const existing = await taskManager.cloudlyRef.domainManager.CDomain.getDomainByName(zoneName);
|
||||||
|
if (existing) {
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || existing.data == null)) {
|
||||||
|
await execution?.addLog('Cancellation requested. Stopping CF sync…', 'warning');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await execution?.addLog(`Updating domain: ${zoneName}`, 'info');
|
||||||
|
await taskManager.cloudlyRef.domainManager.CDomain.updateDomain(existing.id, {
|
||||||
|
status: zoneStatus as any,
|
||||||
|
nameservers: nameServers,
|
||||||
|
cloudflareZoneId: zoneId,
|
||||||
|
syncSource: 'cloudflare',
|
||||||
|
lastSyncAt: now,
|
||||||
|
activationState: existing.data.activationState || 'available',
|
||||||
|
});
|
||||||
|
updated++;
|
||||||
|
} else {
|
||||||
|
await execution?.addLog(`Creating domain: ${zoneName}`, 'info');
|
||||||
|
await taskManager.cloudlyRef.domainManager.CDomain.createDomain({
|
||||||
|
name: zoneName,
|
||||||
|
description: `Synced from Cloudflare zone ${zoneId}`,
|
||||||
|
status: zoneStatus as any,
|
||||||
|
verificationStatus: 'pending',
|
||||||
|
nameservers: nameServers,
|
||||||
|
autoRenew: true,
|
||||||
|
cloudflareZoneId: zoneId,
|
||||||
|
activationState: 'available',
|
||||||
|
syncSource: 'cloudflare',
|
||||||
|
lastSyncAt: now,
|
||||||
|
} as any);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await execution?.setMetric('created', created);
|
||||||
|
await execution?.setMetric('updated', updated);
|
||||||
|
await execution?.addLog(`Cloudflare sync done: ${created} created, ${updated} updated`, 'success');
|
||||||
|
return { created, updated, totalZones: zones.length };
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Cloudflare sync error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('cloudflare-domain-sync', cfDomainSync, {
|
||||||
|
description: 'Import and update domains from Cloudflare zones',
|
||||||
|
category: 'system',
|
||||||
|
schedule: '0 3 * * *', // Daily at 3 AM
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// DNS Sync Task
|
||||||
|
const dnsSync = new plugins.taskbuffer.Task({
|
||||||
|
name: 'dns-sync',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('dns-sync');
|
||||||
|
const dnsManager = taskManager.cloudlyRef.dnsManager;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Starting DNS synchronization...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Aborting DNS sync...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all DNS entries marked as external
|
||||||
|
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
|
||||||
|
'data.sourceType': 'external',
|
||||||
|
});
|
||||||
|
|
||||||
|
await execution?.addLog(`Found ${dnsEntries.length} external DNS entries to sync`, 'info');
|
||||||
|
await execution?.setMetric('totalEntries', dnsEntries.length);
|
||||||
|
|
||||||
|
let syncedCount = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
|
||||||
|
for (const entry of dnsEntries) {
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// TODO: Implement actual sync with external DNS provider
|
||||||
|
await execution?.addLog(`Syncing DNS entry: ${entry.data.name}.${entry.data.zone}`, 'info');
|
||||||
|
syncedCount++;
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Failed to sync ${entry.data.name}: ${error.message}`, 'warning');
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await execution?.setMetric('syncedCount', syncedCount);
|
||||||
|
await execution?.setMetric('failedCount', failedCount);
|
||||||
|
await execution?.addLog(`DNS sync completed: ${syncedCount} synced, ${failedCount} failed`, 'success');
|
||||||
|
|
||||||
|
return { synced: syncedCount, failed: failedCount };
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`DNS sync error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('dns-sync', dnsSync, {
|
||||||
|
description: 'Synchronize DNS entries with external providers',
|
||||||
|
category: 'system',
|
||||||
|
schedule: '0 */6 * * *', // Every 6 hours
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Certificate Renewal Task
|
||||||
|
const certRenewal = new plugins.taskbuffer.Task({
|
||||||
|
name: 'cert-renewal',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('cert-renewal');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Checking certificates for renewal...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Aborting certificate renewal...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all domains (only activated ones are considered for renewal)
|
||||||
|
const domains = await taskManager.cloudlyRef.domainManager.CDomain.getInstances({
|
||||||
|
'data.activationState': 'activated',
|
||||||
|
} as any);
|
||||||
|
await execution?.setMetric('totalDomains', domains.length);
|
||||||
|
|
||||||
|
let renewedCount = 0;
|
||||||
|
let upToDateCount = 0;
|
||||||
|
|
||||||
|
for (const domain of domains) {
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO: Check certificate expiry and renew if needed
|
||||||
|
await execution?.addLog(`Checking certificate for ${domain.data.name}`, 'info');
|
||||||
|
|
||||||
|
// Placeholder logic
|
||||||
|
const needsRenewal = Math.random() > 0.8; // 20% chance for demo
|
||||||
|
|
||||||
|
if (needsRenewal) {
|
||||||
|
await execution?.addLog(`Renewing certificate for ${domain.data.name}`, 'info');
|
||||||
|
// TODO: Actual renewal logic
|
||||||
|
renewedCount++;
|
||||||
|
} else {
|
||||||
|
upToDateCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await execution?.setMetric('renewedCount', renewedCount);
|
||||||
|
await execution?.setMetric('upToDateCount', upToDateCount);
|
||||||
|
await execution?.addLog(`Certificate check completed: ${renewedCount} renewed, ${upToDateCount} up to date`, 'success');
|
||||||
|
|
||||||
|
return { renewed: renewedCount, upToDate: upToDateCount };
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Certificate renewal error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('cert-renewal', certRenewal, {
|
||||||
|
description: 'Check and renew SSL certificates',
|
||||||
|
category: 'security',
|
||||||
|
schedule: '0 2 * * *', // Daily at 2 AM
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup Task
|
||||||
|
const cleanup = new plugins.taskbuffer.Task({
|
||||||
|
name: 'cleanup',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('cleanup');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Starting cleanup tasks...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Aborting cleanup...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old task executions
|
||||||
|
await execution?.addLog('Cleaning old task executions...', 'info');
|
||||||
|
const deletedExecutions = await taskManager.CTaskExecution.cleanupOldExecutions(30);
|
||||||
|
await execution?.setMetric('deletedExecutions', deletedExecutions);
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
|
||||||
|
|
||||||
|
// TODO: Clean up old logs
|
||||||
|
await execution?.addLog('Cleaning old logs...', 'info');
|
||||||
|
// Placeholder
|
||||||
|
const deletedLogs = 0;
|
||||||
|
await execution?.setMetric('deletedLogs', deletedLogs);
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
|
||||||
|
|
||||||
|
// TODO: Clean up Docker images
|
||||||
|
await execution?.addLog('Cleaning unused Docker images...', 'info');
|
||||||
|
// Placeholder
|
||||||
|
const deletedImages = 0;
|
||||||
|
await execution?.setMetric('deletedImages', deletedImages);
|
||||||
|
|
||||||
|
await execution?.addLog(`Cleanup completed: ${deletedExecutions} executions, ${deletedLogs} logs, ${deletedImages} images deleted`, 'success');
|
||||||
|
|
||||||
|
return {
|
||||||
|
executions: deletedExecutions,
|
||||||
|
logs: deletedLogs,
|
||||||
|
images: deletedImages,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Cleanup error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('cleanup', cleanup, {
|
||||||
|
description: 'Remove old logs, executions, and temporary files',
|
||||||
|
category: 'cleanup',
|
||||||
|
schedule: '0 3 * * *', // Daily at 3 AM
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health Check Task
|
||||||
|
const healthCheck = new plugins.taskbuffer.Task({
|
||||||
|
name: 'health-check',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('health-check');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Starting health checks...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Aborting health checks...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all deployments
|
||||||
|
const deployments = await taskManager.cloudlyRef.deploymentManager.getAllDeployments();
|
||||||
|
await execution?.setMetric('totalDeployments', deployments.length);
|
||||||
|
|
||||||
|
let healthyCount = 0;
|
||||||
|
let unhealthyCount = 0;
|
||||||
|
const issues = [];
|
||||||
|
|
||||||
|
for (const deployment of deployments) {
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (deployment.status === 'running') {
|
||||||
|
// TODO: Actual health check logic
|
||||||
|
const isHealthy = Math.random() > 0.1; // 90% healthy for demo
|
||||||
|
|
||||||
|
if (isHealthy) {
|
||||||
|
healthyCount++;
|
||||||
|
} else {
|
||||||
|
unhealthyCount++;
|
||||||
|
issues.push({
|
||||||
|
deploymentId: deployment.id,
|
||||||
|
serviceId: deployment.serviceId,
|
||||||
|
issue: 'Health check failed',
|
||||||
|
});
|
||||||
|
await execution?.addLog(`Deployment ${deployment.id} is unhealthy`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await execution?.setMetric('healthyCount', healthyCount);
|
||||||
|
await execution?.setMetric('unhealthyCount', unhealthyCount);
|
||||||
|
await execution?.setMetric('issues', issues);
|
||||||
|
|
||||||
|
const severity = unhealthyCount > 0 ? 'warning' : 'success';
|
||||||
|
await execution?.addLog(
|
||||||
|
`Health check completed: ${healthyCount} healthy, ${unhealthyCount} unhealthy`,
|
||||||
|
severity as any
|
||||||
|
);
|
||||||
|
|
||||||
|
return { healthy: healthyCount, unhealthy: unhealthyCount, issues };
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Health check error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('health-check', healthCheck, {
|
||||||
|
description: 'Monitor service health across deployments',
|
||||||
|
category: 'monitoring',
|
||||||
|
schedule: '*/15 * * * *', // Every 15 minutes
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resource Usage Report
|
||||||
|
const resourceReport = new plugins.taskbuffer.Task({
|
||||||
|
name: 'resource-report',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('resource-report');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Generating resource usage report...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Aborting report...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all nodes
|
||||||
|
const nodes = await taskManager.cloudlyRef.nodeManager.CClusterNode.getInstances({});
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
nodes: [],
|
||||||
|
totalCpu: 0,
|
||||||
|
totalMemory: 0,
|
||||||
|
totalDisk: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO: Get actual resource usage
|
||||||
|
const nodeUsage = {
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: node.data.name,
|
||||||
|
cpu: Math.random() * 100, // Placeholder
|
||||||
|
memory: Math.random() * 100, // Placeholder
|
||||||
|
disk: Math.random() * 100, // Placeholder
|
||||||
|
};
|
||||||
|
|
||||||
|
report.nodes.push(nodeUsage);
|
||||||
|
report.totalCpu += nodeUsage.cpu;
|
||||||
|
report.totalMemory += nodeUsage.memory;
|
||||||
|
report.totalDisk += nodeUsage.disk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate averages
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
report.totalCpu /= nodes.length;
|
||||||
|
report.totalMemory /= nodes.length;
|
||||||
|
report.totalDisk /= nodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
await execution?.setMetric('report', report);
|
||||||
|
await execution?.addLog(
|
||||||
|
`Resource report generated: Avg CPU ${report.totalCpu.toFixed(1)}%, Memory ${report.totalMemory.toFixed(1)}%, Disk ${report.totalDisk.toFixed(1)}%`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
|
return report;
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Resource report error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('resource-report', resourceReport, {
|
||||||
|
description: 'Generate resource usage reports',
|
||||||
|
category: 'monitoring',
|
||||||
|
schedule: '0 * * * *', // Every hour
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Database Maintenance
|
||||||
|
const dbMaintenance = new plugins.taskbuffer.Task({
|
||||||
|
name: 'db-maintenance',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('db-maintenance');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Starting database maintenance...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Aborting DB maintenance...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement actual database maintenance
|
||||||
|
await execution?.addLog('Analyzing indexes...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
|
||||||
|
await execution?.addLog('Compacting collections...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
|
||||||
|
await execution?.addLog('Updating statistics...', 'info');
|
||||||
|
|
||||||
|
await execution?.setMetric('collectionsOptimized', 5); // Placeholder
|
||||||
|
await execution?.setMetric('indexesRebuilt', 3); // Placeholder
|
||||||
|
|
||||||
|
await execution?.addLog('Database maintenance completed', 'success');
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Database maintenance error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('db-maintenance', dbMaintenance, {
|
||||||
|
description: 'Optimize database performance',
|
||||||
|
category: 'maintenance',
|
||||||
|
schedule: '0 4 * * 0', // Weekly on Sunday at 4 AM
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Security Scan
|
||||||
|
const securityScan = new plugins.taskbuffer.Task({
|
||||||
|
name: 'security-scan',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('security-scan');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Starting security scan...', 'info');
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Aborting security scan...', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vulnerabilities = [];
|
||||||
|
|
||||||
|
// Check for exposed ports
|
||||||
|
await execution?.addLog('Checking for exposed ports...', 'info');
|
||||||
|
// TODO: Actual port scanning logic
|
||||||
|
|
||||||
|
// Check for outdated images
|
||||||
|
await execution?.addLog('Checking for outdated images...', 'info');
|
||||||
|
const images = await taskManager.cloudlyRef.imageManager.CImage.getInstances({});
|
||||||
|
|
||||||
|
for (const image of images) {
|
||||||
|
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
||||||
|
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO: Check if image is outdated
|
||||||
|
const isOutdated = Math.random() > 0.7; // 30% outdated for demo
|
||||||
|
|
||||||
|
if (isOutdated) {
|
||||||
|
vulnerabilities.push({
|
||||||
|
type: 'outdated-image',
|
||||||
|
severity: 'medium',
|
||||||
|
image: image.data.name,
|
||||||
|
version: image.data.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for weak passwords
|
||||||
|
await execution?.addLog('Checking for weak configurations...', 'info');
|
||||||
|
// TODO: Configuration checks
|
||||||
|
|
||||||
|
await execution?.setMetric('vulnerabilitiesFound', vulnerabilities.length);
|
||||||
|
await execution?.setMetric('vulnerabilities', vulnerabilities);
|
||||||
|
|
||||||
|
const severity = vulnerabilities.length > 0 ? 'warning' : 'success';
|
||||||
|
await execution?.addLog(
|
||||||
|
`Security scan completed: ${vulnerabilities.length} vulnerabilities found`,
|
||||||
|
severity as any
|
||||||
|
);
|
||||||
|
|
||||||
|
return { vulnerabilities };
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Security scan error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('security-scan', securityScan, {
|
||||||
|
description: 'Run security checks on services',
|
||||||
|
category: 'security',
|
||||||
|
schedule: '0 1 * * *', // Daily at 1 AM
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Docker Cleanup
|
||||||
|
const dockerCleanup = new plugins.taskbuffer.Task({
|
||||||
|
name: 'docker-cleanup',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const execution = taskManager.getCurrentExecution('docker-cleanup');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execution?.addLog('Starting Docker cleanup...', 'info');
|
||||||
|
|
||||||
|
// TODO: Implement actual Docker cleanup
|
||||||
|
await execution?.addLog('Removing stopped containers...', 'info');
|
||||||
|
const removedContainers = 0; // Placeholder
|
||||||
|
|
||||||
|
await execution?.addLog('Removing unused images...', 'info');
|
||||||
|
const removedImages = 0; // Placeholder
|
||||||
|
|
||||||
|
await execution?.addLog('Removing unused volumes...', 'info');
|
||||||
|
const removedVolumes = 0; // Placeholder
|
||||||
|
|
||||||
|
await execution?.addLog('Removing unused networks...', 'info');
|
||||||
|
const removedNetworks = 0; // Placeholder
|
||||||
|
|
||||||
|
await execution?.setMetric('removedContainers', removedContainers);
|
||||||
|
await execution?.setMetric('removedImages', removedImages);
|
||||||
|
await execution?.setMetric('removedVolumes', removedVolumes);
|
||||||
|
await execution?.setMetric('removedNetworks', removedNetworks);
|
||||||
|
|
||||||
|
await execution?.addLog(
|
||||||
|
`Docker cleanup completed: ${removedContainers} containers, ${removedImages} images, ${removedVolumes} volumes, ${removedNetworks} networks removed`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
containers: removedContainers,
|
||||||
|
images: removedImages,
|
||||||
|
volumes: removedVolumes,
|
||||||
|
networks: removedNetworks,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await execution?.addLog(`Docker cleanup error: ${error.message}`, 'error');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
taskManager.registerTask('docker-cleanup', dockerCleanup, {
|
||||||
|
description: 'Remove unused Docker images and containers',
|
||||||
|
category: 'cleanup',
|
||||||
|
schedule: '0 5 * * *', // Daily at 5 AM
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log('info', 'Predefined tasks registered successfully');
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import { Cloudly } from '../classes.cloudly.js';
|
|
||||||
|
|
||||||
import { logger } from '../logger.js';
|
|
||||||
|
|
||||||
export class CloudlyTaskmanager {
|
|
||||||
public cloudlyRef: Cloudly;
|
|
||||||
|
|
||||||
constructor(cloudlyRefArg: Cloudly) {
|
|
||||||
this.cloudlyRef = cloudlyRefArg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public everyMinuteTask = new plugins.taskbuffer.Task({
|
|
||||||
taskFunction: async () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
public everyHourTask = new plugins.taskbuffer.Task({
|
|
||||||
taskFunction: async () => {
|
|
||||||
logger.log('info', `Performing hourly maintenance check.`);
|
|
||||||
const configs = await this.cloudlyRef.clusterManager.getAllClusters();
|
|
||||||
logger.log('info', `Got ${configs.length} configs`);
|
|
||||||
configs.map((configArg) => {
|
|
||||||
console.log(configArg.name);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
public everyDayTask = new plugins.taskbuffer.Task({
|
|
||||||
taskFunction: async () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
public everyWeekTask = new plugins.taskbuffer.Task({
|
|
||||||
taskFunction: async () => {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -54,6 +54,21 @@ export class CloudlyApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: resolve HTTP typedrequest endpoint
|
||||||
|
private get httpEndpoint() {
|
||||||
|
const base = (this.cloudlyUrl || '').replace(/\/$/, '');
|
||||||
|
return `${base}/typedrequest`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: choose transport (WS if available, else HTTP)
|
||||||
|
private createWsRequest<T extends plugins.typedRequestInterfaces.ITypedRequest>(operation: string) {
|
||||||
|
return this.typedsocketClient?.createTypedRequest<T>(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createHttpRequest<T extends plugins.typedRequestInterfaces.ITypedRequest>(operation: string) {
|
||||||
|
return new plugins.typedrequest.TypedRequest<T>(this.httpEndpoint, operation);
|
||||||
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
|
this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
|
||||||
this.typedrouter,
|
this.typedrouter,
|
||||||
@@ -165,12 +180,52 @@ export class CloudlyApiClient {
|
|||||||
getRegistryById: async (registryNameArg: string) => {
|
getRegistryById: async (registryNameArg: string) => {
|
||||||
return ExternalRegistry.getExternalRegistryById(this, registryNameArg);
|
return ExternalRegistry.getExternalRegistryById(this, registryNameArg);
|
||||||
},
|
},
|
||||||
|
updateRegistry: async (registryId: string, registryData: plugins.servezoneInterfaces.data.IExternalRegistry['data']): Promise<{ resultRegistry: plugins.servezoneInterfaces.data.IExternalRegistry }> => {
|
||||||
|
const op = 'updateExternalRegistry';
|
||||||
|
const payload = { identity: this.identity, registryId, registryData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>(op).fire(payload);
|
||||||
|
},
|
||||||
|
deleteRegistry: async (registryId: string): Promise<{ ok: boolean }> => {
|
||||||
|
const op = 'deleteExternalRegistryById';
|
||||||
|
const payload = { identity: this.identity, registryId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>(op).fire(payload);
|
||||||
|
},
|
||||||
getRegistries: async () => {
|
getRegistries: async () => {
|
||||||
return ExternalRegistry.getExternalRegistries(this);
|
return ExternalRegistry.getExternalRegistries(this);
|
||||||
},
|
},
|
||||||
createRegistry: async (optionsArg: Parameters<typeof ExternalRegistry.createExternalRegistry>[1]) => {
|
createRegistry: async (optionsArg: Parameters<typeof ExternalRegistry.createExternalRegistry>[1]) => {
|
||||||
return ExternalRegistry.createExternalRegistry(this, optionsArg);
|
return ExternalRegistry.createExternalRegistry(this, optionsArg);
|
||||||
|
},
|
||||||
|
verifyRegistry: async (registryId: string): Promise<{ success: boolean; message: string; registry?: ExternalRegistry }> => {
|
||||||
|
const op = 'verifyExternalRegistry';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>(op);
|
||||||
|
const payload = { identity: this.identity, registryId } as any;
|
||||||
|
const resp = wsReq ? await wsReq.fire(payload) : await this.createHttpRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>(op).fire(payload);
|
||||||
|
let registryInstance: ExternalRegistry | undefined;
|
||||||
|
if (resp.registry) {
|
||||||
|
registryInstance = new ExternalRegistry(this);
|
||||||
|
Object.assign(registryInstance, resp.registry);
|
||||||
}
|
}
|
||||||
|
return { success: resp.success, message: resp.message, registry: registryInstance };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth helpers
|
||||||
|
public async loginWithUsernameAndPassword(username: string, password: string): Promise<plugins.servezoneInterfaces.data.IIdentity> {
|
||||||
|
const op = 'adminLoginWithUsernameAndPassword';
|
||||||
|
// Login endpoint is exposed via HTTP typedrequest
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.admin.IReq_Admin_LoginWithUsernameAndPassword>(op);
|
||||||
|
const response = await httpReq.fire({ username, password });
|
||||||
|
this.identity = response.identity;
|
||||||
|
// If WS connection is available, tag it with identity for server-side guards
|
||||||
|
if (this.typedsocketClient) {
|
||||||
|
try { this.typedsocketClient.addTag('identity', this.identity); } catch {}
|
||||||
|
}
|
||||||
|
return this.identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public image = {
|
public image = {
|
||||||
@@ -183,6 +238,13 @@ export class CloudlyApiClient {
|
|||||||
},
|
},
|
||||||
createImage: async (optionsArg: Parameters<typeof Image.createImage>[1]) => {
|
createImage: async (optionsArg: Parameters<typeof Image.createImage>[1]) => {
|
||||||
return Image.createImage(this, optionsArg);
|
return Image.createImage(this, optionsArg);
|
||||||
|
},
|
||||||
|
deleteImage: async (imageId: string): Promise<void> => {
|
||||||
|
const op = 'deleteImage';
|
||||||
|
const payload = { identity: this.identity, imageId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(op);
|
||||||
|
if (wsReq) { await wsReq.fire(payload); return; }
|
||||||
|
await this.createHttpRequest<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(op).fire(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +258,20 @@ export class CloudlyApiClient {
|
|||||||
},
|
},
|
||||||
createService: async (optionsArg: Parameters<typeof Service.createService>[1]) => {
|
createService: async (optionsArg: Parameters<typeof Service.createService>[1]) => {
|
||||||
return Service.createService(this, optionsArg);
|
return Service.createService(this, optionsArg);
|
||||||
|
},
|
||||||
|
updateService: async (serviceId: string, serviceData: plugins.servezoneInterfaces.data.IService['data']): Promise<{ service: plugins.servezoneInterfaces.data.IService }> => {
|
||||||
|
const op = 'updateService';
|
||||||
|
const payload = { identity: this.identity, serviceId, serviceData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(op).fire(payload);
|
||||||
|
},
|
||||||
|
deleteService: async (serviceId: string): Promise<void> => {
|
||||||
|
const op = 'deleteServiceById';
|
||||||
|
const payload = { identity: this.identity, serviceId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(op);
|
||||||
|
if (wsReq) { await wsReq.fire(payload); return; }
|
||||||
|
await this.createHttpRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(op).fire(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +285,14 @@ export class CloudlyApiClient {
|
|||||||
},
|
},
|
||||||
createCluster: async (optionsArg: Parameters<typeof Cluster.createCluster>[1]) => {
|
createCluster: async (optionsArg: Parameters<typeof Cluster.createCluster>[1]) => {
|
||||||
return Cluster.createCluster(this, optionsArg);
|
return Cluster.createCluster(this, optionsArg);
|
||||||
|
},
|
||||||
|
createClusterAdvanced: async (clusterName: string, setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean') => {
|
||||||
|
const op = 'createCluster';
|
||||||
|
const payload: any = { identity: this.identity, clusterName };
|
||||||
|
if (setupMode) payload.setupMode = setupMode;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(op).fire(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +306,13 @@ export class CloudlyApiClient {
|
|||||||
},
|
},
|
||||||
createSecretBundle: async (optionsArg: Parameters<typeof SecretBundle.createSecretBundle>[1]) => {
|
createSecretBundle: async (optionsArg: Parameters<typeof SecretBundle.createSecretBundle>[1]) => {
|
||||||
return SecretBundle.createSecretBundle(this, optionsArg);
|
return SecretBundle.createSecretBundle(this, optionsArg);
|
||||||
|
},
|
||||||
|
deleteSecretBundleById: async (secretBundleId: string): Promise<{ ok: boolean }> => {
|
||||||
|
const op = 'deleteSecretBundleById';
|
||||||
|
const payload = { identity: this.identity, secretBundleId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_DeleteSecretBundleById>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_DeleteSecretBundleById>(op).fire(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +326,260 @@ export class CloudlyApiClient {
|
|||||||
},
|
},
|
||||||
createSecretGroup: async (optionsArg: Parameters<typeof SecretGroup.createSecretGroup>[1]) => {
|
createSecretGroup: async (optionsArg: Parameters<typeof SecretGroup.createSecretGroup>[1]) => {
|
||||||
return SecretGroup.createSecretGroup(this, optionsArg);
|
return SecretGroup.createSecretGroup(this, optionsArg);
|
||||||
|
},
|
||||||
|
deleteSecretGroupById: async (secretGroupId: string): Promise<{ ok: boolean }> => {
|
||||||
|
const op = 'deleteSecretGroupById';
|
||||||
|
const payload = { identity: this.identity, secretGroupId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_DeleteSecretGroupById>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_DeleteSecretGroupById>(op).fire(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings API
|
||||||
|
public settings = {
|
||||||
|
getSettings: async (): Promise<{
|
||||||
|
settings: plugins.servezoneInterfaces.data.ICloudlySettingsMasked
|
||||||
|
}> => {
|
||||||
|
const op = 'getSettings';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.settings.IRequest_GetSettings>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.settings.IRequest_GetSettings>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity });
|
||||||
|
},
|
||||||
|
updateSettings: async (updates: Partial<plugins.servezoneInterfaces.data.ICloudlySettings>): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}> => {
|
||||||
|
const op = 'updateSettings';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity, updates });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity, updates });
|
||||||
|
},
|
||||||
|
testProviderConnection: async (provider: string): Promise<{
|
||||||
|
connectionValid: boolean;
|
||||||
|
message: string;
|
||||||
|
}> => {
|
||||||
|
const op = 'testProviderConnection';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity, provider: provider as any });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity, provider: provider as any });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task API
|
||||||
|
public tasks = {
|
||||||
|
getTasks: async (): Promise<{
|
||||||
|
tasks: Array<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
|
||||||
|
schedule?: string;
|
||||||
|
lastRun?: number;
|
||||||
|
enabled: boolean;
|
||||||
|
}>
|
||||||
|
}> => {
|
||||||
|
const op = 'getTasks';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTasks>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTasks>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity });
|
||||||
|
},
|
||||||
|
getTaskExecutions: async (filter?: any): Promise<{
|
||||||
|
executions: plugins.servezoneInterfaces.data.ITaskExecution[];
|
||||||
|
}> => {
|
||||||
|
const op = 'getTaskExecutions';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity, filter });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity, filter });
|
||||||
|
},
|
||||||
|
getTaskExecutionById: async (executionId: string): Promise<{
|
||||||
|
execution: plugins.servezoneInterfaces.data.ITaskExecution
|
||||||
|
}> => {
|
||||||
|
const op = 'getTaskExecutionById';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity, executionId });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity, executionId });
|
||||||
|
},
|
||||||
|
triggerTask: async (taskName: string, userId?: string): Promise<{
|
||||||
|
execution: plugins.servezoneInterfaces.data.ITaskExecution
|
||||||
|
}> => {
|
||||||
|
const op = 'triggerTask';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity, taskName, userId });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity, taskName, userId });
|
||||||
|
},
|
||||||
|
cancelTask: async (executionId: string): Promise<{ success: boolean }> => {
|
||||||
|
const op = 'cancelTask';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_CancelTask>(op);
|
||||||
|
if (wsReq) {
|
||||||
|
return wsReq.fire({ identity: this.identity, executionId });
|
||||||
|
}
|
||||||
|
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_CancelTask>(op);
|
||||||
|
return httpReq.fire({ identity: this.identity, executionId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain API
|
||||||
|
public domains = {
|
||||||
|
getDomains: async (): Promise<{ domains: plugins.servezoneInterfaces.data.IDomain[] }> => {
|
||||||
|
const op = 'getDomains';
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(op);
|
||||||
|
if (wsReq) return wsReq.fire({ identity: this.identity });
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(op).fire({ identity: this.identity });
|
||||||
|
},
|
||||||
|
getDomainById: async (domainId: string): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain }> => {
|
||||||
|
const op = 'getDomainById';
|
||||||
|
const payload = { identity: this.identity, domainId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomainById>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomainById>(op).fire(payload);
|
||||||
|
},
|
||||||
|
createDomain: async (domainData: plugins.servezoneInterfaces.data.IDomain['data']): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain }> => {
|
||||||
|
const op = 'createDomain';
|
||||||
|
const payload = { identity: this.identity, domainData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(op).fire(payload);
|
||||||
|
},
|
||||||
|
updateDomain: async (domainId: string, domainData: Partial<plugins.servezoneInterfaces.data.IDomain['data']>): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain }> => {
|
||||||
|
const op = 'updateDomain';
|
||||||
|
const payload = { identity: this.identity, domainId, domainData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(op).fire(payload);
|
||||||
|
},
|
||||||
|
deleteDomain: async (domainId: string): Promise<{ success: boolean }> => {
|
||||||
|
const op = 'deleteDomain';
|
||||||
|
const payload = { identity: this.identity, domainId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(op).fire(payload);
|
||||||
|
},
|
||||||
|
verifyDomain: async (domainId: string, verificationMethod?: 'dns' | 'http' | 'email' | 'manual'): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain; verificationResult: any }> => {
|
||||||
|
const op = 'verifyDomain';
|
||||||
|
const payload = { identity: this.identity, domainId, verificationMethod } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(op).fire(payload);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// DNS API
|
||||||
|
public dns = {
|
||||||
|
getDnsEntries: async (zone?: string): Promise<{ dnsEntries: plugins.servezoneInterfaces.data.IDnsEntry[] }> => {
|
||||||
|
const op = 'getDnsEntries';
|
||||||
|
const payload = { identity: this.identity, zone } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(op).fire(payload);
|
||||||
|
},
|
||||||
|
getDnsEntryById: async (dnsEntryId: string): Promise<{ dnsEntry: plugins.servezoneInterfaces.data.IDnsEntry }> => {
|
||||||
|
const op = 'getDnsEntryById';
|
||||||
|
const payload = { identity: this.identity, dnsEntryId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntryById>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntryById>(op).fire(payload);
|
||||||
|
},
|
||||||
|
createDnsEntry: async (dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data']): Promise<{ dnsEntry: plugins.servezoneInterfaces.data.IDnsEntry }> => {
|
||||||
|
const op = 'createDnsEntry';
|
||||||
|
const payload = { identity: this.identity, dnsEntryData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(op).fire(payload);
|
||||||
|
},
|
||||||
|
updateDnsEntry: async (dnsEntryId: string, dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data']): Promise<{ dnsEntry: plugins.servezoneInterfaces.data.IDnsEntry }> => {
|
||||||
|
const op = 'updateDnsEntry';
|
||||||
|
const payload = { identity: this.identity, dnsEntryId, dnsEntryData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(op).fire(payload);
|
||||||
|
},
|
||||||
|
deleteDnsEntry: async (dnsEntryId: string): Promise<{ success: boolean }> => {
|
||||||
|
const op = 'deleteDnsEntry';
|
||||||
|
const payload = { identity: this.identity, dnsEntryId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(op).fire(payload);
|
||||||
|
},
|
||||||
|
getDnsZones: async (): Promise<{ zones: string[] }> => {
|
||||||
|
const op = 'getDnsZones';
|
||||||
|
const payload = { identity: this.identity } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsZones>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsZones>(op).fire(payload);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deployment API
|
||||||
|
public deployments = {
|
||||||
|
getDeployments: async (): Promise<{ deployments: plugins.servezoneInterfaces.data.IDeployment[] }> => {
|
||||||
|
const op = 'getDeployments';
|
||||||
|
const payload = { identity: this.identity } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(op).fire(payload);
|
||||||
|
},
|
||||||
|
getDeploymentById: async (deploymentId: string): Promise<{ deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
|
||||||
|
const op = 'getDeploymentById';
|
||||||
|
const payload = { identity: this.identity, deploymentId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentById>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentById>(op).fire(payload);
|
||||||
|
},
|
||||||
|
createDeployment: async (deploymentData: Partial<plugins.servezoneInterfaces.data.IDeployment>): Promise<{ deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
|
||||||
|
const op = 'createDeployment';
|
||||||
|
const payload = { identity: this.identity, deploymentData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(op).fire(payload);
|
||||||
|
},
|
||||||
|
updateDeployment: async (deploymentId: string, deploymentData: Partial<plugins.servezoneInterfaces.data.IDeployment>): Promise<{ deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
|
||||||
|
const op = 'updateDeployment';
|
||||||
|
const payload = { identity: this.identity, deploymentId, deploymentData } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(op).fire(payload);
|
||||||
|
},
|
||||||
|
deleteDeployment: async (deploymentId: string): Promise<{ success: boolean }> => {
|
||||||
|
const op = 'deleteDeploymentById';
|
||||||
|
const payload = { identity: this.identity, deploymentId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(op).fire(payload);
|
||||||
|
},
|
||||||
|
restartDeployment: async (deploymentId: string): Promise<{ success: boolean; deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
|
||||||
|
const op = 'restartDeployment';
|
||||||
|
const payload = { identity: this.identity, deploymentId } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_RestartDeployment>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_RestartDeployment>(op).fire(payload);
|
||||||
|
},
|
||||||
|
scaleDeployment: async (deploymentId: string, replicas: number): Promise<{ success: boolean; deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
|
||||||
|
const op = 'scaleDeployment';
|
||||||
|
const payload = { identity: this.identity, deploymentId, replicas } as any;
|
||||||
|
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_ScaleDeployment>(op);
|
||||||
|
if (wsReq) return wsReq.fire(payload);
|
||||||
|
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_ScaleDeployment>(op).fire(payload);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ export {
|
|||||||
// @api.global scope
|
// @api.global scope
|
||||||
import * as typedrequest from '@api.global/typedrequest';
|
import * as typedrequest from '@api.global/typedrequest';
|
||||||
import * as typedsocket from '@api.global/typedsocket';
|
import * as typedsocket from '@api.global/typedsocket';
|
||||||
|
import * as typedRequestInterfaces from '@api.global/typedrequest-interfaces';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
typedrequest,
|
typedrequest,
|
||||||
typedsocket
|
typedsocket,
|
||||||
|
typedRequestInterfaces,
|
||||||
}
|
}
|
||||||
|
|
||||||
// @tsclass scope
|
// @tsclass scope
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { CliClient } from "./classes.cliclient.js";
|
import { CliClient } from './classes.cliclient.js';
|
||||||
|
|
||||||
export const runCli = async () => {
|
export const runCli = async () => {
|
||||||
const cliQenv = new plugins.qenv.Qenv();
|
const cliQenv = new plugins.qenv.Qenv();
|
||||||
|
const cloudlyUrl = await cliQenv.getEnvVarOnDemand('CLOUDLY_URL');
|
||||||
|
const token = process.env.CLOUDLY_TOKEN;
|
||||||
|
const username = process.env.CLOUDLY_USERNAME;
|
||||||
|
const password = process.env.CLOUDLY_PASSWORD;
|
||||||
|
|
||||||
const apiClient = new plugins.servezoneApi.CloudlyApiClient({
|
const apiClient = new plugins.servezoneApi.CloudlyApiClient({
|
||||||
registerAs: 'cli',
|
registerAs: 'cli',
|
||||||
cloudlyUrl: await cliQenv.getEnvVarOnDemand('CLOUDLY_URL'),
|
cloudlyUrl,
|
||||||
});
|
});
|
||||||
|
await apiClient.start();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
await apiClient.getIdentityByToken(token, { tagConnection: true, statefullIdentity: true });
|
||||||
|
} else if (username && password) {
|
||||||
|
await apiClient.loginWithUsernameAndPassword(username, password);
|
||||||
|
} else {
|
||||||
|
console.log('No credentials provided. Set CLOUDLY_TOKEN or CLOUDLY_USERNAME/CLOUDLY_PASSWORD.');
|
||||||
|
}
|
||||||
|
|
||||||
const cliClient = new CliClient(apiClient);
|
const cliClient = new CliClient(apiClient);
|
||||||
|
// Default action example: list clusters when invoked without subcommands
|
||||||
|
await cliClient.getClusters();
|
||||||
};
|
};
|
||||||
100
ts_interfaces/data/dns.ts
Normal file
100
ts_interfaces/data/dns.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
export type TDnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA' | 'SRV' | 'CAA' | 'PTR';
|
||||||
|
|
||||||
|
export interface IDnsEntry {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* The DNS record type
|
||||||
|
*/
|
||||||
|
type: TDnsRecordType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DNS record name (e.g., www, @, mail)
|
||||||
|
* @ represents the root domain
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of the DNS record
|
||||||
|
* - For A/AAAA: IP address
|
||||||
|
* - For CNAME: Target domain
|
||||||
|
* - For MX: Mail server hostname
|
||||||
|
* - For TXT: Text value
|
||||||
|
* - For NS: Nameserver hostname
|
||||||
|
* - For SRV: Target hostname
|
||||||
|
* - For CAA: CAA record value
|
||||||
|
* - For PTR: Domain name
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time to live in seconds
|
||||||
|
* Default: 3600 (1 hour)
|
||||||
|
*/
|
||||||
|
ttl: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority (used for MX and SRV records)
|
||||||
|
* Lower values have higher priority
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DNS zone this entry belongs to
|
||||||
|
* e.g., example.com
|
||||||
|
* @deprecated Use domainId instead
|
||||||
|
*/
|
||||||
|
zone: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The domain ID this DNS entry belongs to
|
||||||
|
* Links to the Domain entity
|
||||||
|
*/
|
||||||
|
domainId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional fields for SRV records
|
||||||
|
*/
|
||||||
|
weight?: number;
|
||||||
|
port?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this DNS entry is active
|
||||||
|
*/
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional description for documentation
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the entry was created
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the entry was last updated
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this DNS entry was auto-generated
|
||||||
|
*/
|
||||||
|
isAutoGenerated?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service ID that created this DNS entry (for auto-generated entries)
|
||||||
|
*/
|
||||||
|
sourceServiceId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The source type of this DNS entry
|
||||||
|
* - manual: Created by user through UI/API
|
||||||
|
* - service: Auto-generated from service configuration
|
||||||
|
* - system: Created by system processes
|
||||||
|
* - external: Synced from external DNS providers
|
||||||
|
*/
|
||||||
|
sourceType?: 'manual' | 'service' | 'system' | 'external';
|
||||||
|
};
|
||||||
|
}
|
||||||
124
ts_interfaces/data/domain.ts
Normal file
124
ts_interfaces/data/domain.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
export type TDomainStatus = 'active' | 'pending' | 'expired' | 'suspended' | 'transferred';
|
||||||
|
export type TDomainVerificationStatus = 'verified' | 'pending' | 'failed' | 'not_required';
|
||||||
|
|
||||||
|
export interface IDomain {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* The domain name (e.g., example.com)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description or notes about the domain
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current status of the domain
|
||||||
|
*/
|
||||||
|
status: TDomainStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain verification status
|
||||||
|
*/
|
||||||
|
verificationStatus: TDomainVerificationStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nameservers for the domain
|
||||||
|
*/
|
||||||
|
nameservers: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain registrar information
|
||||||
|
*/
|
||||||
|
registrar?: {
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain registration date (timestamp)
|
||||||
|
*/
|
||||||
|
registeredAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain expiration date (timestamp)
|
||||||
|
*/
|
||||||
|
expiresAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether auto-renewal is enabled
|
||||||
|
*/
|
||||||
|
autoRenew: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DNSSEC enabled
|
||||||
|
*/
|
||||||
|
dnssecEnabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tags for categorization
|
||||||
|
*/
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this domain is primary for the organization
|
||||||
|
*/
|
||||||
|
isPrimary?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSL certificate status
|
||||||
|
*/
|
||||||
|
sslStatus?: 'active' | 'pending' | 'expired' | 'none';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cloudly activation state controls whether we actively manage DNS/certificates
|
||||||
|
* - available: discovered/imported, not actively managed
|
||||||
|
* - activated: actively managed (DNS edits allowed, certs considered)
|
||||||
|
* - ignored: explicitly ignored from management/automation
|
||||||
|
*/
|
||||||
|
activationState?: 'available' | 'activated' | 'ignored';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last verification attempt timestamp
|
||||||
|
*/
|
||||||
|
lastVerificationAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verification method used
|
||||||
|
*/
|
||||||
|
verificationMethod?: 'dns' | 'http' | 'email' | 'manual';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verification token (for DNS/HTTP verification)
|
||||||
|
*/
|
||||||
|
verificationToken?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cloudflare zone ID if managed by Cloudflare
|
||||||
|
*/
|
||||||
|
cloudflareZoneId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync metadata
|
||||||
|
*/
|
||||||
|
syncSource?: 'cloudflare' | 'manual' | null;
|
||||||
|
lastSyncAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether domain is managed externally
|
||||||
|
*/
|
||||||
|
isExternal?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creation timestamp
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,10 +3,108 @@ import * as plugins from '../plugins.js';
|
|||||||
export interface IExternalRegistry {
|
export interface IExternalRegistry {
|
||||||
id: string;
|
id: string;
|
||||||
data: {
|
data: {
|
||||||
|
/**
|
||||||
|
* Registry type
|
||||||
|
*/
|
||||||
type: 'docker' | 'npm';
|
type: 'docker' | 'npm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-readable name for the registry
|
||||||
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry URL (e.g., https://registry.gitlab.com, docker.io)
|
||||||
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
username: string;
|
|
||||||
password: string;
|
/**
|
||||||
|
* Username for authentication (optional for token-based or public registries)
|
||||||
|
*/
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password, access token, or API key for authentication (optional for public registries)
|
||||||
|
*/
|
||||||
|
password?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional description
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this is the default registry for its type
|
||||||
|
*/
|
||||||
|
isDefault?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication type
|
||||||
|
*/
|
||||||
|
authType?: 'none' | 'basic' | 'token' | 'oauth2';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow insecure registry connections (HTTP or self-signed certs)
|
||||||
|
*/
|
||||||
|
insecure?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional namespace/organization for the registry
|
||||||
|
*/
|
||||||
|
namespace?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy configuration
|
||||||
|
*/
|
||||||
|
proxy?: {
|
||||||
|
http?: string;
|
||||||
|
https?: string;
|
||||||
|
noProxy?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry-specific configuration
|
||||||
|
*/
|
||||||
|
config?: {
|
||||||
|
/**
|
||||||
|
* For Docker registries
|
||||||
|
*/
|
||||||
|
dockerConfig?: {
|
||||||
|
email?: string;
|
||||||
|
serverAddress?: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* For npm registries
|
||||||
|
*/
|
||||||
|
npmConfig?: {
|
||||||
|
scope?: string;
|
||||||
|
alwaysAuth?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of the registry connection
|
||||||
|
*/
|
||||||
|
status?: 'active' | 'inactive' | 'error' | 'unverified';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last error message if status is 'error'
|
||||||
|
*/
|
||||||
|
lastError?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the registry was last successfully verified
|
||||||
|
*/
|
||||||
|
lastVerified?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the registry was created
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the registry was last updated
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ export * from './cloudlyconfig.js';
|
|||||||
export * from './cluster.js';
|
export * from './cluster.js';
|
||||||
export * from './config.js';
|
export * from './config.js';
|
||||||
export * from './deployment.js';
|
export * from './deployment.js';
|
||||||
|
export * from './dns.js';
|
||||||
export * from './docker.js';
|
export * from './docker.js';
|
||||||
|
export * from './domain.js';
|
||||||
export * from './event.js';
|
export * from './event.js';
|
||||||
export * from './externalregistry.js';
|
export * from './externalregistry.js';
|
||||||
export * from './image.js';
|
export * from './image.js';
|
||||||
@@ -13,6 +15,7 @@ export * from './clusternode.js';
|
|||||||
export * from './settings.js';
|
export * from './settings.js';
|
||||||
export * from './service.js';
|
export * from './service.js';
|
||||||
export * from './status.js';
|
export * from './status.js';
|
||||||
|
export * from './taskexecution.js';
|
||||||
export * from './traffic.js';
|
export * from './traffic.js';
|
||||||
export * from './user.js';
|
export * from './user.js';
|
||||||
export * from './version.js';
|
export * from './version.js';
|
||||||
|
|||||||
@@ -54,8 +54,22 @@ export interface IService {
|
|||||||
};
|
};
|
||||||
resources?: IServiceRessources;
|
resources?: IServiceRessources;
|
||||||
domains: {
|
domains: {
|
||||||
|
/**
|
||||||
|
* Optional domain ID to specify which domain to use
|
||||||
|
* If not specified, will use the default domain or require manual configuration
|
||||||
|
*/
|
||||||
|
domainId?: string;
|
||||||
|
/**
|
||||||
|
* The subdomain name (e.g., 'api', 'www', '@' for root)
|
||||||
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
* The port to expose (defaults to ports.web if not specified)
|
||||||
|
*/
|
||||||
port?: number;
|
port?: number;
|
||||||
|
/**
|
||||||
|
* The protocol for this domain entry
|
||||||
|
*/
|
||||||
protocol?: 'http' | 'https' | 'ssh';
|
protocol?: 'http' | 'https' | 'ssh';
|
||||||
}[];
|
}[];
|
||||||
deploymentIds: string[];
|
deploymentIds: string[];
|
||||||
|
|||||||
84
ts_interfaces/data/taskexecution.ts
Normal file
84
ts_interfaces/data/taskexecution.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Task execution tracking for the task management system
|
||||||
|
* Tasks themselves are hard-coded using @push.rocks/taskbuffer
|
||||||
|
* This interface tracks execution history and outcomes
|
||||||
|
*/
|
||||||
|
export interface ITaskExecution {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* Name of the task being executed
|
||||||
|
*/
|
||||||
|
taskName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional description of what the task does
|
||||||
|
*/
|
||||||
|
taskDescription?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category for grouping tasks
|
||||||
|
*/
|
||||||
|
category?: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the task started
|
||||||
|
*/
|
||||||
|
startedAt: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when the task completed
|
||||||
|
*/
|
||||||
|
completedAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current status of the task execution
|
||||||
|
*/
|
||||||
|
status: 'running' | 'completed' | 'failed' | 'cancelled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duration in milliseconds
|
||||||
|
*/
|
||||||
|
duration?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the task was triggered
|
||||||
|
*/
|
||||||
|
triggeredBy: 'schedule' | 'manual' | 'system';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User ID if manually triggered
|
||||||
|
*/
|
||||||
|
userId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execution logs
|
||||||
|
*/
|
||||||
|
logs: Array<{
|
||||||
|
timestamp: number;
|
||||||
|
message: string;
|
||||||
|
severity: 'info' | 'warning' | 'error' | 'success';
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task-specific metrics
|
||||||
|
*/
|
||||||
|
metrics?: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Final result/output of the task
|
||||||
|
*/
|
||||||
|
result?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error details if the task failed
|
||||||
|
*/
|
||||||
|
error?: {
|
||||||
|
message: string;
|
||||||
|
stack?: string;
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
141
ts_interfaces/requests/deployment.ts
Normal file
141
ts_interfaces/requests/deployment.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IDeployment } from '../data/deployment.js';
|
||||||
|
import type { IIdentity } from '../data/user.js';
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeploymentById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeploymentById
|
||||||
|
> {
|
||||||
|
method: 'getDeploymentById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeployments
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeployments
|
||||||
|
> {
|
||||||
|
method: 'getDeployments';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployments: IDeployment[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeploymentsByService
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeploymentsByService
|
||||||
|
> {
|
||||||
|
method: 'getDeploymentsByService';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
serviceId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployments: IDeployment[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeploymentsByNode
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeploymentsByNode
|
||||||
|
> {
|
||||||
|
method: 'getDeploymentsByNode';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
nodeId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployments: IDeployment[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_CreateDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_CreateDeployment
|
||||||
|
> {
|
||||||
|
method: 'createDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentData: Partial<IDeployment>;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_UpdateDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_UpdateDeployment
|
||||||
|
> {
|
||||||
|
method: 'updateDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
deploymentData: Partial<IDeployment>;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_DeleteDeploymentById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_DeleteDeploymentById
|
||||||
|
> {
|
||||||
|
method: 'deleteDeploymentById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_RestartDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_RestartDeployment
|
||||||
|
> {
|
||||||
|
method: 'restartDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_ScaleDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_ScaleDeployment
|
||||||
|
> {
|
||||||
|
method: 'scaleDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
replicas: number;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
93
ts_interfaces/requests/dns.ts
Normal file
93
ts_interfaces/requests/dns.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IDnsEntry } from '../data/dns.js';
|
||||||
|
import type { IIdentity } from '../data/user.js';
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDnsEntries
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDnsEntries
|
||||||
|
> {
|
||||||
|
method: 'getDnsEntries';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
zone?: string; // Optional filter by zone
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntries: IDnsEntry[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDnsEntryById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDnsEntryById
|
||||||
|
> {
|
||||||
|
method: 'getDnsEntryById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntry: IDnsEntry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_CreateDnsEntry
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_CreateDnsEntry
|
||||||
|
> {
|
||||||
|
method: 'createDnsEntry';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryData: IDnsEntry['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntry: IDnsEntry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_UpdateDnsEntry
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_UpdateDnsEntry
|
||||||
|
> {
|
||||||
|
method: 'updateDnsEntry';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryId: string;
|
||||||
|
dnsEntryData: IDnsEntry['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
dnsEntry: IDnsEntry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_DeleteDnsEntry
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_DeleteDnsEntry
|
||||||
|
> {
|
||||||
|
method: 'deleteDnsEntry';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
dnsEntryId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDnsZones
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDnsZones
|
||||||
|
> {
|
||||||
|
method: 'getDnsZones';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
zones: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
99
ts_interfaces/requests/domain.ts
Normal file
99
ts_interfaces/requests/domain.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IDomain } from '../data/domain.js';
|
||||||
|
import type { IIdentity } from '../data/user.js';
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDomains
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDomains
|
||||||
|
> {
|
||||||
|
method: 'getDomains';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domains: IDomain[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetDomainById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetDomainById
|
||||||
|
> {
|
||||||
|
method: 'getDomainById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_CreateDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_CreateDomain
|
||||||
|
> {
|
||||||
|
method: 'createDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainData: IDomain['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_UpdateDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_UpdateDomain
|
||||||
|
> {
|
||||||
|
method: 'updateDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
domainData: IDomain['data'];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_DeleteDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_DeleteDomain
|
||||||
|
> {
|
||||||
|
method: 'deleteDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_VerifyDomain
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_VerifyDomain
|
||||||
|
> {
|
||||||
|
method: 'verifyDomain';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
domainId: string;
|
||||||
|
verificationMethod?: 'dns' | 'http' | 'email' | 'manual';
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
domain: IDomain;
|
||||||
|
verificationResult: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
details?: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ export interface IReq_UpdateRegistry extends plugins.typedrequestInterfaces.impl
|
|||||||
method: 'updateExternalRegistry';
|
method: 'updateExternalRegistry';
|
||||||
request: {
|
request: {
|
||||||
identity: userInterfaces.IIdentity;
|
identity: userInterfaces.IIdentity;
|
||||||
|
registryId: string;
|
||||||
registryData: data.IExternalRegistry['data'];
|
registryData: data.IExternalRegistry['data'];
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
@@ -70,3 +71,19 @@ export interface IReq_DeleteRegistryById extends plugins.typedrequestInterfaces.
|
|||||||
ok: boolean;
|
ok: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IReq_VerifyRegistry extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_VerifyRegistry
|
||||||
|
> {
|
||||||
|
method: 'verifyExternalRegistry';
|
||||||
|
request: {
|
||||||
|
identity: userInterfaces.IIdentity;
|
||||||
|
registryId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
registry?: data.IExternalRegistry;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import * as baremetalRequests from './baremetal.js';
|
|||||||
import * as certificateRequests from './certificate.js';
|
import * as certificateRequests from './certificate.js';
|
||||||
import * as clusterRequests from './cluster.js';
|
import * as clusterRequests from './cluster.js';
|
||||||
import * as configRequests from './config.js';
|
import * as configRequests from './config.js';
|
||||||
|
import * as deploymentRequests from './deployment.js';
|
||||||
|
import * as dnsRequests from './dns.js';
|
||||||
|
import * as domainRequests from './domain.js';
|
||||||
import * as externalRegistryRequests from './externalregistry.js';
|
import * as externalRegistryRequests from './externalregistry.js';
|
||||||
import * as identityRequests from './identity.js';
|
import * as identityRequests from './identity.js';
|
||||||
import * as imageRequests from './image.js';
|
import * as imageRequests from './image.js';
|
||||||
@@ -19,6 +22,7 @@ import * as serverRequests from './server.js';
|
|||||||
import * as serviceRequests from './service.js';
|
import * as serviceRequests from './service.js';
|
||||||
import * as settingsRequests from './settings.js';
|
import * as settingsRequests from './settings.js';
|
||||||
import * as statusRequests from './status.js';
|
import * as statusRequests from './status.js';
|
||||||
|
import * as taskRequests from './task.js';
|
||||||
import * as versionRequests from './version.js';
|
import * as versionRequests from './version.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -27,6 +31,9 @@ export {
|
|||||||
certificateRequests as certificate,
|
certificateRequests as certificate,
|
||||||
clusterRequests as cluster,
|
clusterRequests as cluster,
|
||||||
configRequests as config,
|
configRequests as config,
|
||||||
|
deploymentRequests as deployment,
|
||||||
|
dnsRequests as dns,
|
||||||
|
domainRequests as domain,
|
||||||
externalRegistryRequests as externalRegistry,
|
externalRegistryRequests as externalRegistry,
|
||||||
identityRequests as identity,
|
identityRequests as identity,
|
||||||
imageRequests as image,
|
imageRequests as image,
|
||||||
@@ -41,6 +48,7 @@ export {
|
|||||||
serviceRequests as service,
|
serviceRequests as service,
|
||||||
settingsRequests as settings,
|
settingsRequests as settings,
|
||||||
statusRequests as status,
|
statusRequests as status,
|
||||||
|
taskRequests as task,
|
||||||
versionRequests as version,
|
versionRequests as version,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
95
ts_interfaces/requests/task.ts
Normal file
95
ts_interfaces/requests/task.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as data from '../data/index.js';
|
||||||
|
import * as userInterfaces from '../data/user.js';
|
||||||
|
|
||||||
|
// Get all tasks
|
||||||
|
export interface IRequest_Any_Cloudly_GetTasks
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetTasks
|
||||||
|
> {
|
||||||
|
method: 'getTasks';
|
||||||
|
request: {
|
||||||
|
identity: userInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
tasks: Array<{
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
|
||||||
|
schedule?: string;
|
||||||
|
lastRun?: number;
|
||||||
|
enabled: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get task executions
|
||||||
|
export interface IRequest_Any_Cloudly_GetTaskExecutions
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetTaskExecutions
|
||||||
|
> {
|
||||||
|
method: 'getTaskExecutions';
|
||||||
|
request: {
|
||||||
|
identity: userInterfaces.IIdentity;
|
||||||
|
filter?: {
|
||||||
|
taskName?: string;
|
||||||
|
status?: string;
|
||||||
|
startedAfter?: number;
|
||||||
|
startedBefore?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
executions: data.ITaskExecution[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get task execution by ID
|
||||||
|
export interface IRequest_Any_Cloudly_GetTaskExecutionById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetTaskExecutionById
|
||||||
|
> {
|
||||||
|
method: 'getTaskExecutionById';
|
||||||
|
request: {
|
||||||
|
identity: userInterfaces.IIdentity;
|
||||||
|
executionId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
execution: data.ITaskExecution;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger task manually
|
||||||
|
export interface IRequest_Any_Cloudly_TriggerTask
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_TriggerTask
|
||||||
|
> {
|
||||||
|
method: 'triggerTask';
|
||||||
|
request: {
|
||||||
|
identity: userInterfaces.IIdentity;
|
||||||
|
taskName: string;
|
||||||
|
userId?: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
execution: data.ITaskExecution;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel a running task
|
||||||
|
export interface IRequest_Any_Cloudly_CancelTask
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_CancelTask
|
||||||
|
> {
|
||||||
|
method: 'cancelTask';
|
||||||
|
request: {
|
||||||
|
identity: userInterfaces.IIdentity;
|
||||||
|
executionId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/cloudly',
|
name: '@serve.zone/cloudly',
|
||||||
version: '5.2.0',
|
version: '5.3.0',
|
||||||
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
|
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,24 +14,27 @@ export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState>
|
|||||||
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
|
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
|
||||||
async (statePartArg, payloadArg) => {
|
async (statePartArg, payloadArg) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
const trLogin =
|
let identity: plugins.interfaces.data.IIdentity = null;
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.admin.IReq_Admin_LoginWithUsernameAndPassword>(
|
try {
|
||||||
'/typedrequest',
|
identity = await apiClient.loginWithUsernameAndPassword(payloadArg.username, payloadArg.password);
|
||||||
'adminLoginWithUsernameAndPassword'
|
} catch (err) {
|
||||||
);
|
|
||||||
const response = await trLogin.fire({
|
|
||||||
username: payloadArg.username,
|
|
||||||
password: payloadArg.password,
|
|
||||||
}).catch(err => {
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return {
|
|
||||||
...statePartArg.getState(),
|
|
||||||
}
|
}
|
||||||
});
|
const newState = {
|
||||||
return {
|
|
||||||
...currentState,
|
...currentState,
|
||||||
...(response.identity ? { identity: response.identity } : {}),
|
...(identity ? { identity } : {}),
|
||||||
};
|
};
|
||||||
|
try {
|
||||||
|
// Keep shared API client in sync and establish WS for modules using sockets
|
||||||
|
apiClient.identity = identity || null;
|
||||||
|
if (apiClient.identity) {
|
||||||
|
if (!apiClient['typedsocketClient']) {
|
||||||
|
await apiClient.start();
|
||||||
|
}
|
||||||
|
try { apiClient.typedsocketClient.addTag('identity', apiClient.identity); } catch {}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return newState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -47,10 +50,14 @@ export interface IDataState {
|
|||||||
secretGroups?: plugins.interfaces.data.ISecretGroup[];
|
secretGroups?: plugins.interfaces.data.ISecretGroup[];
|
||||||
secretBundles?: plugins.interfaces.data.ISecretBundle[];
|
secretBundles?: plugins.interfaces.data.ISecretBundle[];
|
||||||
clusters?: plugins.interfaces.data.ICluster[];
|
clusters?: plugins.interfaces.data.ICluster[];
|
||||||
|
externalRegistries?: plugins.interfaces.data.IExternalRegistry[];
|
||||||
images?: any[];
|
images?: any[];
|
||||||
services?: any[];
|
services?: plugins.interfaces.data.IService[];
|
||||||
deployments?: any[];
|
deployments?: plugins.interfaces.data.IDeployment[];
|
||||||
dns?: any[];
|
domains?: plugins.interfaces.data.IDomain[];
|
||||||
|
dnsEntries?: plugins.interfaces.data.IDnsEntry[];
|
||||||
|
tasks?: any[];
|
||||||
|
taskExecutions?: plugins.interfaces.data.ITaskExecution[];
|
||||||
mails?: any[];
|
mails?: any[];
|
||||||
logs?: any[];
|
logs?: any[];
|
||||||
s3?: any[];
|
s3?: any[];
|
||||||
@@ -63,10 +70,14 @@ export const dataState = await appstate.getStatePart<IDataState>(
|
|||||||
secretGroups: [],
|
secretGroups: [],
|
||||||
secretBundles: [],
|
secretBundles: [],
|
||||||
clusters: [],
|
clusters: [],
|
||||||
|
externalRegistries: [],
|
||||||
images: [],
|
images: [],
|
||||||
services: [],
|
services: [],
|
||||||
deployments: [],
|
deployments: [],
|
||||||
dns: [],
|
domains: [],
|
||||||
|
dnsEntries: [],
|
||||||
|
tasks: [],
|
||||||
|
taskExecutions: [],
|
||||||
mails: [],
|
mails: [],
|
||||||
logs: [],
|
logs: [],
|
||||||
s3: [],
|
s3: [],
|
||||||
@@ -76,83 +87,204 @@ export const dataState = await appstate.getStatePart<IDataState>(
|
|||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Shared API client instance (used by UI actions)
|
||||||
|
export const apiClient = new plugins.servezoneApi.CloudlyApiClient({
|
||||||
|
registerAs: 'api',
|
||||||
|
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
// Getting data
|
// Getting data
|
||||||
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
// SecretsGroups
|
// SecretsGroups
|
||||||
const trGetSecretGroups =
|
try {
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretgroup.IReq_GetSecretGroups>(
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
'/typedrequest',
|
const secretGroups = await apiClient.secretgroup.getSecretGroups();
|
||||||
'getSecretGroups'
|
|
||||||
);
|
|
||||||
const response = await trGetSecretGroups.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
});
|
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
secretGroups: response.secretGroups,
|
secretGroups: secretGroups,
|
||||||
};
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch secret groups:', err);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
secretGroups: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// SecretBundles
|
// SecretBundles
|
||||||
const trGetSecretBundles =
|
try {
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretbundle.IReq_GetSecretBundles>(
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
'/typedrequest',
|
const responseSecretBundles = await apiClient.secretbundle.getSecretBundles();
|
||||||
'getSecretBundles'
|
|
||||||
);
|
|
||||||
const responseSecretBundles = await trGetSecretBundles.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
});
|
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
secretBundles: responseSecretBundles.secretBundles,
|
secretBundles: responseSecretBundles,
|
||||||
};
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch secret bundles:', err);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
secretBundles: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// images
|
// images
|
||||||
const trGetImages =
|
try {
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_GetAllImages>(
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
'/typedrequest',
|
const images = await apiClient.image.getImages();
|
||||||
'getAllImages'
|
|
||||||
);
|
|
||||||
const responseImages = await trGetImages.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
});
|
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
images: responseImages.images,
|
images: images,
|
||||||
};
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch images:', err);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
images: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Clusters
|
// Clusters
|
||||||
const trGetClusters =
|
try {
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>(
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
'/typedrequest',
|
const clusters = await apiClient.cluster.getClusters();
|
||||||
'getClusters'
|
|
||||||
);
|
|
||||||
const responseClusters = await trGetClusters.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
});
|
|
||||||
|
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
clusters: responseClusters.clusters,
|
clusters: clusters,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch clusters:', err);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
clusters: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// External Registries via shared API client
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const registries = await apiClient.externalRegistry.getRegistries();
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
externalRegistries: registries,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch external registries:', error);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
externalRegistries: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const services = await apiClient.services.getServices();
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
services: services,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch services:', error);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
services: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deployments
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const responseDeployments = await apiClient.deployments.getDeployments();
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
deployments: responseDeployments?.deployments || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch deployments:', error);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
deployments: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domains via API client
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const responseDomains = await apiClient.domains.getDomains();
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
domains: responseDomains?.domains || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch domains:', error);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
domains: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS Entries via API client
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const responseDnsEntries = await apiClient.dns.getDnsEntries();
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
dnsEntries: responseDnsEntries?.dnsEntries || [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch DNS entries:', error);
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
dnsEntries: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentState;
|
return currentState;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Service Actions
|
||||||
|
export const createServiceAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.services.createService(payloadArg.serviceData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateServiceAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.services.updateService(payloadArg.serviceId, payloadArg.serviceData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteServiceAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { serviceId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.services.deleteService(payloadArg.serviceId);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// SecretGroup Actions
|
// SecretGroup Actions
|
||||||
export const createSecretGroupAction = dataState.createAction(
|
export const createSecretGroupAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => {
|
async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
const trCreateSecretGroup =
|
try {
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretgroup.IReq_CreateSecretGroup>(
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
'/typedrequest',
|
await apiClient.secretgroup.createSecretGroup(payloadArg.data);
|
||||||
'createSecretGroup'
|
|
||||||
);
|
|
||||||
const response = await trCreateSecretGroup.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
secretGroup: payloadArg,
|
|
||||||
});
|
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create secret group:', err);
|
||||||
|
}
|
||||||
return currentState;
|
return currentState;
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
@@ -161,16 +293,13 @@ export const createSecretGroupAction = dataState.createAction(
|
|||||||
export const deleteSecretGroupAction = dataState.createAction(
|
export const deleteSecretGroupAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { secretGroupId: string }) => {
|
async (statePartArg, payloadArg: { secretGroupId: string }) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
const trDeleteSecretGroup =
|
try {
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretgroup.IReq_DeleteSecretGroupById>(
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
'/typedrequest',
|
await apiClient.secretgroup.deleteSecretGroupById(payloadArg.secretGroupId);
|
||||||
'deleteSecretGroupById'
|
|
||||||
);
|
|
||||||
const response = await trDeleteSecretGroup.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
secretGroupId: payloadArg.secretGroupId,
|
|
||||||
});
|
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete secret group:', err);
|
||||||
|
}
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -179,16 +308,13 @@ export const deleteSecretGroupAction = dataState.createAction(
|
|||||||
export const deleteSecretBundleAction = dataState.createAction(
|
export const deleteSecretBundleAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { configBundleId: string }) => {
|
async (statePartArg, payloadArg: { configBundleId: string }) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
const trDeleteConfigBundle =
|
try {
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretbundle.IReq_DeleteSecretBundleById>(
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
'/typedrequest',
|
await apiClient.secretbundle.deleteSecretBundleById(payloadArg.configBundleId);
|
||||||
'deleteSecretBundleById'
|
|
||||||
);
|
|
||||||
const response = await trDeleteConfigBundle.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
secretBundleId: payloadArg.configBundleId,
|
|
||||||
});
|
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete secret bundle:', err);
|
||||||
|
}
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -197,22 +323,9 @@ export const deleteSecretBundleAction = dataState.createAction(
|
|||||||
export const createImageAction = dataState.createAction(
|
export const createImageAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { imageName: string, description: string }) => {
|
async (statePartArg, payloadArg: { imageName: string, description: string }) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
const trCreateImage =
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_CreateImage>(
|
await apiClient.image.createImage({ name: payloadArg.imageName, description: payloadArg.description });
|
||||||
'/typedrequest',
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
'createImage'
|
|
||||||
);
|
|
||||||
const response = await trCreateImage.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
name: payloadArg.imageName,
|
|
||||||
description: payloadArg.description,
|
|
||||||
});
|
|
||||||
currentState = {
|
|
||||||
...currentState,
|
|
||||||
...{
|
|
||||||
images: [...currentState.images, response.image],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -220,25 +333,237 @@ export const createImageAction = dataState.createAction(
|
|||||||
export const deleteImageAction = dataState.createAction(
|
export const deleteImageAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { imageId: string }) => {
|
async (statePartArg, payloadArg: { imageId: string }) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
const trDeleteImage =
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_DeleteImage>(
|
await apiClient.image.deleteImage(payloadArg.imageId);
|
||||||
'/typedrequest',
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
'deleteImage'
|
|
||||||
);
|
|
||||||
const response = await trDeleteImage.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
imageId: payloadArg.imageId,
|
|
||||||
});
|
|
||||||
currentState = {
|
|
||||||
...currentState,
|
|
||||||
...{
|
|
||||||
images: currentState.images.filter((image) => image.id !== payloadArg.imageId),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Deployment Actions
|
||||||
|
export const createDeploymentAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.deployments.createDeployment(payloadArg.deploymentData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateDeploymentAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteDeploymentAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { deploymentId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.deployments.deleteDeployment(payloadArg.deploymentId);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// DNS Actions
|
||||||
|
export const createDnsEntryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.dns.createDnsEntry(payloadArg.dnsEntryData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateDnsEntryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.dns.updateDnsEntry(payloadArg.dnsEntryId, payloadArg.dnsEntryData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteDnsEntryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { dnsEntryId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.dns.deleteDnsEntry(payloadArg.dnsEntryId);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Domain Actions
|
||||||
|
export const createDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.domains.createDomain(payloadArg.domainData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.domains.updateDomain(payloadArg.domainId, payloadArg.domainData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.domains.deleteDomain(payloadArg.domainId);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const verifyDomainAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.domains.verifyDomain(payloadArg.domainId, payloadArg.verificationMethod);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// External Registry Actions
|
||||||
|
export const createExternalRegistryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.externalRegistry.createRegistry(payloadArg.registryData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateExternalRegistryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.externalRegistry.updateRegistry(payloadArg.registryId, payloadArg.registryData);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update external registry:', err);
|
||||||
|
}
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteExternalRegistryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { registryId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.externalRegistry.deleteRegistry(payloadArg.registryId);
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete external registry:', err);
|
||||||
|
}
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const verifyExternalRegistryAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { registryId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
try {
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const result = await apiClient.externalRegistry.verifyRegistry(payloadArg.registryId);
|
||||||
|
if (result.success && result.registry) {
|
||||||
|
const regs = (currentState.externalRegistries || []).slice();
|
||||||
|
const idx = regs.findIndex(r => r.id === payloadArg.registryId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
// Preserve instance; update its data + shallow props
|
||||||
|
const instance: any = regs[idx];
|
||||||
|
instance.data = result.registry.data;
|
||||||
|
instance.id = result.registry.id;
|
||||||
|
regs[idx] = instance;
|
||||||
|
}
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
externalRegistries: regs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to verify external registry:', err);
|
||||||
|
}
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Task Actions
|
||||||
|
export const taskActions = {
|
||||||
|
getTasks: dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: {}) => {
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const response = await apiClient.tasks.getTasks();
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
tasks: response.tasks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
getTaskExecutions: dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { filter?: any }) => {
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
const response = await apiClient.tasks.getTaskExecutions(payloadArg.filter);
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
taskExecutions: response.executions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
getTaskExecutionById: dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { executionId: string }) => {
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.tasks.getTaskExecutionById(payloadArg.executionId);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
triggerTask: dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { taskName: string; userId?: string }) => {
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.tasks.triggerTask(payloadArg.taskName, payloadArg.userId);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
cancelTask: dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { executionId: string }) => {
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
|
await apiClient.tasks.cancelTask(payloadArg.executionId);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
// cluster
|
// cluster
|
||||||
export const addClusterAction = dataState.createAction(
|
export const addClusterAction = dataState.createAction(
|
||||||
async (
|
async (
|
||||||
@@ -249,21 +574,8 @@ export const addClusterAction = dataState.createAction(
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
const trAddCluster =
|
apiClient.identity = loginStatePart.getState().identity;
|
||||||
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IRequest_CreateCluster>(
|
await apiClient.cluster.createClusterAdvanced(payloadArg.clusterName, payloadArg.setupMode);
|
||||||
'/typedrequest',
|
return await dataState.dispatchAction(getAllDataAction, null);
|
||||||
'createCluster'
|
|
||||||
);
|
|
||||||
const response = await trAddCluster.fire({
|
|
||||||
identity: loginStatePart.getState().identity,
|
|
||||||
...payloadArg,
|
|
||||||
});
|
|
||||||
currentState = {
|
|
||||||
...currentState,
|
|
||||||
...{
|
|
||||||
clusters: [...currentState.clusters, response.cluster],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return currentState;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,21 +11,23 @@ import {
|
|||||||
html,
|
html,
|
||||||
state
|
state
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
import { CloudlyViewBackups } from './cloudly-view-backups.js';
|
import { CloudlyViewBackups } from './views/backups/index.js';
|
||||||
import { CloudlyViewClusters } from './cloudly-view-clusters.js';
|
import { CloudlyViewClusters } from './views/clusters/index.js';
|
||||||
import { CloudlyViewDbs } from './cloudly-view-dbs.js';
|
import { CloudlyViewDbs } from './views/dbs/index.js';
|
||||||
import { CloudlyViewDeployments } from './cloudly-view-deployments.js';
|
import { CloudlyViewDeployments } from './views/deployments/index.js';
|
||||||
import { CloudlyViewDns } from './cloudly-view-dns.js';
|
import { CloudlyViewDns } from './views/dns/index.js';
|
||||||
import { CloudlyViewImages } from './cloudly-view-images.js';
|
import { CloudlyViewDomains } from './views/domains/index.js';
|
||||||
import { CloudlyViewLogs } from './cloudly-view-logs.js';
|
import { CloudlyViewImages } from './views/images/index.js';
|
||||||
import { CloudlyViewMails } from './cloudly-view-mails.js';
|
import { CloudlyViewLogs } from './views/logs/index.js';
|
||||||
import { CloudlyViewOverview } from './cloudly-view-overview.js';
|
import { CloudlyViewMails } from './views/mails/index.js';
|
||||||
import { CloudlyViewS3 } from './cloudly-view-s3.js';
|
import { CloudlyViewOverview } from './views/overview/index.js';
|
||||||
import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js';
|
import { CloudlyViewS3 } from './views/s3/index.js';
|
||||||
import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js';
|
import { CloudlyViewSecretBundles } from './views/secretbundles/index.js';
|
||||||
import { CloudlyViewServices } from './cloudly-view-services.js';
|
import { CloudlyViewSecretGroups } from './views/secretgroups/index.js';
|
||||||
import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js';
|
import { CloudlyViewServices } from './views/services/index.js';
|
||||||
import { CloudlyViewSettings } from './cloudly-view-settings.js';
|
import { CloudlyViewExternalRegistries } from './views/externalregistries/index.js';
|
||||||
|
import { CloudlyViewSettings } from './views/settings/index.js';
|
||||||
|
import { CloudlyViewTasks } from './views/tasks/index.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -42,39 +44,8 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
clusters: [],
|
clusters: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
// Keep view tabs stable across renders to preserve active selection
|
||||||
super();
|
private readonly viewTabs: plugins.deesCatalog.IView[] = [
|
||||||
document.title = `cloudly v${commitinfo.version}`;
|
|
||||||
const subcription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subcription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
.maincontainer {
|
|
||||||
position: relative;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 24px;
|
|
||||||
font-family: 'Cal Sans';
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<div class="maincontainer">
|
|
||||||
<dees-simple-login name="cloudly v${commitinfo.version}">
|
|
||||||
<dees-simple-appdash name="cloudly v${commitinfo.version}"
|
|
||||||
.viewTabs=${[
|
|
||||||
{
|
{
|
||||||
name: 'Overview',
|
name: 'Overview',
|
||||||
iconName: 'lucide:LayoutDashboard',
|
iconName: 'lucide:LayoutDashboard',
|
||||||
@@ -125,6 +96,16 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
iconName: 'lucide:Rocket',
|
iconName: 'lucide:Rocket',
|
||||||
element: CloudlyViewDeployments,
|
element: CloudlyViewDeployments,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Tasks',
|
||||||
|
iconName: 'lucide:ListChecks',
|
||||||
|
element: CloudlyViewTasks,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Domains',
|
||||||
|
iconName: 'lucide:Globe2',
|
||||||
|
element: CloudlyViewDomains,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'DNS',
|
name: 'DNS',
|
||||||
iconName: 'lucide:Globe',
|
iconName: 'lucide:Globe',
|
||||||
@@ -159,8 +140,42 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
name: 'Fleet',
|
name: 'Fleet',
|
||||||
iconName: 'lucide:Truck',
|
iconName: 'lucide:Truck',
|
||||||
element: CloudlyViewBackups,
|
element: CloudlyViewBackups,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
document.title = `cloudly v${commitinfo.version}`;
|
||||||
|
const subcription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subcription);
|
||||||
}
|
}
|
||||||
] as plugins.deesCatalog.IView[]}
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
.maincontainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: 'Cal Sans';
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<div class="maincontainer">
|
||||||
|
<dees-simple-login name="cloudly v${commitinfo.version}">
|
||||||
|
<dees-simple-appdash name="cloudly v${commitinfo.version}"
|
||||||
|
.viewTabs=${this.viewTabs}
|
||||||
></dees-simple-appdash>
|
></dees-simple-appdash>
|
||||||
</dees-simple-login>
|
</dees-simple-login>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,6 +217,13 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
console.log(loginState);
|
console.log(loginState);
|
||||||
if (loginState.identity) {
|
if (loginState.identity) {
|
||||||
this.identity = loginState.identity;
|
this.identity = loginState.identity;
|
||||||
|
try {
|
||||||
|
appstate.apiClient.identity = loginState.identity;
|
||||||
|
if (!appstate.apiClient['typedsocketClient']) {
|
||||||
|
await appstate.apiClient.start();
|
||||||
|
}
|
||||||
|
try { appstate.apiClient.typedsocketClient.addTag('identity', appstate.apiClient.identity); } catch {}
|
||||||
|
} catch (e) { console.warn('Failed to initialize API client WS', e); }
|
||||||
await simpleLogin.switchToSlottedContent();
|
await simpleLogin.switchToSlottedContent();
|
||||||
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
|
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-backups')
|
|
||||||
export class CloudlyViewBackups extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Backups</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'Backups'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.backups}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-clusters')
|
|
||||||
export class CloudlyViewClusters extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Clusters</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'Clusters'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.clusters}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
console.log(itemArg);
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add cluster',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add Cluster',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'clusterName'}
|
|
||||||
.label=${'cluster name'}
|
|
||||||
.description=${'a descriptive name for the cluster'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-dropdown
|
|
||||||
.key=${'setupMode'}
|
|
||||||
.label=${'Setup Mode'}
|
|
||||||
.description=${'How the cluster infrastructure should be managed'}
|
|
||||||
.options=${[
|
|
||||||
{option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'},
|
|
||||||
{option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'},
|
|
||||||
{option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true},
|
|
||||||
{option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true}
|
|
||||||
]}
|
|
||||||
.selectedOption=${'manual'}
|
|
||||||
></dees-input-dropdown>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'create',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
const data: {
|
|
||||||
clusterName: string;
|
|
||||||
setupMode: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
|
||||||
} = (await modalArg.shadowRoot
|
|
||||||
.querySelector('dees-form')
|
|
||||||
.collectFormData()) as any;
|
|
||||||
await appstate.dataState.dispatchAction(appstate.addClusterAction, data);
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-dbs')
|
|
||||||
export class CloudlyViewDbs extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>DBs</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'DBs'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.dbs}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-deployments')
|
|
||||||
export class CloudlyViewDeployments extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'Deployments'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.deployments}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-dns')
|
|
||||||
export class CloudlyViewDns extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>DNS</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'DNS'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.deployments}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-externalregistries')
|
|
||||||
export class CloudlyViewExternalRegistries extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>External Registries</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'External Registries'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.deployments}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-images')
|
|
||||||
export class CloudlyViewImages extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Images</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
heading1="Images"
|
|
||||||
heading2="an image is needed for running a service"
|
|
||||||
.data=${this.data.images}
|
|
||||||
.displayFunction=${(image: plugins.interfaces.data.IImage) => {
|
|
||||||
return {
|
|
||||||
id: image.id,
|
|
||||||
name: image.data.name,
|
|
||||||
description: image.data.description,
|
|
||||||
versions: image.data.versions.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'create Image',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
iconName: 'plus',
|
|
||||||
actionFunc: async () => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'create new Image',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text
|
|
||||||
.label=${'name'}
|
|
||||||
.key=${'data.name'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.label=${'description'}
|
|
||||||
.key=${'data.description'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'save',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
|
|
||||||
const formData = await deesForm.collectFormData();
|
|
||||||
console.log(`Prepare saving of data:`);
|
|
||||||
console.log(formData);
|
|
||||||
await appstate.dataState.dispatchAction(appstate.createImageAction, {
|
|
||||||
imageName: formData['data.name'] as string,
|
|
||||||
description: formData['data.description'] as string,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'edit',
|
|
||||||
type: ['contextmenu', 'inRow', 'doubleClick'],
|
|
||||||
iconName: 'penToSquare',
|
|
||||||
actionFunc: async (
|
|
||||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
|
||||||
) => {
|
|
||||||
const environmentsArray: Array<
|
|
||||||
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
|
|
||||||
environment: string;
|
|
||||||
}
|
|
||||||
> = [];
|
|
||||||
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
|
|
||||||
environmentsArray.push({
|
|
||||||
environment: environmentName,
|
|
||||||
...dataArg.item.data.environments[environmentName],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Edit Secret',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'id'}
|
|
||||||
.disabled=${true}
|
|
||||||
.label=${'ID'}
|
|
||||||
.value=${dataArg.item.id}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.name'}
|
|
||||||
.disabled=${false}
|
|
||||||
.label=${'name'}
|
|
||||||
.value=${dataArg.item.data.name}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.description'}
|
|
||||||
.disabled=${false}
|
|
||||||
.label=${'description'}
|
|
||||||
.value=${dataArg.item.data.description}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.key'}
|
|
||||||
.disabled=${false}
|
|
||||||
.label=${'key'}
|
|
||||||
.value=${dataArg.item.data.key}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-table
|
|
||||||
.key=${'environments'}
|
|
||||||
.heading1=${'Environments'}
|
|
||||||
.heading2=${'double-click to edit values'}
|
|
||||||
.data=${environmentsArray.map((itemArg) => {
|
|
||||||
return {
|
|
||||||
environment: itemArg.environment,
|
|
||||||
value: itemArg.value,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
.editableFields=${['environment', 'value']}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
actionDataArg.table.data.splice(
|
|
||||||
actionDataArg.table.data.indexOf(actionDataArg.item),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'Cancel',
|
|
||||||
iconName: null,
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Save',
|
|
||||||
iconName: null,
|
|
||||||
action: async (modalArg) => {
|
|
||||||
const data = await modalArg.shadowRoot
|
|
||||||
.querySelector('dees-form')
|
|
||||||
.collectFormData();
|
|
||||||
console.log(data);
|
|
||||||
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
|
|
||||||
id: dataArg.item.id,
|
|
||||||
data: {
|
|
||||||
name: data['data.name'] as string,
|
|
||||||
description: data['data.description'] as string,
|
|
||||||
key: data['data.key'] as string,
|
|
||||||
environments: {},
|
|
||||||
tags: dataArg.item.data.tags,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
|
|
||||||
{};
|
|
||||||
for (const itemArg of data['environments'] as any[]) {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'history',
|
|
||||||
iconName: 'clockRotateLeft',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (
|
|
||||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
|
||||||
) => {
|
|
||||||
const historyArray: Array<{
|
|
||||||
environment: string;
|
|
||||||
value: string;
|
|
||||||
}> = [];
|
|
||||||
for (const environment of Object.keys(dataArg.item.data.environments)) {
|
|
||||||
for (const historyItem of dataArg.item.data.environments[environment].history) {
|
|
||||||
historyArray.push({
|
|
||||||
environment,
|
|
||||||
value: historyItem.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `history for ${dataArg.item.data.key}`,
|
|
||||||
content: html`
|
|
||||||
<dees-table
|
|
||||||
.data=${historyArray}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (
|
|
||||||
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
|
|
||||||
) => {
|
|
||||||
console.log('delete', itemArg);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'close',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (
|
|
||||||
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>
|
|
||||||
) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete Image "${itemArg.item.data.name}"`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">Do you really want to delete the image?</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${itemArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
console.log(`Delete ${itemArg.item.id}`);
|
|
||||||
await appstate.dataState.dispatchAction(appstate.deleteImageAction, {
|
|
||||||
imageId: itemArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-logs')
|
|
||||||
export class CloudlyViewLogs extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Logs</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'Logs'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.deployments}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-mails')
|
|
||||||
export class CloudlyViewMails extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css``
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Mails</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'Mails'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.deployments}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-overview')
|
|
||||||
export class CloudlyViewOverview extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
dees-statsgrid {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
// Calculate total nodes across all clusters
|
|
||||||
const totalNodes = this.data.clusters?.reduce((sum, cluster) =>
|
|
||||||
sum + (cluster.data.nodes?.length || 0), 0) || 0;
|
|
||||||
|
|
||||||
// Create tiles for the stats grid
|
|
||||||
const statsTiles = [
|
|
||||||
{
|
|
||||||
id: 'clusters',
|
|
||||||
title: 'Total Clusters',
|
|
||||||
value: this.data.clusters?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Network',
|
|
||||||
description: 'Active clusters'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'nodes',
|
|
||||||
title: 'Total Nodes',
|
|
||||||
value: totalNodes,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Server',
|
|
||||||
description: 'Connected nodes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'services',
|
|
||||||
title: 'Services',
|
|
||||||
value: this.data.services?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Layers',
|
|
||||||
description: 'Deployed services'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deployments',
|
|
||||||
title: 'Deployments',
|
|
||||||
value: this.data.deployments?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Rocket',
|
|
||||||
description: 'Active deployments'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'secretGroups',
|
|
||||||
title: 'Secret Groups',
|
|
||||||
value: this.data.secretGroups?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:ShieldCheck',
|
|
||||||
description: 'Configured secret groups'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'secretBundles',
|
|
||||||
title: 'Secret Bundles',
|
|
||||||
value: this.data.secretBundles?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:LockKeyhole',
|
|
||||||
description: 'Available secret bundles'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'images',
|
|
||||||
title: 'Images',
|
|
||||||
value: this.data.images?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Image',
|
|
||||||
description: 'Container images'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dns',
|
|
||||||
title: 'DNS Zones',
|
|
||||||
value: this.data.dns?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Globe',
|
|
||||||
description: 'Managed DNS zones'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'databases',
|
|
||||||
title: 'Databases',
|
|
||||||
value: this.data.dbs?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Database',
|
|
||||||
description: 'Database instances'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'backups',
|
|
||||||
title: 'Backups',
|
|
||||||
value: this.data.backups?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Save',
|
|
||||||
description: 'Available backups'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mails',
|
|
||||||
title: 'Mail Domains',
|
|
||||||
value: this.data.mails?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Mail',
|
|
||||||
description: 'Mail configurations'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 's3',
|
|
||||||
title: 'S3 Buckets',
|
|
||||||
value: this.data.s3?.length || 0,
|
|
||||||
type: 'number' as const,
|
|
||||||
iconName: 'lucide:Cloud',
|
|
||||||
description: 'Storage buckets'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Overview</cloudly-sectionheading>
|
|
||||||
<dees-statsgrid
|
|
||||||
.tiles=${statsTiles}
|
|
||||||
.minTileWidth=${250}
|
|
||||||
.gap=${16}
|
|
||||||
></dees-statsgrid>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-s3')
|
|
||||||
export class CloudlyViewS3 extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>S3</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'S3'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.s3}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-secretbundles')
|
|
||||||
export class CloudlyViewSecretBundles extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subscription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>SecretBundles</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'SecretBundles'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.secretBundles}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
|
|
||||||
return {
|
|
||||||
name: itemArg.data.name,
|
|
||||||
secretGroups: (() => {
|
|
||||||
const secretGroupIds = itemArg.data.includedSecretGroupIds;
|
|
||||||
let secretGroupNames: string[] = [];
|
|
||||||
for (const secretGroupId of secretGroupIds) {
|
|
||||||
const secretGroup = this.data.secretGroups.find(
|
|
||||||
(secretGroupArg) => secretGroupArg.id === secretGroupId
|
|
||||||
);
|
|
||||||
if (secretGroup) {
|
|
||||||
secretGroupNames.push(secretGroup.data.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return secretGroupNames.join(', ');
|
|
||||||
})(),
|
|
||||||
tags: html`<dees-chips
|
|
||||||
.selectionMode=${'none'}
|
|
||||||
.selectableChips=${itemArg.data.includedTags}
|
|
||||||
></dees-chips>`,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add SecretBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add SecretBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'edit',
|
|
||||||
iconName: 'penToSquare',
|
|
||||||
type: ['doubleClick', 'contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Edit SecretBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .label=${'purpose'}></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'save', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-secretsgroups')
|
|
||||||
export class CloudlyViewSecretGroups extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>SecretGroups</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
heading1="SecretGroups"
|
|
||||||
heading2="decoded in client"
|
|
||||||
.data=${this.data.secretGroups}
|
|
||||||
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
|
|
||||||
return {
|
|
||||||
name: secretGroup.data.name,
|
|
||||||
priority: secretGroup.data.priority,
|
|
||||||
tags: html`<dees-chips
|
|
||||||
.selectionMode=${'none'}
|
|
||||||
.selectableChips=${secretGroup.data.tags}
|
|
||||||
></dees-chips>`,
|
|
||||||
key: secretGroup.data.key,
|
|
||||||
history: (() => {
|
|
||||||
const allHistory = [];
|
|
||||||
for (const environment in secretGroup.data.environments) {
|
|
||||||
allHistory.push(...secretGroup.data.environments[environment].history);
|
|
||||||
}
|
|
||||||
return allHistory.length;
|
|
||||||
})(),
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add SecretGroup',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
iconName: 'plus',
|
|
||||||
actionFunc: async () => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'create new SecretGroup',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text
|
|
||||||
.label=${'name'}
|
|
||||||
.key=${'data.name'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.label=${'description'}
|
|
||||||
.key=${'data.description'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.label=${'Secret Key (data.key)'}
|
|
||||||
.key=${'data.key'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'Environments'}
|
|
||||||
.heading2=${'keys need to be unique'}
|
|
||||||
key="environments"
|
|
||||||
.data=${[
|
|
||||||
{
|
|
||||||
environment: 'production',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environment: 'staging',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add environment',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['footer'],
|
|
||||||
actionFunc: async (dataArg) => {
|
|
||||||
dataArg.table.data.push({
|
|
||||||
environment: 'new environment',
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
dataArg.table.requestUpdate('data');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete environment',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['inRow'],
|
|
||||||
actionFunc: async (dataArg) => {
|
|
||||||
dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1);
|
|
||||||
dataArg.table.requestUpdate('data');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
.editableFields=${['environment', 'value']}
|
|
||||||
></dees-table>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'save',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
|
|
||||||
const formData = await deesForm.collectFormData();
|
|
||||||
console.log(`Prepare saving of data:`);
|
|
||||||
console.log(formData);
|
|
||||||
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
|
|
||||||
{};
|
|
||||||
for (const itemArg of formData['environments'] as any[]) {
|
|
||||||
environments[itemArg.environment] = {
|
|
||||||
value: itemArg.value,
|
|
||||||
history: [],
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, {
|
|
||||||
id: null,
|
|
||||||
data: {
|
|
||||||
name: formData['data.name'] as string,
|
|
||||||
description: formData['data.description'] as string,
|
|
||||||
key: formData['data.key'] as string,
|
|
||||||
environments,
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'edit',
|
|
||||||
type: ['contextmenu', 'inRow', 'doubleClick'],
|
|
||||||
iconName: 'penToSquare',
|
|
||||||
actionFunc: async (
|
|
||||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
|
||||||
) => {
|
|
||||||
const environmentsArray: Array<
|
|
||||||
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
|
|
||||||
environment: string;
|
|
||||||
}
|
|
||||||
> = [];
|
|
||||||
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
|
|
||||||
environmentsArray.push({
|
|
||||||
environment: environmentName,
|
|
||||||
...dataArg.item.data.environments[environmentName],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Edit Secret',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'id'}
|
|
||||||
.disabled=${true}
|
|
||||||
.label=${'ID'}
|
|
||||||
.value=${dataArg.item.id}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.name'}
|
|
||||||
.disabled=${false}
|
|
||||||
.label=${'name'}
|
|
||||||
.value=${dataArg.item.data.name}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.description'}
|
|
||||||
.disabled=${false}
|
|
||||||
.label=${'description'}
|
|
||||||
.value=${dataArg.item.data.description}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.key'}
|
|
||||||
.disabled=${false}
|
|
||||||
.label=${'key'}
|
|
||||||
.value=${dataArg.item.data.key}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-table
|
|
||||||
.key=${'environments'}
|
|
||||||
.heading1=${'Environments'}
|
|
||||||
.heading2=${'double-click to edit values'}
|
|
||||||
.data=${environmentsArray.map((itemArg) => {
|
|
||||||
return {
|
|
||||||
environment: itemArg.environment,
|
|
||||||
value: itemArg.value,
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
.editableFields=${['environment', 'value']}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
actionDataArg.table.data.splice(
|
|
||||||
actionDataArg.table.data.indexOf(actionDataArg.item),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'Cancel',
|
|
||||||
iconName: null,
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Save',
|
|
||||||
iconName: null,
|
|
||||||
action: async (modalArg) => {
|
|
||||||
const data = await modalArg.shadowRoot
|
|
||||||
.querySelector('dees-form')
|
|
||||||
.collectFormData();
|
|
||||||
console.log(data);
|
|
||||||
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
|
|
||||||
id: dataArg.item.id,
|
|
||||||
data: {
|
|
||||||
name: data['data.name'] as string,
|
|
||||||
description: data['data.description'] as string,
|
|
||||||
key: data['data.key'] as string,
|
|
||||||
environments: {},
|
|
||||||
tags: dataArg.item.data.tags,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
|
|
||||||
{};
|
|
||||||
for (const itemArg of data['environments'] as any[]) {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'history',
|
|
||||||
iconName: 'clockRotateLeft',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (
|
|
||||||
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
|
||||||
) => {
|
|
||||||
const historyArray: Array<{
|
|
||||||
environment: string;
|
|
||||||
value: string;
|
|
||||||
}> = [];
|
|
||||||
for (const environment of Object.keys(dataArg.item.data.environments)) {
|
|
||||||
for (const historyItem of dataArg.item.data.environments[environment].history) {
|
|
||||||
historyArray.push({
|
|
||||||
environment,
|
|
||||||
value: historyItem.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `history for ${dataArg.item.data.key}`,
|
|
||||||
content: html`
|
|
||||||
<dees-table
|
|
||||||
.data=${historyArray}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (
|
|
||||||
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
|
|
||||||
) => {
|
|
||||||
console.log('delete', itemArg);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'close',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (
|
|
||||||
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
|
|
||||||
) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ${itemArg.item.data.key}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">Do you really want to delete the secret?</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${itemArg.item.data.key}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
console.log(`Delete ${itemArg.item.id}`);
|
|
||||||
await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, {
|
|
||||||
secretGroupId: itemArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-services')
|
|
||||||
export class CloudlyViewServices extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private data: appstate.IDataState = {
|
|
||||||
secretGroups: [],
|
|
||||||
secretBundles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const subecription = appstate.dataState
|
|
||||||
.select((stateArg) => stateArg)
|
|
||||||
.subscribe((dataArg) => {
|
|
||||||
this.data = dataArg;
|
|
||||||
});
|
|
||||||
this.rxSubscriptions.push(subecription);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Services</cloudly-sectionheading>
|
|
||||||
<dees-table
|
|
||||||
.heading1=${'Services'}
|
|
||||||
.heading2=${'decoded in client'}
|
|
||||||
.data=${this.data.services}
|
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
|
||||||
return {
|
|
||||||
id: itemArg.id,
|
|
||||||
serverAmount: itemArg.data.servers.length,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'add configBundle',
|
|
||||||
iconName: 'plus',
|
|
||||||
type: ['header', 'footer'],
|
|
||||||
actionFunc: async (dataActionArg) => {
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: 'Add ConfigBundle',
|
|
||||||
content: html`
|
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.secretGroupIds'}
|
|
||||||
.label=${'secretGroupIds'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'data.includedTags'}
|
|
||||||
.label=${'includedTags'}
|
|
||||||
.value=${''}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
iconName: 'trash',
|
|
||||||
type: ['contextmenu', 'inRow'],
|
|
||||||
actionFunc: async (actionDataArg) => {
|
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
|
||||||
content: html`
|
|
||||||
<div style="text-align:center">
|
|
||||||
Do you really want to delete the ConfigBundle?
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
|
||||||
>
|
|
||||||
${actionDataArg.item.id}
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'cancel',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'delete',
|
|
||||||
action: async (modalArg) => {
|
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
|
||||||
configBundleId: actionDataArg.item.id,
|
|
||||||
});
|
|
||||||
await modalArg.destroy();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as plugins.deesCatalog.ITableAction[]}
|
|
||||||
></dees-table>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,478 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
customElement,
|
|
||||||
html,
|
|
||||||
state,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
property,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as appstate from '../appstate.js';
|
|
||||||
|
|
||||||
@customElement('cloudly-view-settings')
|
|
||||||
export class CloudlyViewSettings extends DeesElement {
|
|
||||||
@state()
|
|
||||||
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {};
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private isLoading = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private testResults: {[key: string]: {success: boolean; message: string}} = {};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.loadSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
shared.viewHostCss,
|
|
||||||
css`
|
|
||||||
.settings-container {
|
|
||||||
padding: 24px 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.provider-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-status dees-button {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
dees-panel {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid.single {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.form-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
private async loadSettings() {
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
|
|
||||||
plugins.interfaces.requests.settings.IRequest_GetSettings
|
|
||||||
>(
|
|
||||||
'/typedrequest',
|
|
||||||
'getSettings'
|
|
||||||
);
|
|
||||||
const response = await trRequest.fire({});
|
|
||||||
this.settings = response.settings;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load settings:', error);
|
|
||||||
plugins.deesCatalog.DeesToast.createAndShow({
|
|
||||||
message: `Failed to load settings: ${error.message}`,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async saveSettings(formData: any) {
|
|
||||||
console.log('saveSettings called with formData:', formData);
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {};
|
|
||||||
|
|
||||||
// Process form data
|
|
||||||
for (const [key, value] of Object.entries(formData)) {
|
|
||||||
console.log(`Processing ${key}:`, value);
|
|
||||||
if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) {
|
|
||||||
// Only update if value changed (not masked)
|
|
||||||
updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('Updates to send:', updates);
|
|
||||||
|
|
||||||
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
|
|
||||||
plugins.interfaces.requests.settings.IRequest_UpdateSettings
|
|
||||||
>(
|
|
||||||
'/typedrequest',
|
|
||||||
'updateSettings'
|
|
||||||
);
|
|
||||||
const response = await trRequest.fire({ updates });
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
plugins.deesCatalog.DeesToast.createAndShow({
|
|
||||||
message: 'Settings saved successfully',
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
await this.loadSettings(); // Reload to get masked values
|
|
||||||
} else {
|
|
||||||
throw new Error(response.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save settings:', error);
|
|
||||||
plugins.deesCatalog.DeesToast.createAndShow({
|
|
||||||
message: `Failed to save settings: ${error.message}`,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async testConnection(provider: string) {
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
|
|
||||||
plugins.interfaces.requests.settings.IRequest_TestProviderConnection
|
|
||||||
>(
|
|
||||||
'/typedrequest',
|
|
||||||
'testProviderConnection'
|
|
||||||
);
|
|
||||||
const response = await trRequest.fire({ provider: provider as any });
|
|
||||||
|
|
||||||
this.testResults = {
|
|
||||||
...this.testResults,
|
|
||||||
[provider]: {
|
|
||||||
success: response.connectionValid,
|
|
||||||
message: response.message
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show toast notification
|
|
||||||
plugins.deesCatalog.DeesToast.createAndShow({
|
|
||||||
message: response.message,
|
|
||||||
type: response.connectionValid ? 'success' : 'error',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.testResults = {
|
|
||||||
...this.testResults,
|
|
||||||
[provider]: {
|
|
||||||
success: false,
|
|
||||||
message: `Test failed: ${error.message}`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
plugins.deesCatalog.DeesToast.createAndShow({
|
|
||||||
message: `Connection test failed: ${error.message}`,
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderProviderStatus(provider: string) {
|
|
||||||
const result = this.testResults[provider];
|
|
||||||
if (!result) return '';
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<dees-badge
|
|
||||||
.type=${result.success ? 'success' : 'error'}
|
|
||||||
.text=${result.success ? 'Connected' : 'Failed'}
|
|
||||||
></dees-badge>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
if (this.isLoading && Object.keys(this.settings).length === 0) {
|
|
||||||
return html`
|
|
||||||
<div class="loading-container">
|
|
||||||
<dees-spinner></dees-spinner>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<cloudly-sectionheading>Settings</cloudly-sectionheading>
|
|
||||||
<div class="settings-container">
|
|
||||||
<dees-form @formData=${(e: CustomEvent) => {
|
|
||||||
console.log('formData event received:', e);
|
|
||||||
console.log('Event detail:', e.detail);
|
|
||||||
console.log('Event detail.data:', e.detail.data);
|
|
||||||
this.saveSettings(e.detail.data);
|
|
||||||
}}>
|
|
||||||
|
|
||||||
<!-- Hetzner Cloud -->
|
|
||||||
<dees-panel
|
|
||||||
.title=${'Hetzner Cloud'}
|
|
||||||
.subtitle=${'Configure Hetzner Cloud API access'}
|
|
||||||
.variant=${'outline'}
|
|
||||||
>
|
|
||||||
<div class="test-status">
|
|
||||||
${this.renderProviderStatus('hetzner')}
|
|
||||||
<dees-button
|
|
||||||
.text=${'Test Connection'}
|
|
||||||
.type=${'secondary'}
|
|
||||||
@click=${(e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.testConnection('hetzner');
|
|
||||||
}}
|
|
||||||
></dees-button>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid single">
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'hetznerToken'}
|
|
||||||
.label=${'API Token'}
|
|
||||||
.value=${this.settings.hetznerToken || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'Your Hetzner Cloud API token for managing infrastructure'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
</div>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<!-- Cloudflare -->
|
|
||||||
<dees-panel
|
|
||||||
.title=${'Cloudflare'}
|
|
||||||
.subtitle=${'Configure Cloudflare API access'}
|
|
||||||
.variant=${'outline'}
|
|
||||||
>
|
|
||||||
<div class="test-status">
|
|
||||||
${this.renderProviderStatus('cloudflare')}
|
|
||||||
<dees-button
|
|
||||||
.text=${'Test Connection'}
|
|
||||||
.type=${'secondary'}
|
|
||||||
@click=${(e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.testConnection('cloudflare');
|
|
||||||
}}
|
|
||||||
></dees-button>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid single">
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'cloudflareToken'}
|
|
||||||
.label=${'API Token'}
|
|
||||||
.value=${this.settings.cloudflareToken || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'Cloudflare API token with DNS and Zone permissions'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
</div>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<!-- AWS -->
|
|
||||||
<dees-panel
|
|
||||||
.title=${'Amazon Web Services'}
|
|
||||||
.subtitle=${'Configure AWS credentials'}
|
|
||||||
.variant=${'outline'}
|
|
||||||
>
|
|
||||||
<div class="test-status">
|
|
||||||
${this.renderProviderStatus('aws')}
|
|
||||||
<dees-button
|
|
||||||
.text=${'Test Connection'}
|
|
||||||
.type=${'secondary'}
|
|
||||||
@click=${(e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.testConnection('aws');
|
|
||||||
}}
|
|
||||||
></dees-button>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid">
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'awsAccessKey'}
|
|
||||||
.label=${'Access Key ID'}
|
|
||||||
.value=${this.settings.awsAccessKey || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'AWS IAM access key identifier'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'awsSecretKey'}
|
|
||||||
.label=${'Secret Access Key'}
|
|
||||||
.value=${this.settings.awsSecretKey || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'AWS IAM secret access key'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid single">
|
|
||||||
<dees-input-dropdown
|
|
||||||
.key=${'awsRegion'}
|
|
||||||
.label=${'Default Region'}
|
|
||||||
.selectedOption=${this.settings.awsRegion || 'us-east-1'}
|
|
||||||
.options=${[
|
|
||||||
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
|
|
||||||
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
|
|
||||||
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
|
|
||||||
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
|
|
||||||
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
|
|
||||||
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
|
|
||||||
]}
|
|
||||||
.description=${'Default AWS region for resource provisioning'}
|
|
||||||
></dees-input-dropdown>
|
|
||||||
</div>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<!-- DigitalOcean -->
|
|
||||||
<dees-panel
|
|
||||||
.title=${'DigitalOcean'}
|
|
||||||
.subtitle=${'Configure DigitalOcean API access'}
|
|
||||||
.variant=${'outline'}
|
|
||||||
>
|
|
||||||
<div class="test-status">
|
|
||||||
${this.renderProviderStatus('digitalocean')}
|
|
||||||
<dees-button
|
|
||||||
.text=${'Test Connection'}
|
|
||||||
.type=${'secondary'}
|
|
||||||
@click=${(e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.testConnection('digitalocean');
|
|
||||||
}}
|
|
||||||
></dees-button>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid single">
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'digitalOceanToken'}
|
|
||||||
.label=${'Personal Access Token'}
|
|
||||||
.value=${this.settings.digitalOceanToken || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'DigitalOcean personal access token with read/write scope'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
</div>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<!-- Azure -->
|
|
||||||
<dees-panel
|
|
||||||
.title=${'Microsoft Azure'}
|
|
||||||
.subtitle=${'Configure Azure service principal'}
|
|
||||||
.variant=${'outline'}
|
|
||||||
>
|
|
||||||
<div class="test-status">
|
|
||||||
${this.renderProviderStatus('azure')}
|
|
||||||
<dees-button
|
|
||||||
.text=${'Test Connection'}
|
|
||||||
.type=${'secondary'}
|
|
||||||
@click=${(e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.testConnection('azure');
|
|
||||||
}}
|
|
||||||
></dees-button>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid">
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'azureClientId'}
|
|
||||||
.label=${'Application (Client) ID'}
|
|
||||||
.value=${this.settings.azureClientId || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'Azure AD application client ID'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'azureClientSecret'}
|
|
||||||
.label=${'Client Secret'}
|
|
||||||
.value=${this.settings.azureClientSecret || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'Azure AD application client secret'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid">
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'azureTenantId'}
|
|
||||||
.label=${'Directory (Tenant) ID'}
|
|
||||||
.value=${this.settings.azureTenantId || ''}
|
|
||||||
.description=${'Azure AD tenant identifier'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'azureSubscriptionId'}
|
|
||||||
.label=${'Subscription ID'}
|
|
||||||
.value=${this.settings.azureSubscriptionId || ''}
|
|
||||||
.description=${'Azure subscription for resource management'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
</div>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<!-- Google Cloud -->
|
|
||||||
<dees-panel
|
|
||||||
.title=${'Google Cloud Platform'}
|
|
||||||
.subtitle=${'Configure GCP service account'}
|
|
||||||
.variant=${'outline'}
|
|
||||||
>
|
|
||||||
<div class="test-status">
|
|
||||||
${this.renderProviderStatus('google')}
|
|
||||||
<dees-button
|
|
||||||
.text=${'Test Connection'}
|
|
||||||
.type=${'secondary'}
|
|
||||||
@click=${(e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.testConnection('google');
|
|
||||||
}}
|
|
||||||
></dees-button>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid single">
|
|
||||||
<dees-input-textarea
|
|
||||||
.key=${'googleCloudKeyJson'}
|
|
||||||
.label=${'Service Account Key (JSON)'}
|
|
||||||
.value=${this.settings.googleCloudKeyJson || ''}
|
|
||||||
.isPasswordBool=${true}
|
|
||||||
.description=${'Complete JSON key file for service account authentication'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-grid single">
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'googleCloudProjectId'}
|
|
||||||
.label=${'Project ID'}
|
|
||||||
.value=${this.settings.googleCloudProjectId || ''}
|
|
||||||
.description=${'Google Cloud project identifier'}
|
|
||||||
.required=${false}
|
|
||||||
></dees-input-text>
|
|
||||||
</div>
|
|
||||||
</dees-panel>
|
|
||||||
|
|
||||||
<div class="actions-container">
|
|
||||||
<dees-form-submit
|
|
||||||
.text=${'Save All Settings'}
|
|
||||||
.disabled=${this.isLoading}
|
|
||||||
></dees-form-submit>
|
|
||||||
</div>
|
|
||||||
</dees-form>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './shared/index.js';
|
export * from './shared/index.js';
|
||||||
export * from './cloudly-dashboard.js';
|
export * from './cloudly-dashboard.js';
|
||||||
export * from './cloudly-view-secretgroups.js';
|
export * from './views/secretgroups/index.js';
|
||||||
export * from './cloudly-view-secretbundles.js';
|
export * from './views/secretbundles/index.js';
|
||||||
|
|||||||
52
ts_web/elements/views/backups/index.ts
Normal file
52
ts_web/elements/views/backups/index.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-backups')
|
||||||
|
export class CloudlyViewBackups extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Backups</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Backups'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.backups}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||||
|
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||||
|
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||||
|
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-backups': CloudlyViewBackups; } }
|
||||||
|
|
||||||
111
ts_web/elements/views/clusters/index.ts
Normal file
111
ts_web/elements/views/clusters/index.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-clusters')
|
||||||
|
export class CloudlyViewClusters extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css``,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Clusters</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Clusters'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.clusters}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||||
|
return {
|
||||||
|
id: itemArg.id,
|
||||||
|
serverAmount: itemArg.data.servers.length,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'add cluster',
|
||||||
|
iconName: 'plus',
|
||||||
|
type: ['header', 'footer'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Add Cluster',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'clusterName'} .label=${'cluster name'} .description=${'a descriptive name for the cluster'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'setupMode'} .label=${'Setup Mode'} .description=${'How the cluster infrastructure should be managed'}
|
||||||
|
.options=${[
|
||||||
|
{option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'},
|
||||||
|
{option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'},
|
||||||
|
{option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true},
|
||||||
|
{option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true}
|
||||||
|
]}
|
||||||
|
.selectedOption=${'manual'}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'create', action: async (modalArg: any) => {
|
||||||
|
const data = (await modalArg.shadowRoot.querySelector('dees-form').collectFormData()) as any;
|
||||||
|
await appstate.dataState.dispatchAction(appstate.addClusterAction, data);
|
||||||
|
await modalArg.destroy();
|
||||||
|
}},
|
||||||
|
{ name: 'cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg: any) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||||
|
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||||
|
{ name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-view-clusters': CloudlyViewClusters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
52
ts_web/elements/views/dbs/index.ts
Normal file
52
ts_web/elements/views/dbs/index.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-dbs')
|
||||||
|
export class CloudlyViewDbs extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>DBs</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'DBs'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.dbs}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||||
|
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||||
|
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||||
|
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-dbs': CloudlyViewDbs; } }
|
||||||
|
|
||||||
222
ts_web/elements/views/deployments/index.ts
Normal file
222
ts_web/elements/views/deployments/index.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-deployments')
|
||||||
|
export class CloudlyViewDeployments extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
||||||
|
.status-running { background: #4caf50; color: white; }
|
||||||
|
.status-stopped { background: #f44336; color: white; }
|
||||||
|
.status-paused { background: #ff9800; color: white; }
|
||||||
|
.status-deploying { background: #2196f3; color: white; }
|
||||||
|
.health-indicator { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; }
|
||||||
|
.health-healthy { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.health-unhealthy { background: #ffebee; color: #c62828; }
|
||||||
|
.health-unknown { background: #f5f5f5; color: #666; }
|
||||||
|
.resource-usage { display: flex; gap: 12px; font-size: 0.9em; color: #888; }
|
||||||
|
.resource-item { display: flex; align-items: center; gap: 4px; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private getServiceName(serviceId: string): string {
|
||||||
|
const service = this.data.services?.find(s => s.id === serviceId);
|
||||||
|
return service?.data?.name || serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNodeName(nodeId: string): string {
|
||||||
|
return nodeId.substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusBadgeHtml(status: string): any {
|
||||||
|
const className = `status-badge status-${status}`;
|
||||||
|
return html`<span class="${className}">${status}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHealthIndicatorHtml(health?: string): any {
|
||||||
|
if (!health) health = 'unknown';
|
||||||
|
const className = `health-indicator health-${health}`;
|
||||||
|
const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?';
|
||||||
|
return html`<span class="${className}">${icon} ${health}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any {
|
||||||
|
if (!deployment.resourceUsage) {
|
||||||
|
return html`<span style="color: #aaa;">N/A</span>`;
|
||||||
|
}
|
||||||
|
const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage;
|
||||||
|
return html`
|
||||||
|
<div class="resource-usage">
|
||||||
|
<div class="resource-item">
|
||||||
|
<lucide-icon name="Cpu" size="14"></lucide-icon>
|
||||||
|
${cpuUsagePercent?.toFixed(1) || 0}%
|
||||||
|
</div>
|
||||||
|
<div class="resource-item">
|
||||||
|
<lucide-icon name="MemoryStick" size="14"></lucide-icon>
|
||||||
|
${memoryUsedMB || 0} MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Deployments'}
|
||||||
|
.heading2=${'Service deployments running on cluster nodes'}
|
||||||
|
.data=${this.data.deployments || []}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => {
|
||||||
|
return {
|
||||||
|
Service: this.getServiceName(itemArg.serviceId),
|
||||||
|
Node: this.getNodeName(itemArg.nodeId),
|
||||||
|
Status: this.getStatusBadgeHtml(itemArg.status),
|
||||||
|
Health: this.getHealthIndicatorHtml(itemArg.healthStatus),
|
||||||
|
'Container ID': itemArg.containerId ? html`<span style="font-family: monospace; font-size: 0.9em;">${itemArg.containerId.substring(0, 12)}</span>` : html`<span style="color: #aaa;">N/A</span>`,
|
||||||
|
Version: itemArg.version || 'latest',
|
||||||
|
'Resource Usage': this.getResourceUsageHtml(itemArg),
|
||||||
|
'Last Updated': itemArg.deployedAt ? new Date(itemArg.deployedAt).toLocaleString() : 'Never',
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Deploy Service',
|
||||||
|
iconName: 'plus',
|
||||||
|
type: ['header', 'footer'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
const availableServices = this.data.services || [];
|
||||||
|
if (availableServices.length === 0) {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'No Services Available',
|
||||||
|
content: html`<div style="text-align: center; padding: 24px;"><lucide-icon name="AlertCircle" size="48" style="color: #ff9800; margin-bottom: 16px;"></lucide-icon><div>Please create a service first before creating deployments.</div></div>`,
|
||||||
|
menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Deploy Service',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown .key=${'serviceId'} .label=${'Service'} .options=${availableServices.map(s => ({ key: s.id, value: s.data.name }))} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'nodeId'} .label=${'Target Node ID'} .required=${true} .description=${'Enter the cluster node ID where this service should be deployed'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'version'} .label=${'Version'} .value=${'latest'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'status'} .label=${'Initial Status'} .options=${['deploying', 'running']} .value=${'deploying'} .required=${true}></dees-input-dropdown>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Deploy', action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createDeploymentAction, {
|
||||||
|
deploymentData: {
|
||||||
|
serviceId: formData.serviceId,
|
||||||
|
nodeId: formData.nodeId,
|
||||||
|
status: formData.status,
|
||||||
|
version: formData.version,
|
||||||
|
deployedAt: Date.now(),
|
||||||
|
usedImageId: 'placeholder',
|
||||||
|
deploymentLog: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await modalArg.destroy();
|
||||||
|
}},
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Restart',
|
||||||
|
iconName: 'refresh-cw',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg: any) => {
|
||||||
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Restart Deployment`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">Are you sure you want to restart this deployment?</div>
|
||||||
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
|
<div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div>
|
||||||
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||||
|
{ name: 'Restart', action: async (modalArg: any) => { console.log('Restart deployment:', deployment); await modalArg.destroy(); } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stop',
|
||||||
|
iconName: 'square',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg: any) => {
|
||||||
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, {
|
||||||
|
deploymentId: deployment.id,
|
||||||
|
deploymentData: { ...deployment, status: 'stopped' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg: any) => {
|
||||||
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete Deployment`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">Are you sure you want to delete this deployment?</div>
|
||||||
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
|
<div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div>
|
||||||
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div>
|
||||||
|
<div style="color: #f44336; margin-top: 8px;">This action cannot be undone.</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||||
|
{ name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, { deploymentId: deployment.id, }); await modalArg.destroy(); } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-view-deployments': CloudlyViewDeployments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
155
ts_web/elements/views/dns/index.ts
Normal file
155
ts_web/elements/views/dns/index.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-dns')
|
||||||
|
export class CloudlyViewDns extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.dns-type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||||
|
.type-A { background: #4CAF50; }
|
||||||
|
.type-AAAA { background: #45a049; }
|
||||||
|
.type-CNAME { background: #2196F3; }
|
||||||
|
.type-MX { background: #FF9800; }
|
||||||
|
.type-TXT { background: #9C27B0; }
|
||||||
|
.type-NS { background: #795548; }
|
||||||
|
.type-SOA { background: #607D8B; }
|
||||||
|
.type-SRV { background: #E91E63; }
|
||||||
|
.type-CAA { background: #00BCD4; }
|
||||||
|
.type-PTR { background: #673AB7; }
|
||||||
|
.status-active { color: #4CAF50; }
|
||||||
|
.status-inactive { color: #f44336; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private getRecordTypeBadge(type: string) { return html`<span class="dns-type-badge type-${type}">${type}</span>`; }
|
||||||
|
private getStatusBadge(active: boolean) { return html`<span class="${active ? 'status-active' : 'status-inactive'}">${active ? '✓ Active' : '✗ Inactive'}</span>`; }
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>DNS Management</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'DNS Entries'}
|
||||||
|
.heading2=${'Manage DNS records for your domains'}
|
||||||
|
.data=${this.data.dnsEntries || []}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDnsEntry) => {
|
||||||
|
return {
|
||||||
|
Type: this.getRecordTypeBadge(itemArg.data.type),
|
||||||
|
Name: itemArg.data.name === '@' ? '<root>' : itemArg.data.name,
|
||||||
|
Value: itemArg.data.value,
|
||||||
|
TTL: `${itemArg.data.ttl}s`,
|
||||||
|
Priority: itemArg.data.priority || '-',
|
||||||
|
Zone: itemArg.data.zone,
|
||||||
|
Status: this.getStatusBadge(itemArg.data.active),
|
||||||
|
Description: itemArg.data.description || '-',
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'Add DNS Entry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Add DNS Entry',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown .key=${'type'} .label=${'Record Type'} .options=${[
|
||||||
|
{key: 'A', option: 'A - IPv4 Address'}, {key: 'AAAA', option: 'AAAA - IPv6 Address'}, {key: 'CNAME', option: 'CNAME - Canonical Name'}, {key: 'MX', option: 'MX - Mail Exchange'}, {key: 'TXT', option: 'TXT - Text Record'}, {key: 'NS', option: 'NS - Name Server'}, {key: 'SOA', option: 'SOA - Start of Authority'}, {key: 'SRV', option: 'SRV - Service'}, {key: 'CAA', option: 'CAA - Certification Authority'}, {key: 'PTR', option: 'PTR - Pointer'}, ]} .value=${'A'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Name'} .placeholder=${'@ for root, www, mail, etc.'} .value=${'@'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'value'} .label=${'Value'} .placeholder=${'IP address, domain, or text value'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${'3600'} .type=${'number'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'priority'} .label=${'Priority (MX/SRV only)'} .type=${'number'} .placeholder=${'10'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'weight'} .label=${'Weight (SRV only)'} .type=${'number'} .placeholder=${'0'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'port'} .label=${'Port (SRV only)'} .type=${'number'} .placeholder=${'443'}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'active'} .label=${'Active'} .value=${true}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .placeholder=${'What is this record for?'}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Create DNS Entry', action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
// Guard: only allow on activated domains
|
||||||
|
const domain = (this.data.domains || []).find((d: any) => d.id === formData.domainId);
|
||||||
|
if (!domain || (domain.data as any).activationState !== 'activated') {
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Selected domain is not activated. Activate it first.', type: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, { dnsEntryData: { type: formData.type, domainId: formData.domainId, zone: '', name: formData.name || '@', value: formData.value, ttl: parseInt(formData.ttl) || 3600, priority: formData.priority ? parseInt(formData.priority) : undefined, weight: formData.weight ? parseInt(formData.weight) : undefined, port: formData.port ? parseInt(formData.port) : undefined, active: formData.active, description: formData.description || undefined, }, });
|
||||||
|
await modalArg.destroy();
|
||||||
|
} },
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} },
|
||||||
|
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Edit DNS Entry`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown .key=${'type'} .label=${'Record Type'} .options=${[
|
||||||
|
{key: 'A', option: 'A - IPv4 Address'}, {key: 'AAAA', option: 'AAAA - IPv6 Address'}, {key: 'CNAME', option: 'CNAME - Canonical Name'}, {key: 'MX', option: 'MX - Mail Exchange'}, {key: 'TXT', option: 'TXT - Text Record'}, {key: 'NS', option: 'NS - Name Server'}, {key: 'SOA', option: 'SOA - Start of Authority'}, {key: 'SRV', option: 'SRV - Service'}, {key: 'CAA', option: 'CAA - Certification Authority'}, {key: 'PTR', option: 'PTR - Pointer'}, ]} .value=${dnsEntry.data.type} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .value=${dnsEntry.data.domainId || ''} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Name'} .value=${dnsEntry.data.name} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'value'} .label=${'Value'} .value=${dnsEntry.data.value} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${dnsEntry.data.ttl} .type=${'number'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'priority'} .label=${'Priority (MX/SRV only)'} .value=${dnsEntry.data.priority || ''} .type=${'number'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'weight'} .label=${'Weight (SRV only)'} .value=${dnsEntry.data.weight || ''} .type=${'number'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'port'} .label=${'Port (SRV only)'} .value=${dnsEntry.data.port || ''} .type=${'number'}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'active'} .label=${'Active'} .value=${dnsEntry.data.active}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .value=${dnsEntry.data.description || ''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Update DNS Entry', action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
if (formData.domainId) {
|
||||||
|
const domain = (this.data.domains || []).find((d: any) => d.id === formData.domainId);
|
||||||
|
if (!domain || (domain.data as any).activationState !== 'activated') {
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Selected domain is not activated. Activate it first.', type: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, { dnsEntryId: dnsEntry.id, dnsEntryData: { ...dnsEntry.data, type: formData.type, domainId: formData.domainId, zone: '', name: formData.name || '@', value: formData.value, ttl: parseInt(formData.ttl) || 3600, priority: formData.priority ? parseInt(formData.priority) : undefined, weight: formData.weight ? parseInt(formData.weight) : undefined, port: formData.port ? parseInt(formData.port) : undefined, active: formData.active, description: formData.description || undefined, }, });
|
||||||
|
await modalArg.destroy();
|
||||||
|
} },
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} },
|
||||||
|
{ name: 'Duplicate', iconName: 'copy', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, { dnsEntryData: { ...dnsEntry.data, description: `Copy of ${dnsEntry.data.description || dnsEntry.data.name}`, }, }); } },
|
||||||
|
{ name: 'Toggle Active', iconName: 'power', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, { dnsEntryId: dnsEntry.id, dnsEntryData: { ...dnsEntry.data, active: !dnsEntry.data.active, }, }); } },
|
||||||
|
{ name: 'Delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete DNS Entry`, content: html`<div style="text-align:center">Are you sure you want to delete this DNS entry?</div><div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"><div style="color: #fff; font-weight: bold;">${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone}</div><div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${dnsEntry.data.value}</div>${dnsEntry.data.description ? html`<div style=\"color: #888; font-size: 0.85em; margin-top: 8px;\">${dnsEntry.data.description}</div>` : ''}</div>`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, { dnsEntryId: dnsEntry.id, }); await modalArg.destroy(); } }, ], }); } },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-dns': CloudlyViewDns; } }
|
||||||
185
ts_web/elements/views/domains/index.ts
Normal file
185
ts_web/elements/views/domains/index.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-domains')
|
||||||
|
export class CloudlyViewDomains extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], domains: [], dnsEntries: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||||
|
.status-active { background: #4CAF50; }
|
||||||
|
.status-pending { background: #FF9800; }
|
||||||
|
.status-expired { background: #f44336; }
|
||||||
|
.status-suspended { background: #9E9E9E; }
|
||||||
|
.status-transferred { background: #607D8B; }
|
||||||
|
.verification-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
||||||
|
.verification-verified { background: #4CAF50; color: white; }
|
||||||
|
.verification-pending { background: #FF9800; color: white; }
|
||||||
|
.verification-failed { background: #f44336; color: white; }
|
||||||
|
.verification-not_required { background: #E0E0E0; color: #333; }
|
||||||
|
.ssl-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; }
|
||||||
|
.ssl-active { color: #4CAF50; }
|
||||||
|
.ssl-pending { color: #FF9800; }
|
||||||
|
.ssl-expired { color: #f44336; }
|
||||||
|
.ssl-none { color: #9E9E9E; }
|
||||||
|
.nameserver-list { font-size: 0.85em; color: #666; }
|
||||||
|
.activation-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
||||||
|
.activation-available { background: #2b2b2b; color: #bbb; border: 1px solid #444; }
|
||||||
|
.activation-activated { background: #4CAF50; color: #fff; }
|
||||||
|
.activation-ignored { background: #9E9E9E; color: #fff; }
|
||||||
|
.expiry-warning { color: #FF9800; font-weight: 500; }
|
||||||
|
.expiry-critical { color: #f44336; font-weight: bold; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private getStatusBadge(status: string) { return html`<span class="status-badge status-${status}">${status.toUpperCase()}</span>`; }
|
||||||
|
private getVerificationBadge(status: string) { const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase(); return html`<span class="verification-badge verification-${status}">${displayText}</span>`; }
|
||||||
|
private getSslBadge(sslStatus?: string) { if (!sslStatus) return html`<span class="ssl-badge ssl-none">—</span>`; const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓'; return html`<span class="ssl-badge ssl-${sslStatus}">${icon} ${sslStatus.toUpperCase()}</span>`; }
|
||||||
|
private getActivationBadge(state?: 'available'|'activated'|'ignored') { const s = state || 'available'; return html`<span class="activation-badge activation-${s}">${s.toUpperCase()}</span>`; }
|
||||||
|
private formatDate(timestamp?: number) { if (!timestamp) return '—'; const date = new Date(timestamp); return date.toLocaleDateString(); }
|
||||||
|
private getDaysUntilExpiry(expiresAt?: number) { if (!expiresAt) return null; const days = Math.floor((expiresAt - Date.now()) / (1000 * 60 * 60 * 24)); return days; }
|
||||||
|
private getExpiryDisplay(expiresAt?: number) { const days = this.getDaysUntilExpiry(expiresAt); if (days === null) return '—'; if (days < 0) { return html`<span class="expiry-critical">Expired ${Math.abs(days)} days ago</span>`; } else if (days <= 30) { return html`<span class="expiry-warning">Expires in ${days} days</span>`; } else { return `${days} days`; } }
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Domain Management</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Domains'}
|
||||||
|
.heading2=${'Manage your domains and DNS zones'}
|
||||||
|
.data=${this.data.domains || []}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDomain) => {
|
||||||
|
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0;
|
||||||
|
return {
|
||||||
|
Domain: html`<div><div style="font-weight: 500;">${itemArg.data.name}</div>${itemArg.data.description ? html`<div style="font-size: 0.85em; color: #666; margin-top: 2px;">${itemArg.data.description}</div>` : ''}</div>`,
|
||||||
|
Status: this.getStatusBadge(itemArg.data.status),
|
||||||
|
Verification: this.getVerificationBadge(itemArg.data.verificationStatus),
|
||||||
|
SSL: this.getSslBadge(itemArg.data.sslStatus),
|
||||||
|
Activation: this.getActivationBadge((itemArg.data as any).activationState),
|
||||||
|
'DNS Records': dnsCount,
|
||||||
|
Registrar: itemArg.data.registrar?.name || '—',
|
||||||
|
Expires: this.getExpiryDisplay(itemArg.data.expiresAt),
|
||||||
|
'Auto-Renew': itemArg.data.autoRenew ? '✓' : '✗',
|
||||||
|
Nameservers: html`<div class="nameserver-list">${itemArg.data.nameservers?.join(', ') || '—'}</div>`,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'Sync from Cloudflare', iconName: 'cloud', type: ['header'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName: 'cloudflare-domain-sync' } as any); await appstate.dataState.dispatchAction(appstate.getAllDataAction, null); plugins.deesCatalog.DeesToast.createAndShow({ message: 'Triggered Cloudflare sync', type: 'success' }); } },
|
||||||
|
{ name: 'Add Domain', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Add Domain',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Domain Name'} .placeholder=${'example.com'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description'} .placeholder=${'Main company domain'}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'status'} .label=${'Status'} .options=${[{key: 'active', option: 'Active'}, {key: 'pending', option: 'Pending'}, {key: 'expired', option: 'Expired'}, {key: 'suspended', option: 'Suspended'}]} .value=${'pending'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'nameservers'} .label=${'Nameservers (comma separated)'} .placeholder=${'ns1.example.com, ns2.example.com'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'registrarName'} .label=${'Registrar Name'} .placeholder=${'GoDaddy, Namecheap, etc.'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'registrarUrl'} .label=${'Registrar URL'} .placeholder=${'https://registrar.com'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'expiresAt'} .label=${'Expiration Date'} .type=${'date'}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'autoRenew'} .label=${'Auto-Renew Enabled'} .value=${true}></dees-input-checkbox>
|
||||||
|
<dees-input-checkbox .key=${'dnssecEnabled'} .label=${'DNSSEC Enabled'} .value=${false}></dees-input-checkbox>
|
||||||
|
<dees-input-checkbox .key=${'isPrimary'} .label=${'Primary Domain'} .value=${false}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'tags'} .label=${'Tags (comma separated)'} .placeholder=${'production, critical'}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Create Domain', action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
const nameservers = formData.nameservers ? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns) : [];
|
||||||
|
const tags = formData.tags ? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag) : [];
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createDomainAction, { domainData: { name: formData.name, description: formData.description || undefined, status: formData.status, verificationStatus: 'pending', nameservers, registrar: formData.registrarName ? { name: formData.registrarName, url: formData.registrarUrl || undefined, } : undefined, expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined, autoRenew: formData.autoRenew, dnssecEnabled: formData.dnssecEnabled, isPrimary: formData.isPrimary, tags: tags.length > 0 ? tags : undefined, }, });
|
||||||
|
await modalArg.destroy();
|
||||||
|
}},
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} },
|
||||||
|
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Edit Domain: ${domain.data.name}`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Domain Name'} .value=${domain.data.name} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${domain.data.description || ''}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'status'} .label=${'Status'} .options=${[{key: 'active', option: 'Active'}, {key: 'pending', option: 'Pending'}, {key: 'expired', option: 'Expired'}, {key: 'suspended', option: 'Suspended'}, {key: 'transferred', option: 'Transferred'}]} .value=${domain.data.status} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-checkbox .key=${'autoRenew'} .label=${'Auto-Renew Enabled'} .value=${domain.data.autoRenew}></dees-input-checkbox>
|
||||||
|
<dees-input-checkbox .key=${'dnssecEnabled'} .label=${'DNSSEC Enabled'} .value=${domain.data.dnssecEnabled}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'tags'} .label=${'Tags (comma separated)'} .value=${(domain.data.tags || []).join(', ')}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Save Changes', action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
const tags = formData.tags ? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag) : [];
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, updates: { name: formData.name, description: formData.description || undefined, status: formData.status, autoRenew: formData.autoRenew, dnssecEnabled: formData.dnssecEnabled, tags }, });
|
||||||
|
await modalArg.destroy();
|
||||||
|
}},
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} },
|
||||||
|
{ name: 'Verify Ownership', iconName: 'check-circle', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Verify Domain: ${domain.data.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center; padding: 20px;">
|
||||||
|
<p>Choose a verification method for <strong>${domain.data.name}</strong></p>
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown .key=${'method'} .label=${'Verification Method'} .options=${[{key: 'dns', option: 'DNS TXT Record'}, {key: 'http', option: 'HTTP File Upload'}, {key: 'email', option: 'Email Verification'}, {key: 'manual', option: 'Manual Verification'}]} .value=${'dns'} .required=${true}></dees-input-dropdown>
|
||||||
|
</dees-form>
|
||||||
|
${domain.data.verificationToken ? html`<div style="margin-top: 20px; padding: 15px; background: #333; border-radius: 8px;"><div style="color: #aaa; font-size: 0.9em;">Verification Token:</div><code style="color: #4CAF50; word-break: break-all;">${domain.data.verificationToken}</code></div>` : ''}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Start Verification', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); await appstate.dataState.dispatchAction(appstate.verifyDomainAction, { domainId: domain.id, verificationMethod: formData.method, }); await modalArg.destroy(); } },
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} },
|
||||||
|
{ name: 'View DNS Records', iconName: 'list', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; console.log('View DNS records for domain:', domain.data.name); } },
|
||||||
|
{ name: 'Delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
|
||||||
|
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === domain.data.name).length || 0;
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete Domain`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">Are you sure you want to delete this domain?</div>
|
||||||
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
|
<div style="color: #fff; font-weight: bold; font-size: 1.1em;">${domain.data.name}</div>
|
||||||
|
${domain.data.description ? html`<div style="color: #aaa; margin-top: 4px;">${domain.data.description}</div>` : ''}
|
||||||
|
${dnsCount > 0 ? html`<div style="color: #f44336; margin-top: 12px; padding: 8px; background: #1a1a1a; border-radius: 4px;">⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDomainAction, { domainId: domain.id, }); await modalArg.destroy(); } }, ],
|
||||||
|
});
|
||||||
|
} },
|
||||||
|
{ name: 'Activate', iconName: 'check', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'activated' } as any }); } },
|
||||||
|
{ name: 'Deactivate', iconName: 'slash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'available' } as any }); } },
|
||||||
|
{ name: 'Ignore', iconName: 'ban', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'ignored' } as any }); } },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap { 'cloudly-view-domains': CloudlyViewDomains; }
|
||||||
|
}
|
||||||
118
ts_web/elements/views/externalregistries/index.ts
Normal file
118
ts_web/elements/views/externalregistries/index.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-externalregistries')
|
||||||
|
export class CloudlyViewExternalRegistries extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||||
|
.status-active { background: #4CAF50; }
|
||||||
|
.status-inactive { background: #9E9E9E; }
|
||||||
|
.status-error { background: #f44336; }
|
||||||
|
.status-unverified { background: #FF9800; }
|
||||||
|
.type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
|
||||||
|
.type-docker { background: #2196F3; }
|
||||||
|
.type-npm { background: #CB3837; }
|
||||||
|
.default-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; background: #673AB7; color: white; margin-left: 8px; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>External Registries</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'External Registries'}
|
||||||
|
.heading2=${'Configure external Docker and NPM registries'}
|
||||||
|
.data=${this.data.externalRegistries || []}
|
||||||
|
.displayFunction=${(registry: plugins.interfaces.data.IExternalRegistry) => {
|
||||||
|
return {
|
||||||
|
Name: html`${registry.data.name}${registry.data.isDefault ? html`<span class="default-badge">DEFAULT</span>` : ''}`,
|
||||||
|
Type: html`<span class="type-badge type-${registry.data.type}">${registry.data.type.toUpperCase()}</span>`,
|
||||||
|
URL: registry.data.url,
|
||||||
|
Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'),
|
||||||
|
Namespace: registry.data.namespace || '-',
|
||||||
|
Status: html`<span class="status-badge status-${registry.data.status || 'unverified'}">${(registry.data.status || 'unverified').toUpperCase()}</span>`,
|
||||||
|
'Last Verified': registry.data.lastVerified ? new Date(registry.data.lastVerified).toLocaleString() : 'Never',
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'Add Registry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add External Registry', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${'docker'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Registry Name'} .placeholder=${'My Docker Hub'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'url'} .label=${'Registry URL'} .placeholder=${'https://index.docker.io/v2/ or registry.gitlab.com'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .placeholder=${'username or leave empty for token auth'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'password'} .label=${'Password / Token (NPM _authToken, Docker access token, etc.)'} .placeholder=${'Token or password'} .isPasswordBool=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .placeholder=${'myorg'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .placeholder=${'Production Docker registry'}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${'none'}></dees-input-dropdown>
|
||||||
|
<dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${false}></dees-input-checkbox>
|
||||||
|
<dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${false}></dees-input-checkbox>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'Create Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); await appstate.dataState.dispatchAction(appstate.createExternalRegistryAction, { registryData: { type: formData.type, name: formData.name, url: formData.url, username: formData.username, password: formData.password, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: `Edit Registry: ${registry.data.name}`, content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${registry.data.type} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Registry Name'} .value=${registry.data.name} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'url'} .label=${'Registry URL'} .value=${registry.data.url} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .value=${registry.data.username || ''} .placeholder=${'Leave empty for token auth'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'password'} .label=${'Password / Token (leave empty to keep current)'} .placeholder=${'New token or password'} .isPasswordBool=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .value=${registry.data.namespace || ''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .value=${registry.data.description || ''}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${registry.data.authType || 'none'}></dees-input-dropdown>
|
||||||
|
<dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${registry.data.isDefault || false}></dees-input-checkbox>
|
||||||
|
<dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${registry.data.insecure || false}></dees-input-checkbox>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'Update Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); const updateData: any = { type: formData.type, name: formData.name, url: formData.url, username: formData.username, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }; if (formData.password) { updateData.password = formData.password; } await appstate.dataState.dispatchAction(appstate.updateExternalRegistryAction, { registryId: registry.id, updates: updateData, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'Test Connection', iconName: 'check-circle', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||||
|
const loadingModal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Testing Registry Connection', content: html`<div style="text-align: center; padding: 20px;"><dees-spinner></dees-spinner><p style="margin-top: 20px;">Testing connection to ${registry.data.name}...</p></div>`, menuOptions: [] });
|
||||||
|
await appstate.dataState.dispatchAction(appstate.verifyExternalRegistryAction, { registryId: registry.id, });
|
||||||
|
await loadingModal.destroy();
|
||||||
|
const updatedRegistry = this.data.externalRegistries?.find(r => r.id === registry.id);
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Connection Test Result', content: html`<div style="text-align: center; padding: 20px;">${updatedRegistry?.data.status === 'active' ? html`<div style="color: #4CAF50; font-size: 48px;">✓</div><p style="margin-top: 20px; color: #4CAF50;">Connection successful!</p>` : html`<div style="color: #f44336; font-size: 48px;">✗</div><p style="margin-top: 20px; color: #f44336;">Connection failed!</p>${updatedRegistry?.data.lastError ? html`<p style="margin-top: 10px; font-size: 0.9em; color: #999;">Error: ${updatedRegistry.data.lastError}</p>` : ''}`}</div>`, menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'Delete', iconName: 'trash', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete Registry: ${registry.data.name}`, content: html`<div style="text-align:center"><p>Do you really want to delete this external registry?</p><p style="color: #999; font-size: 0.9em; margin-top: 10px;">This will remove all stored credentials and configuration.</p></div><div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${registry.data.name} (${registry.data.url})</div>`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteExternalRegistryAction, { registryId: registry.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } }
|
||||||
|
|
||||||
142
ts_web/elements/views/images/index.ts
Normal file
142
ts_web/elements/views/images/index.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-images')
|
||||||
|
export class CloudlyViewImages extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css``,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Images</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
heading1="Images"
|
||||||
|
heading2="an image is needed for running a service"
|
||||||
|
.data=${this.data.images}
|
||||||
|
.displayFunction=${(image: plugins.interfaces.data.IImage) => {
|
||||||
|
return { id: image.id, name: image.data.name, description: image.data.description, versions: image.data.versions.length };
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'create Image',
|
||||||
|
type: ['header', 'footer'],
|
||||||
|
iconName: 'plus',
|
||||||
|
actionFunc: async () => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'create new Image',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||||
|
{ name: 'save', action: async (modalArg: any) => {
|
||||||
|
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
|
||||||
|
const formData = await deesForm.collectFormData();
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createImageAction, { imageName: formData['data.name'] as string, description: formData['data.description'] as string });
|
||||||
|
await modalArg.destroy();
|
||||||
|
} },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
type: ['contextmenu', 'inRow', 'doubleClick'],
|
||||||
|
iconName: 'penToSquare',
|
||||||
|
actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||||
|
const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = [];
|
||||||
|
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
|
||||||
|
environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName] });
|
||||||
|
}
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Edit Secret',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text>
|
||||||
|
<dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'}
|
||||||
|
.data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value }))}
|
||||||
|
.editableFields=${['environment', 'value']}
|
||||||
|
.dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}>
|
||||||
|
</dees-table>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||||
|
{ name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'history',
|
||||||
|
iconName: 'clockRotateLeft',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||||
|
const historyArray: Array<{ environment: string; value: string; }> = [];
|
||||||
|
for (const environment of Object.keys(dataArg.item.data.environments)) {
|
||||||
|
for (const historyItem of dataArg.item.data.environments[environment].history) {
|
||||||
|
historyArray.push({ environment, value: historyItem.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `history for ${dataArg.item.data.key}`,
|
||||||
|
content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`,
|
||||||
|
menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete Image "${itemArg.item.data.name}"`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">Do you really want to delete the image?</div>
|
||||||
|
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${itemArg.item.id}</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
|
||||||
|
{ name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteImageAction, { imageId: itemArg.item.id, }); await modalArg.destroy(); } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-view-images': CloudlyViewImages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
61
ts_web/elements/views/logs/index.ts
Normal file
61
ts_web/elements/views/logs/index.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-logs')
|
||||||
|
export class CloudlyViewLogs extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Logs</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Logs'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.deployments}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||||
|
return { id: itemArg.id, serverAmount: itemArg.data.servers.length };
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||||
|
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||||
|
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||||
|
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-logs': CloudlyViewLogs; } }
|
||||||
|
|
||||||
61
ts_web/elements/views/mails/index.ts
Normal file
61
ts_web/elements/views/mails/index.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-mails')
|
||||||
|
export class CloudlyViewMails extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Mails</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Mails'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.deployments}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||||
|
return { id: itemArg.id, serverAmount: itemArg.data.servers.length };
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||||
|
<div style=\"text-align:center\">Do you really want to delete the ConfigBundle?</div>
|
||||||
|
<div style=\"font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;\">${actionDataArg.item.id}</div>
|
||||||
|
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-mails': CloudlyViewMails; } }
|
||||||
|
|
||||||
71
ts_web/elements/views/overview/index.ts
Normal file
71
ts_web/elements/views/overview/index.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-overview')
|
||||||
|
export class CloudlyViewOverview extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {
|
||||||
|
secretGroups: [],
|
||||||
|
secretBundles: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
dees-statsgrid { margin-top: 24px; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const totalNodes = this.data.clusters?.reduce((sum, cluster) =>
|
||||||
|
sum + (cluster.data.nodes?.length || 0), 0) || 0;
|
||||||
|
|
||||||
|
const statsTiles = [
|
||||||
|
{ id: 'clusters', title: 'Total Clusters', value: this.data.clusters?.length || 0, type: 'number' as const, iconName: 'lucide:Network', description: 'Active clusters' },
|
||||||
|
{ id: 'nodes', title: 'Total Nodes', value: totalNodes, type: 'number' as const, iconName: 'lucide:Server', description: 'Connected nodes' },
|
||||||
|
{ id: 'services', title: 'Services', value: this.data.services?.length || 0, type: 'number' as const, iconName: 'lucide:Layers', description: 'Deployed services' },
|
||||||
|
{ id: 'deployments', title: 'Deployments', value: this.data.deployments?.length || 0, type: 'number' as const, iconName: 'lucide:Rocket', description: 'Active deployments' },
|
||||||
|
{ id: 'secretGroups', title: 'Secret Groups', value: this.data.secretGroups?.length || 0, type: 'number' as const, iconName: 'lucide:ShieldCheck', description: 'Configured secret groups' },
|
||||||
|
{ id: 'secretBundles', title: 'Secret Bundles', value: this.data.secretBundles?.length || 0, type: 'number' as const, iconName: 'lucide:LockKeyhole', description: 'Available secret bundles' },
|
||||||
|
{ id: 'images', title: 'Images', value: this.data.images?.length || 0, type: 'number' as const, iconName: 'lucide:Image', description: 'Container images' },
|
||||||
|
{ id: 'dns', title: 'DNS Entries', value: this.data.dnsEntries?.length || 0, type: 'number' as const, iconName: 'lucide:Globe', description: 'Managed DNS records' },
|
||||||
|
{ id: 'databases', title: 'Databases', value: this.data.dbs?.length || 0, type: 'number' as const, iconName: 'lucide:Database', description: 'Database instances' },
|
||||||
|
{ id: 'backups', title: 'Backups', value: this.data.backups?.length || 0, type: 'number' as const, iconName: 'lucide:Save', description: 'Available backups' },
|
||||||
|
{ id: 'mails', title: 'Mail Domains', value: this.data.mails?.length || 0, type: 'number' as const, iconName: 'lucide:Mail', description: 'Mail configurations' },
|
||||||
|
{ id: 's3', title: 'S3 Buckets', value: this.data.s3?.length || 0, type: 'number' as const, iconName: 'lucide:Cloud', description: 'Storage buckets' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Overview</cloudly-sectionheading>
|
||||||
|
<dees-statsgrid .tiles=${statsTiles} .minTileWidth=${250} .gap=${16}></dees-statsgrid>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-view-overview': CloudlyViewOverview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
52
ts_web/elements/views/s3/index.ts
Normal file
52
ts_web/elements/views/s3/index.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-s3')
|
||||||
|
export class CloudlyViewS3 extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>S3</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'S3'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.s3}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||||
|
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||||
|
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||||
|
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-s3': CloudlyViewS3; } }
|
||||||
|
|
||||||
76
ts_web/elements/views/secretbundles/index.ts
Normal file
76
ts_web/elements/views/secretbundles/index.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-secretbundles')
|
||||||
|
export class CloudlyViewSecretBundles extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>SecretBundles</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'SecretBundles'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.secretBundles || []}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
|
||||||
|
return {
|
||||||
|
name: itemArg.data.name,
|
||||||
|
secretGroups: (() => {
|
||||||
|
const secretGroupIds = itemArg.data.includedSecretGroupIds;
|
||||||
|
let secretGroupNames: string[] = [];
|
||||||
|
for (const secretGroupId of secretGroupIds) {
|
||||||
|
const secretGroup = this.data.secretGroups.find((secretGroupArg: any) => secretGroupArg.id === secretGroupId);
|
||||||
|
if (secretGroup) { secretGroupNames.push(secretGroup.data.name); }
|
||||||
|
}
|
||||||
|
return secretGroupNames.join(', ');
|
||||||
|
})(),
|
||||||
|
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${itemArg.data.includedTags}></dees-chips>`,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'add SecretBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add SecretBundle', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
|
||||||
|
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
|
||||||
|
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
|
||||||
|
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'edit', iconName: 'penToSquare', type: ['doubleClick', 'contextmenu', 'inRow'], actionFunc: async () => {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit SecretBundle', content: html`<dees-form><dees-input-text .label=${'purpose'}></dees-input-text></dees-form>`, menuOptions: [ { name: 'save', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } }
|
||||||
|
|
||||||
77
ts_web/elements/views/secretgroups/index.ts
Normal file
77
ts_web/elements/views/secretgroups/index.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-secretsgroups')
|
||||||
|
export class CloudlyViewSecretGroups extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>SecretGroups</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
heading1="SecretGroups"
|
||||||
|
heading2="decoded in client"
|
||||||
|
.data=${this.data.secretGroups || []}
|
||||||
|
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
|
||||||
|
return {
|
||||||
|
name: secretGroup.data.name,
|
||||||
|
priority: secretGroup.data.priority,
|
||||||
|
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${secretGroup.data.tags}></dees-chips>`,
|
||||||
|
key: secretGroup.data.key,
|
||||||
|
history: (() => { const allHistory = []; for (const environment in secretGroup.data.environments) { allHistory.push(...secretGroup.data.environments[environment].history); } return allHistory.length; })(),
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{ name: 'add SecretGroup', type: ['header', 'footer'], iconName: 'plus', actionFunc: async () => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: 'create new SecretGroup', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text .label=${'Secret Key (data.key)'} .key=${'data.key'} .value=${''}></dees-input-text>
|
||||||
|
<dees-table heading1=${'Environments'} heading2=${'keys need to be unique'} key="environments" .data=${[{ environment: 'production', value: '' }, { environment: 'staging', value: '' }]} .dataActions=${[{ name: 'add environment', iconName: 'plus', type: ['footer'], actionFunc: async (dataArg: any) => { dataArg.table.data.push({ environment: 'new environment', value: '' }); dataArg.table.requestUpdate('data'); } }, { name: 'delete environment', iconName: 'trash', type: ['inRow'], actionFunc: async (dataArg: any) => { dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1); dataArg.table.requestUpdate('data'); } }] as plugins.deesCatalog.ITableAction[]} .editableFields=${['environment', 'value']}>
|
||||||
|
</dees-table>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'save', action: async (modalArg: any) => { const deesForm = modalArg.shadowRoot.querySelector('dees-form'); const formData = await deesForm.collectFormData(); const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = {}; for (const itemArg of formData['environments'] as any[]) { environments[itemArg.environment] = { value: itemArg.value, history: [], lastUpdated: Date.now(), }; } await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, { id: null, data: { name: formData['data.name'] as string, description: formData['data.description'] as string, key: formData['data.key'] as string, environments, tags: [], }, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'edit', type: ['contextmenu', 'inRow', 'doubleClick'], iconName: 'penToSquare', actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||||
|
const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = [];
|
||||||
|
for (const environmentName of Object.keys(dataArg.item.data.environments)) { environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName], }); }
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit Secret', content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text>
|
||||||
|
<dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'} .data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value, }))} .editableFields=${['environment', 'value']} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}>
|
||||||
|
</dees-table>
|
||||||
|
</dees-form>
|
||||||
|
`, menuOptions: [ { name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'history', iconName: 'clockRotateLeft', type: ['contextmenu', 'inRow'], actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||||
|
const historyArray: Array<{ environment: string; value: string; }> = []; for (const environment of Object.keys(dataArg.item.data.environments)) { for (const historyItem of dataArg.item.data.environments[environment].history) { historyArray.push({ environment, value: historyItem.value, }); } }
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({ heading: `history for ${dataArg.item.data.key}`, content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`, menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ${itemArg.item.data.key}`, content: html`<div style="text-align:center">Do you really want to delete the secret?</div><div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${itemArg.item.data.key}</div>`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, { secretGroupId: itemArg.item.id, }); await modalArg.destroy(); } } ] });
|
||||||
|
} },
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretsgroups': CloudlyViewSecretGroups; } }
|
||||||
|
|
||||||
225
ts_web/elements/views/services/index.ts
Normal file
225
ts_web/elements/views/services/index.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-services')
|
||||||
|
export class CloudlyViewServices extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.category-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 500; }
|
||||||
|
.category-base { background: #2196f3; color: white; }
|
||||||
|
.category-distributed { background: #9c27b0; color: white; }
|
||||||
|
.category-workload { background: #4caf50; color: white; }
|
||||||
|
.strategy-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; background: #444; color: #ccc; margin-left: 4px; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private getCategoryIcon(category: string): string {
|
||||||
|
switch (category) {
|
||||||
|
case 'base': return 'lucide:ServerCog';
|
||||||
|
case 'distributed': return 'lucide:Network';
|
||||||
|
case 'workload': return 'lucide:Container';
|
||||||
|
default: return 'lucide:Box';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCategoryBadgeHtml(category: string): any {
|
||||||
|
const className = `category-badge category-${category}`;
|
||||||
|
return html`<span class="${className}">${category}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStrategyBadgeHtml(strategy: string): any {
|
||||||
|
return html`<span class="strategy-badge">${strategy}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Services</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Services'}
|
||||||
|
.heading2=${'Service configuration and deployment management'}
|
||||||
|
.data=${this.data.services || []}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.IService) => {
|
||||||
|
return {
|
||||||
|
Name: itemArg.data.name,
|
||||||
|
Description: itemArg.data.description,
|
||||||
|
Category: this.getCategoryBadgeHtml(itemArg.data.serviceCategory || 'workload'),
|
||||||
|
'Deployment Strategy': html`
|
||||||
|
${this.getStrategyBadgeHtml(itemArg.data.deploymentStrategy || 'custom')}
|
||||||
|
${itemArg.data.maxReplicas ? html`<span style="color: #888; margin-left: 8px;">Max: ${itemArg.data.maxReplicas}</span>` : ''}
|
||||||
|
${itemArg.data.antiAffinity ? html`<span style="color: #f44336; margin-left: 8px;">⚡ Anti-affinity</span>` : ''}
|
||||||
|
`,
|
||||||
|
'Image': `${itemArg.data.imageId}:${itemArg.data.imageVersion}`,
|
||||||
|
'Scale Factor': itemArg.data.scaleFactor,
|
||||||
|
'Balancing': itemArg.data.balancingStrategy,
|
||||||
|
'Deployments': itemArg.data.deploymentIds?.length || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Add Service',
|
||||||
|
iconName: 'plus',
|
||||||
|
type: ['header', 'footer'],
|
||||||
|
actionFunc: async () => {
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Add Service',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Service Name'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} .value=${'workload'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-dropdown .key=${'deploymentStrategy'} .label=${'Deployment Strategy'} .options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]} .value=${'custom'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${'1'} .type=${'number'}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${false}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'imageId'} .label=${'Image ID'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${'1'} .type=${'number'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${'round-robin'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'webPort'} .label=${'Web Port'} .value=${'80'} .type=${'number'} .required=${true}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Create Service', action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createServiceAction, {
|
||||||
|
serviceData: {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
serviceCategory: formData.serviceCategory,
|
||||||
|
deploymentStrategy: formData.deploymentStrategy,
|
||||||
|
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
|
||||||
|
antiAffinity: formData.antiAffinity,
|
||||||
|
imageId: formData.imageId,
|
||||||
|
imageVersion: formData.imageVersion,
|
||||||
|
scaleFactor: parseInt(formData.scaleFactor),
|
||||||
|
balancingStrategy: formData.balancingStrategy,
|
||||||
|
ports: { web: parseInt(formData.webPort) },
|
||||||
|
environment: {},
|
||||||
|
domains: [],
|
||||||
|
deploymentIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await modalArg.destroy();
|
||||||
|
}},
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
iconName: 'edit',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg: any) => {
|
||||||
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Edit Service: ${service.data.name}`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} .value=${service.data.serviceCategory || 'workload'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-dropdown .key=${'deploymentStrategy'} .label=${'Deployment Strategy'} .options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]} .value=${service.data.deploymentStrategy || 'custom'} .required=${true}></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${service.data.maxReplicas || ''} .type=${'number'}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${service.data.antiAffinity || false}></dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${service.data.scaleFactor} .type=${'number'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${service.data.balancingStrategy} .required=${true}></dees-input-dropdown>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Update Service', action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateServiceAction, {
|
||||||
|
serviceId: service.id,
|
||||||
|
serviceData: {
|
||||||
|
...service.data,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
serviceCategory: formData.serviceCategory,
|
||||||
|
deploymentStrategy: formData.deploymentStrategy,
|
||||||
|
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
|
||||||
|
antiAffinity: formData.antiAffinity,
|
||||||
|
imageVersion: formData.imageVersion,
|
||||||
|
scaleFactor: parseInt(formData.scaleFactor),
|
||||||
|
balancingStrategy: formData.balancingStrategy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await modalArg.destroy();
|
||||||
|
}},
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Deploy',
|
||||||
|
iconName: 'rocket',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg: any) => {
|
||||||
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
||||||
|
console.log('Deploy service:', service);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg: any) => {
|
||||||
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete Service: ${service.data.name}`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">Are you sure you want to delete this service?</div>
|
||||||
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
|
<div style="color: #fff; font-weight: bold;">${service.data.name}</div>
|
||||||
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div>
|
||||||
|
<div style="color: #f44336; margin-top: 8px;">This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
{ name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { serviceId: service.id }); await modalArg.destroy(); } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-view-services': CloudlyViewServices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
206
ts_web/elements/views/settings/index.ts
Normal file
206
ts_web/elements/views/settings/index.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-settings')
|
||||||
|
export class CloudlyViewSettings extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private isLoading = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private testResults: {[key: string]: {success: boolean; message: string}} = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.settings-container { padding: 24px 0; display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
.provider-icon { margin-right: 8px; font-size: 20px; }
|
||||||
|
.test-status { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||||
|
.test-status dees-button { margin-left: auto; }
|
||||||
|
.loading-container { display: flex; justify-content: center; padding: 48px; }
|
||||||
|
.actions-container { display: flex; justify-content: center; margin-top: 24px; }
|
||||||
|
dees-panel { margin-bottom: 16px; }
|
||||||
|
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
|
.form-grid.single { grid-template-columns: 1fr; }
|
||||||
|
@media (max-width: 768px) { .form-grid { grid-template-columns: 1fr; } }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private async loadSettings() {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const response = await appstate.apiClient.settings.getSettings();
|
||||||
|
this.settings = response.settings;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load settings:', error);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to load settings: ${error.message}`, type: 'error' });
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveSettings(formData: any) {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {};
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) {
|
||||||
|
updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await appstate.apiClient.settings.updateSettings(updates);
|
||||||
|
if (response.success) {
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Settings saved successfully', type: 'success' });
|
||||||
|
await this.loadSettings();
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to save settings:', error);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to save settings: ${error.message}`, type: 'error' });
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testConnection(provider: string) {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const response = await appstate.apiClient.settings.testProviderConnection(provider);
|
||||||
|
this.testResults = { ...this.testResults, [provider]: { success: response.connectionValid, message: response.message } };
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: response.message, type: response.connectionValid ? 'success' : 'error' });
|
||||||
|
} catch (error: any) {
|
||||||
|
this.testResults = { ...this.testResults, [provider]: { success: false, message: `Test failed: ${error.message}` } };
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: `Connection test failed: ${error.message}`, type: 'error' });
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderProviderStatus(provider: string) {
|
||||||
|
const result = this.testResults[provider];
|
||||||
|
if (!result) return '' as any;
|
||||||
|
return html`<dees-badge .type=${result.success ? 'success' : 'error'} .text=${result.success ? 'Connected' : 'Failed'}></dees-badge>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.isLoading && Object.keys(this.settings).length === 0) {
|
||||||
|
return html`<div class="loading-container"><dees-spinner></dees-spinner></div>`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Settings</cloudly-sectionheading>
|
||||||
|
<div class="settings-container">
|
||||||
|
<dees-form @formData=${(e: CustomEvent) => { this.saveSettings((e.detail as any).data); }}>
|
||||||
|
<dees-panel .title=${'Hetzner Cloud'} .subtitle=${'Configure Hetzner Cloud API access'} .variant=${'outline'}>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('hetzner')}
|
||||||
|
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('hetzner'); }}></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text .key=${'hetznerToken'} .label=${'API Token'} .value=${this.settings.hetznerToken || ''} .isPasswordBool=${true} .description=${'Your Hetzner Cloud API token for managing infrastructure'} .required=${false}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Cloudflare'} .subtitle=${'Configure Cloudflare API access'} .variant=${'outline'}>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('cloudflare')}
|
||||||
|
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('cloudflare'); }}></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text .key=${'cloudflareToken'} .label=${'API Token'} .value=${this.settings.cloudflareToken || ''} .isPasswordBool=${true} .description=${'Cloudflare API token with DNS and Zone permissions'} .required=${false}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Amazon Web Services'} .subtitle=${'Configure AWS credentials'} .variant=${'outline'}>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('aws')}
|
||||||
|
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('aws'); }}></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<dees-input-text .key=${'awsAccessKey'} .label=${'Access Key ID'} .value=${this.settings.awsAccessKey || ''} .isPasswordBool=${true} .description=${'AWS IAM access key identifier'} .required=${false}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'awsSecretKey'} .label=${'Secret Access Key'} .value=${this.settings.awsSecretKey || ''} .isPasswordBool=${true} .description=${'AWS IAM secret access key'} .required=${false}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-dropdown .key=${'awsRegion'} .label=${'Default Region'} .selectedOption=${this.settings.awsRegion || 'us-east-1'} .options=${[
|
||||||
|
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
|
||||||
|
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
|
||||||
|
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
|
||||||
|
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
|
||||||
|
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
|
||||||
|
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
|
||||||
|
]} .description=${'Default AWS region for resource provisioning'}></dees-input-dropdown>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'DigitalOcean'} .subtitle=${'Configure DigitalOcean API access'} .variant=${'outline'}>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('digitalocean')}
|
||||||
|
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('digitalocean'); }}></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text .key=${'digitalOceanToken'} .label=${'Personal Access Token'} .value=${this.settings.digitalOceanToken || ''} .isPasswordBool=${true} .description=${'DigitalOcean personal access token with read/write scope'} .required=${false}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Microsoft Azure'} .subtitle=${'Configure Azure service principal'} .variant=${'outline'}>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('azure')}
|
||||||
|
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('azure'); }}></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<dees-input-text .key=${'azureClientId'} .label=${'Application (Client) ID'} .value=${this.settings.azureClientId || ''} .isPasswordBool=${true} .description=${'Azure AD application client ID'} .required=${false}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'azureClientSecret'} .label=${'Client Secret'} .value=${this.settings.azureClientSecret || ''} .isPasswordBool=${true} .description=${'Azure AD application client secret'} .required=${false}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<dees-input-text .key=${'azureTenantId'} .label=${'Directory (Tenant) ID'} .value=${this.settings.azureTenantId || ''} .description=${'Azure AD tenant identifier'} .required=${false}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'azureSubscriptionId'} .label=${'Subscription ID'} .value=${this.settings.azureSubscriptionId || ''} .description=${'Azure subscription for resource management'} .required=${false}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Google Cloud Platform'} .subtitle=${'Configure GCP service account'} .variant=${'outline'}>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('google')}
|
||||||
|
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('google'); }}></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-textarea .key=${'googleCloudKeyJson'} .label=${'Service Account Key (JSON)'} .value=${this.settings.googleCloudKeyJson || ''} .isPasswordBool=${true} .description=${'Complete JSON key file for service account authentication'} .required=${false}></dees-input-textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text .key=${'googleCloudProjectId'} .label=${'Project ID'} .value=${this.settings.googleCloudProjectId || ''} .description=${'Google Cloud project identifier'} .required=${false}></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<div class="actions-container">
|
||||||
|
<dees-form-submit .text=${'Save All Settings'} .disabled=${this.isLoading}></dees-form-submit>
|
||||||
|
</div>
|
||||||
|
</dees-form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-view-settings': CloudlyViewSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
308
ts_web/elements/views/tasks/index.ts
Normal file
308
ts_web/elements/views/tasks/index.ts
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import * as shared from '../../shared/index.js';
|
||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../../../appstate.js';
|
||||||
|
import './parts/cloudly-task-panel.js';
|
||||||
|
import './parts/cloudly-execution-details.js';
|
||||||
|
import { formatCronFriendly, formatDate, formatDuration } from './utils.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-tasks')
|
||||||
|
export class CloudlyViewTasks extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private loading = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private filterStatus: string = 'all';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private searchQuery: string = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private categoryFilter: string = 'all';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private autoRefresh: boolean = true;
|
||||||
|
|
||||||
|
private _refreshHandle: any = null;
|
||||||
|
@state()
|
||||||
|
private canceling: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subscription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subscription);
|
||||||
|
|
||||||
|
// Load initial data (non-blocking)
|
||||||
|
this.loadInitialData();
|
||||||
|
|
||||||
|
// Start periodic refresh (lightweight; executions only by default)
|
||||||
|
this.startAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadInitialData() {
|
||||||
|
try {
|
||||||
|
await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {});
|
||||||
|
await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load initial task data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAutoRefresh() {
|
||||||
|
this.stopAutoRefresh();
|
||||||
|
if (!this.autoRefresh) return;
|
||||||
|
this._refreshHandle = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await this.loadExecutionsWithFilter();
|
||||||
|
} catch (err) {
|
||||||
|
// ignore transient errors during refresh
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopAutoRefresh() {
|
||||||
|
if (this._refreshHandle) {
|
||||||
|
clearInterval(this._refreshHandle);
|
||||||
|
this._refreshHandle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectedCallback(): Promise<void> {
|
||||||
|
await (super.disconnectedCallback?.());
|
||||||
|
this.stopAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.toolbar { display: flex; gap: 12px; align-items: center; margin: 4px 0 16px 0; flex-wrap: wrap; }
|
||||||
|
.toolbar .spacer { flex: 1 1 auto; }
|
||||||
|
.search-input { background: #111; color: #ddd; border: 1px solid #333; border-radius: 6px; padding: 8px 10px; min-width: 220px; }
|
||||||
|
.chipbar { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.chip { padding: 6px 10px; background: #2a2a2a; color: #bbb; border: 1px solid #444; border-radius: 16px; cursor: pointer; transition: all 0.2s; user-select: none; }
|
||||||
|
.chip.active { background: #2196f3; border-color: #2196f3; color: white; }
|
||||||
|
|
||||||
|
.task-list { display: flex; flex-direction: column; gap: 16px; margin-bottom: 32px; }
|
||||||
|
|
||||||
|
.secondary-button { padding: 6px 12px; background: #2b2b2b; color: #ccc; border: 1px solid #444; border-radius: 6px; cursor: pointer; font-size: 0.9em; transition: background 0.2s, border-color 0.2s; }
|
||||||
|
.secondary-button:hover { background: #363636; border-color: #555; }
|
||||||
|
|
||||||
|
/* Shared badge styles used within the table content */
|
||||||
|
.status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
|
||||||
|
.status-running { background: #2196f3; color: white; }
|
||||||
|
.status-completed { background: #4caf50; color: white; }
|
||||||
|
.status-failed { background: #f44336; color: white; }
|
||||||
|
.status-cancelled { background: #ff9800; color: white; }
|
||||||
|
|
||||||
|
.execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; }
|
||||||
|
.log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; }
|
||||||
|
.log-info { color: #2196f3; }
|
||||||
|
.log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); }
|
||||||
|
.log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); }
|
||||||
|
.log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private async triggerTask(taskName: string) {
|
||||||
|
try {
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Run Task: ${taskName}`,
|
||||||
|
content: html`<div><p>Do you want to trigger this task now?</p></div>`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Run now',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName });
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: `Task ${taskName} triggered`, type: 'success' });
|
||||||
|
await modalArg.destroy();
|
||||||
|
await this.loadExecutionsWithFilter();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to trigger task:', error);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to trigger: ${error.message}`, type: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cancelTaskFor(taskName: string) {
|
||||||
|
try {
|
||||||
|
const executions = (this.data.taskExecutions || [])
|
||||||
|
.filter((e: any) => e.data.taskName === taskName && e.data.status === 'running')
|
||||||
|
.sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0));
|
||||||
|
const running = executions[0];
|
||||||
|
if (!running) return;
|
||||||
|
|
||||||
|
this.canceling = { ...this.canceling, [running.id]: true };
|
||||||
|
try {
|
||||||
|
await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: running.id });
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancelled ${taskName}`, type: 'success' });
|
||||||
|
} finally {
|
||||||
|
this.canceling = { ...this.canceling, [running.id]: false };
|
||||||
|
await this.loadExecutionsWithFilter();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to cancel task:', err);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err.message}`, type: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadExecutionsWithFilter() {
|
||||||
|
try {
|
||||||
|
const filter: any = {};
|
||||||
|
if (this.filterStatus !== 'all') {
|
||||||
|
filter.status = this.filterStatus;
|
||||||
|
}
|
||||||
|
await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, { filter });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load executions:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
|
||||||
|
this.selectedExecution = execution;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.shadowRoot?.querySelector('cloudly-sectionheading + cloudly-execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) {
|
||||||
|
await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Logs: ${execution.data.taskName}`,
|
||||||
|
content: html`
|
||||||
|
<div class="execution-logs">
|
||||||
|
${(execution.data.logs || []).map((log: any) => html`
|
||||||
|
<div class="log-entry log-${log.severity}"><span>${formatDate(log.timestamp)}</span> - ${log.message}</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Copy All',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText((execution.data.logs || [])
|
||||||
|
.map((l: any) => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n'));
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' });
|
||||||
|
} catch (e) {
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ name: 'Close', action: async (modalArg: any) => modalArg.destroy() }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const tasks = (this.data.tasks || []) as any[];
|
||||||
|
const categories = Array.from(new Set(tasks.map(t => t.category))).sort();
|
||||||
|
const filteredTasks = tasks
|
||||||
|
.filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter)
|
||||||
|
.filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Tasks</cloudly-sectionheading>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Task Library'} .subtitle=${'Run maintenance, monitoring and system tasks'} .variant=${'outline'}>
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="chipbar">
|
||||||
|
<div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}"
|
||||||
|
@click=${() => { this.categoryFilter = 'all'; }}>
|
||||||
|
All
|
||||||
|
</div>
|
||||||
|
${categories.map(cat => html`
|
||||||
|
<div class="chip ${this.categoryFilter === cat ? 'active' : ''}"
|
||||||
|
@click=${() => { this.categoryFilter = cat; }}>
|
||||||
|
${cat}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<input class="search-input" placeholder="Search tasks" .value=${this.searchQuery}
|
||||||
|
@input=${(e: any) => { this.searchQuery = e.target.value; }} />
|
||||||
|
<button class="secondary-button" @click=${async () => { await this.loadExecutionsWithFilter(); }}>Refresh</button>
|
||||||
|
<button class="secondary-button" @click=${() => { this.autoRefresh = !this.autoRefresh; this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); }}>
|
||||||
|
${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-list">
|
||||||
|
${filteredTasks.map(task => html`
|
||||||
|
<cloudly-task-panel
|
||||||
|
.task=${task}
|
||||||
|
.executions=${this.data.taskExecutions || []}
|
||||||
|
.canceling=${this.canceling}
|
||||||
|
.onRun=${(name: string) => this.triggerTask(name)}
|
||||||
|
.onCancel=${(name: string) => this.cancelTaskFor(name)}
|
||||||
|
.onOpenDetails=${(exec: any) => this.openExecutionDetails(exec)}
|
||||||
|
.onOpenLogs=${(exec: any) => this.openLogsModal(exec)}
|
||||||
|
></cloudly-task-panel>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
|
||||||
|
|
||||||
|
<dees-panel .title=${'Recent Executions'} .subtitle=${'History of task runs and their outcomes'} .variant=${'outline'}>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Task Executions'}
|
||||||
|
.heading2=${'History of task runs and their outcomes'}
|
||||||
|
.data=${this.data.taskExecutions || []}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||||
|
return {
|
||||||
|
Task: itemArg.data.taskName,
|
||||||
|
Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`,
|
||||||
|
'Started At': formatDate(itemArg.data.startedAt),
|
||||||
|
Duration: itemArg.data.duration ? formatDuration(itemArg.data.duration) : '-',
|
||||||
|
'Triggered By': itemArg.data.triggeredBy,
|
||||||
|
Logs: itemArg.data.logs?.length || 0,
|
||||||
|
} as any;
|
||||||
|
}}
|
||||||
|
.actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => {
|
||||||
|
const actions: any[] = [
|
||||||
|
{ name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], actionFunc: async () => { this.selectedExecution = itemArg; } }
|
||||||
|
];
|
||||||
|
if (itemArg.data.status === 'running') {
|
||||||
|
actions.push({ name: 'Cancel', iconName: 'lucide:SquareX', type: ['inRow'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); await this.loadExecutionsWithFilter(); } });
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}}
|
||||||
|
></dees-table>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
${this.selectedExecution ? html`
|
||||||
|
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
|
||||||
|
<cloudly-execution-details .execution=${this.selectedExecution}></cloudly-execution-details>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-view-tasks': CloudlyViewTasks;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element';
|
||||||
|
import { formatDate, formatDuration } from '../utils.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-execution-details')
|
||||||
|
export class CloudlyExecutionDetails extends DeesElement {
|
||||||
|
@property({ type: Object }) execution: any;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
.execution-details h3, .execution-details h4 { margin: 8px 0; }
|
||||||
|
.metrics { display: flex; gap: 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #333; }
|
||||||
|
.metric { display: flex; flex-direction: column; }
|
||||||
|
.metric-label { color: #666; font-size: 0.85em; }
|
||||||
|
.metric-value { color: #fff; font-size: 1.1em; font-weight: 600; }
|
||||||
|
.execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; }
|
||||||
|
.log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; }
|
||||||
|
.log-info { color: #2196f3; }
|
||||||
|
.log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); }
|
||||||
|
.log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); }
|
||||||
|
.log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const execution = this.execution;
|
||||||
|
if (!execution) return html``;
|
||||||
|
return html`
|
||||||
|
<div class="execution-details">
|
||||||
|
<h3>Execution Details: ${execution.data.taskName}</h3>
|
||||||
|
<div class="metrics">
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Started</span>
|
||||||
|
<span class="metric-value">${formatDate(execution.data.startedAt)}</span>
|
||||||
|
</div>
|
||||||
|
${execution.data.completedAt ? html`
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Completed</span>
|
||||||
|
<span class="metric-value">${formatDate(execution.data.completedAt)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${execution.data.duration ? html`
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Duration</span>
|
||||||
|
<span class="metric-value">${formatDuration(execution.data.duration)}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">Triggered By</span>
|
||||||
|
<span class="metric-value">${execution.data.triggeredBy}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${execution.data.logs && execution.data.logs.length > 0 ? html`
|
||||||
|
<h4>Logs</h4>
|
||||||
|
<div class="execution-logs">
|
||||||
|
${execution.data.logs.map((log: any) => html`
|
||||||
|
<div class="log-entry log-${log.severity}">
|
||||||
|
<span>${formatDate(log.timestamp)}</span> - ${log.message}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${execution.data.metrics ? html`
|
||||||
|
<h4>Metrics</h4>
|
||||||
|
<div class="metrics">
|
||||||
|
${Object.entries(execution.data.metrics).map(([key, value]) => html`
|
||||||
|
<div class="metric">
|
||||||
|
<span class="metric-label">${key}</span>
|
||||||
|
<span class="metric-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span>
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${execution.data.error ? html`
|
||||||
|
<h4>Error</h4>
|
||||||
|
<div class="execution-logs">
|
||||||
|
<div class="log-entry log-error">
|
||||||
|
${execution.data.error.message}
|
||||||
|
${execution.data.error.stack ? html`<pre>${execution.data.error.stack}</pre>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-execution-details': CloudlyExecutionDetails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
206
ts_web/elements/views/tasks/parts/cloudly-task-panel.ts
Normal file
206
ts_web/elements/views/tasks/parts/cloudly-task-panel.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element';
|
||||||
|
import { formatCronFriendly, formatDuration, formatRelativeTime, getCategoryHue, getCategoryIcon } from '../utils.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-task-panel')
|
||||||
|
export class CloudlyTaskPanel extends DeesElement {
|
||||||
|
@property({ type: Object }) task: any;
|
||||||
|
@property({ type: Array }) executions: any[] = [];
|
||||||
|
@property({ type: Object }) canceling: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
// Callbacks provided by parent view
|
||||||
|
@property({ attribute: false }) onRun?: (taskName: string) => void;
|
||||||
|
@property({ attribute: false }) onCancel?: (taskName: string) => void;
|
||||||
|
@property({ attribute: false }) onOpenDetails?: (execution: any) => void;
|
||||||
|
@property({ attribute: false }) onOpenLogs?: (execution: any) => void;
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
.task-panel {
|
||||||
|
background: #131313;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.task-panel:hover { border-color: #3a3a3a; }
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.header-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
|
.header-right { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.task-icon { color: #cfcfcf; font-size: 28px; }
|
||||||
|
.task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.task-subtitle { color: #8c8c8c; font-size: 0.9em; }
|
||||||
|
|
||||||
|
.task-description {
|
||||||
|
color: #b5b5b5;
|
||||||
|
font-size: 0.95em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
.metric-item {
|
||||||
|
background: #0f0f0f;
|
||||||
|
border: 1px solid #2c2c2c;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.metric-item .label { color: #8d8d8d; font-size: 0.8em; }
|
||||||
|
.metric-item .value { color: #eaeaea; font-weight: 600; margin-top: 4px; }
|
||||||
|
|
||||||
|
.lastline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||||
|
.dot.info { background: #2196f3; }
|
||||||
|
.dot.success { background: #4caf50; }
|
||||||
|
.dot.warning { background: #ff9800; }
|
||||||
|
.dot.error { background: #f44336; }
|
||||||
|
|
||||||
|
.panel-footer { display: flex; gap: 12px; margin-top: 12px; }
|
||||||
|
|
||||||
|
.link-button { background: transparent; border: none; color: #8ab4ff; cursor: pointer; padding: 0; font-size: 0.95em; }
|
||||||
|
.link-button:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.status-running { background: #2196f3; color: white; }
|
||||||
|
.status-completed { background: #4caf50; color: white; }
|
||||||
|
.status-failed { background: #f44336; color: white; }
|
||||||
|
.status-cancelled { background: #ff9800; color: white; }
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private computeData() {
|
||||||
|
const task = this.task || {};
|
||||||
|
const executions = this.executions || [];
|
||||||
|
const lastExecution = executions
|
||||||
|
.filter((e: any) => e.data.taskName === task.name)
|
||||||
|
.sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0];
|
||||||
|
const isRunning = lastExecution?.data.status === 'running';
|
||||||
|
const executionsForTask = executions.filter((e: any) => e.data.taskName === task.name);
|
||||||
|
const now = Date.now();
|
||||||
|
const last24hCount = executionsForTask.filter((e: any) => (e.data.startedAt || 0) > now - 86_400_000).length;
|
||||||
|
const completed = executionsForTask.filter((e: any) => e.data.status === 'completed');
|
||||||
|
const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0;
|
||||||
|
const avgDuration = completed.length ? Math.round(completed.reduce((acc: number, e: any) => acc + (e.data.duration || 0), 0) / completed.length) : undefined;
|
||||||
|
const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null;
|
||||||
|
const subtitle = [
|
||||||
|
task.category,
|
||||||
|
task.schedule ? `⏱ ${formatCronFriendly(task.schedule)}` : null,
|
||||||
|
isRunning
|
||||||
|
? (lastExecution?.data.startedAt ? `Started ${formatRelativeTime(lastExecution.data.startedAt)}` : 'Running')
|
||||||
|
: (task.lastRun ? `Last ${formatRelativeTime(task.lastRun)}` : 'Never run')
|
||||||
|
].filter(Boolean).join(' • ');
|
||||||
|
return { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle };
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const task = this.task;
|
||||||
|
const { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle } = this.computeData();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="task-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<dees-icon class="task-icon" .icon=${getCategoryIcon(task.category)}></dees-icon>
|
||||||
|
<div>
|
||||||
|
<div class="task-name" title=${task.name}>${task.name}</div>
|
||||||
|
<div class="task-subtitle" title=${task.schedule || ''}>${subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
${lastExecution ? html`<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>` : html`<span class="status-badge" style="background:#2e2e2e;color:#ddd;border:1px solid #3a3a3a;">idle</span>`}
|
||||||
|
${isRunning ? html`
|
||||||
|
<dees-spinner style="--size: 18px"></dees-spinner>
|
||||||
|
<dees-button
|
||||||
|
.text=${this.canceling[lastExecution!.id] ? 'Cancelling…' : 'Cancel'}
|
||||||
|
.type=${'secondary'}
|
||||||
|
.disabled=${!!this.canceling[lastExecution!.id]}
|
||||||
|
@click=${() => this.onCancel?.(task.name)}
|
||||||
|
></dees-button>
|
||||||
|
` : html`
|
||||||
|
<dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.onRun?.(task.name)}></dees-button>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-description" title=${task.description || ''}>${task.description}</div>
|
||||||
|
|
||||||
|
${lastExecution ? html`
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">Last Status</div>
|
||||||
|
<div class="value">
|
||||||
|
<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">Avg Duration</div>
|
||||||
|
<div class="value">${avgDuration ? formatDuration(avgDuration) : '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">24h Runs · Success</div>
|
||||||
|
<div class="value">${last24hCount} · ${successRate}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lastline">
|
||||||
|
${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'}
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<button class="link-button" @click=${() => this.onOpenDetails?.(lastExecution)}>Details</button>
|
||||||
|
${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.onOpenLogs?.(lastExecution)}>Logs</button>` : ''}
|
||||||
|
</div>
|
||||||
|
` : html`
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">Last Status</div>
|
||||||
|
<div class="value">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">Avg Duration</div>
|
||||||
|
<div class="value">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-item">
|
||||||
|
<div class="label">24h Runs · Success</div>
|
||||||
|
<div class="value">0 · 0%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'cloudly-task-panel': CloudlyTaskPanel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
68
ts_web/elements/views/tasks/utils.ts
Normal file
68
ts_web/elements/views/tasks/utils.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
export function formatDate(timestamp: number): string {
|
||||||
|
return new Date(timestamp).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDuration(ms: number): string {
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
|
||||||
|
return `${(ms / 3600000).toFixed(1)}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRelativeTime(ts?: number): string {
|
||||||
|
if (!ts) return '-';
|
||||||
|
const diff = Date.now() - ts;
|
||||||
|
const abs = Math.abs(diff);
|
||||||
|
if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`;
|
||||||
|
if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`;
|
||||||
|
if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`;
|
||||||
|
return `${Math.round(abs / 86_400_000)}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategoryIcon(category: string): string {
|
||||||
|
switch (category) {
|
||||||
|
case 'maintenance':
|
||||||
|
return 'lucide:Wrench';
|
||||||
|
case 'deployment':
|
||||||
|
return 'lucide:Rocket';
|
||||||
|
case 'backup':
|
||||||
|
return 'lucide:Archive';
|
||||||
|
case 'monitoring':
|
||||||
|
return 'lucide:Activity';
|
||||||
|
case 'cleanup':
|
||||||
|
return 'lucide:Trash2';
|
||||||
|
case 'system':
|
||||||
|
return 'lucide:Settings';
|
||||||
|
case 'security':
|
||||||
|
return 'lucide:Shield';
|
||||||
|
default:
|
||||||
|
return 'lucide:Play';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCategoryHue(category: string): number {
|
||||||
|
switch (category) {
|
||||||
|
case 'maintenance': return 28; // orange
|
||||||
|
case 'deployment': return 208; // blue
|
||||||
|
case 'backup': return 122; // green
|
||||||
|
case 'monitoring': return 280; // purple
|
||||||
|
case 'cleanup': return 20; // brownish
|
||||||
|
case 'system': return 200; // steel
|
||||||
|
case 'security': return 0; // red
|
||||||
|
default: return 210; // default blue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCronFriendly(cron?: string): string {
|
||||||
|
if (!cron) return '';
|
||||||
|
const parts = cron.trim().split(/\s+/);
|
||||||
|
if (parts.length !== 5) return cron; // fallback
|
||||||
|
const [min, hour, dom, mon, dow] = parts;
|
||||||
|
if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute';
|
||||||
|
if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`;
|
||||||
|
if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`;
|
||||||
|
if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly';
|
||||||
|
if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily';
|
||||||
|
if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly';
|
||||||
|
return cron;
|
||||||
|
}
|
||||||
@@ -19,3 +19,7 @@ export {
|
|||||||
webjwt,
|
webjwt,
|
||||||
smartstate,
|
smartstate,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose API client so UI can share it with CLI
|
||||||
|
import * as servezoneApi from '@serve.zone/api';
|
||||||
|
export { servezoneApi };
|
||||||
|
|||||||
Reference in New Issue
Block a user