adjast wss + docker logs
This commit is contained in:
@@ -31,9 +31,17 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
|
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
|
||||||
|
|
||||||
// --- НОВОЕ: Docker API для управления контейнерами (Созидание) ---
|
// --- НОВОЕ: Docker API для управления контейнерами (Созидание) ---
|
||||||
|
// Ядро и API
|
||||||
|
implementation 'com.github.docker-java:docker-java-api:3.3.6'
|
||||||
implementation 'com.github.docker-java:docker-java-core:3.3.6'
|
implementation 'com.github.docker-java:docker-java-core:3.3.6'
|
||||||
|
|
||||||
|
// Транспорт (HTTP Client 5) — именно он нужен для DockerConfig.java
|
||||||
implementation 'com.github.docker-java:docker-java-transport-httpclient5:3.3.6'
|
implementation 'com.github.docker-java:docker-java-transport-httpclient5:3.3.6'
|
||||||
|
|
||||||
|
// Spring Boot Starter для STOMP WebSockets
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-websocket'
|
||||||
|
|
||||||
|
|
||||||
// Работа с SQL и PostgreSQL[cite: 3]
|
// Работа с SQL и PostgreSQL[cite: 3]
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
implementation 'org.postgresql:postgresql:42.6.0'
|
implementation 'org.postgresql:postgresql:42.6.0'
|
||||||
|
|||||||
Generated
+130
-3
@@ -22,6 +22,7 @@
|
|||||||
"@material/material-color-utilities": "^0.3.0",
|
"@material/material-color-utilities": "^0.3.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "19.0.0",
|
"@ng-bootstrap/ng-bootstrap": "19.0.0",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"angular-markdown-editor": "^3.1.1",
|
"angular-markdown-editor": "^3.1.1",
|
||||||
"bootstrap": "5.3.6",
|
"bootstrap": "5.3.6",
|
||||||
"marked": "^18.0.3",
|
"marked": "^18.0.3",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"ngx-markdown": "^20.0.0",
|
"ngx-markdown": "^20.0.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
"sockjs-client": "^1.6.1",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"viewerjs": "^1.11.7",
|
"viewerjs": "^1.11.7",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
@@ -44,6 +46,7 @@
|
|||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/marked": "^5.0.2",
|
"@types/marked": "^5.0.2",
|
||||||
"@types/node": "^20.17.19",
|
"@types/node": "^20.17.19",
|
||||||
|
"@types/sockjs-client": "^1.5.4",
|
||||||
"jasmine-core": "~5.7.0",
|
"jasmine-core": "~5.7.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
@@ -3986,6 +3989,12 @@
|
|||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@stomp/stompjs": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@tufjs/canonical-json": {
|
"node_modules/@tufjs/canonical-json": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
|
||||||
@@ -4487,6 +4496,13 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sockjs-client": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -6692,6 +6708,18 @@
|
|||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/faye-websocket": {
|
||||||
|
"version": "0.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
||||||
|
"integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"websocket-driver": ">=0.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -7166,6 +7194,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-parser-js": {
|
||||||
|
"version": "0.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
|
||||||
|
"integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/http-proxy": {
|
"node_modules/http-proxy": {
|
||||||
"version": "1.18.1",
|
"version": "1.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
@@ -7301,7 +7335,6 @@
|
|||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ini": {
|
"node_modules/ini": {
|
||||||
@@ -8774,7 +8807,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/msgpackr": {
|
"node_modules/msgpackr": {
|
||||||
@@ -9657,6 +9689,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/querystringify": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -9728,7 +9766,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/restore-cursor": {
|
"node_modules/restore-cursor": {
|
||||||
@@ -9912,6 +9949,26 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-regex-test": {
|
"node_modules/safe-regex-test": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||||
@@ -10311,6 +10368,43 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sockjs-client": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^3.2.7",
|
||||||
|
"eventsource": "^2.0.2",
|
||||||
|
"faye-websocket": "^0.11.4",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"url-parse": "^1.5.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/sockjs-client"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sockjs-client/node_modules/debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sockjs-client/node_modules/eventsource": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/socks": {
|
"node_modules/socks": {
|
||||||
"version": "2.8.8",
|
"version": "2.8.8",
|
||||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz",
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz",
|
||||||
@@ -10777,6 +10871,16 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url-parse": {
|
||||||
|
"version": "1.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"querystringify": "^2.1.1",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
@@ -10989,6 +11093,29 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/websocket-driver": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"http-parser-js": ">=0.5.1",
|
||||||
|
"safe-buffer": ">=5.1.0",
|
||||||
|
"websocket-extensions": ">=0.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket-extensions": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"@material/material-color-utilities": "^0.3.0",
|
"@material/material-color-utilities": "^0.3.0",
|
||||||
"@ng-bootstrap/ng-bootstrap": "19.0.0",
|
"@ng-bootstrap/ng-bootstrap": "19.0.0",
|
||||||
"@popperjs/core": "2.11.8",
|
"@popperjs/core": "2.11.8",
|
||||||
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"angular-markdown-editor": "^3.1.1",
|
"angular-markdown-editor": "^3.1.1",
|
||||||
"bootstrap": "5.3.6",
|
"bootstrap": "5.3.6",
|
||||||
"marked": "^18.0.3",
|
"marked": "^18.0.3",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"ngx-markdown": "^20.0.0",
|
"ngx-markdown": "^20.0.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
"sockjs-client": "^1.6.1",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"viewerjs": "^1.11.7",
|
"viewerjs": "^1.11.7",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
@@ -50,6 +52,7 @@
|
|||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@types/marked": "^5.0.2",
|
"@types/marked": "^5.0.2",
|
||||||
"@types/node": "^20.17.19",
|
"@types/node": "^20.17.19",
|
||||||
|
"@types/sockjs-client": "^1.5.4",
|
||||||
"jasmine-core": "~5.7.0",
|
"jasmine-core": "~5.7.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
|||||||
+66
-42
@@ -1,13 +1,14 @@
|
|||||||
|
// project-details.component.ts
|
||||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Subscription, timer, switchMap } from 'rxjs';
|
import { Subscription, timer, switchMap, catchError, of } from 'rxjs';
|
||||||
import { ProjectService, Project, DockerContainer, ProjectDetailResponse } from '../../../services/project.service';
|
import { ProjectService, Project, DockerContainer, ProjectDetailResponse } from '../../../services/project.service';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
import {MatCard, MatCardContent} from '@angular/material/card';
|
import { MatCard, MatCardContent } from '@angular/material/card';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-project-details',
|
selector: 'app-project-details',
|
||||||
@@ -25,16 +26,16 @@ import {MatCard, MatCardContent} from '@angular/material/card';
|
|||||||
styleUrls: ['./project-details.component.scss']
|
styleUrls: ['./project-details.component.scss']
|
||||||
})
|
})
|
||||||
export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChecked {
|
export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChecked {
|
||||||
@ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Привязка к #consoleScroll
|
@ViewChild('consoleScroll') private consoleContainer!: ElementRef; // Прив'язка до вікна терміналу
|
||||||
|
|
||||||
project?: Project;
|
project?: Project;
|
||||||
config?: any;
|
config?: any;
|
||||||
containers: DockerContainer[] = [];
|
containers: DockerContainer[] = [];
|
||||||
logs: string[] = [];
|
logs: string[] = []; // Масив для зберігання рядків логів
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
private pollingSub?: Subscription;
|
private pollingSub?: Subscription; // Підписка на моніторинг контейнерів
|
||||||
private logSub?: Subscription;
|
private logSub?: Subscription; // Підписка на потік логів через STOMP
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -50,46 +51,90 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Завантаження основних даних проекту
|
||||||
private loadData(id: string): void {
|
private loadData(id: string): void {
|
||||||
this.projectService.getProjectDetails(id).subscribe({
|
this.projectService.getProjectDetails(id).subscribe({
|
||||||
next: (data: ProjectDetailResponse) => {
|
next: (data: ProjectDetailResponse) => {
|
||||||
this.project = data.project;
|
this.project = data.project;
|
||||||
this.config = data.config;
|
this.config = data.config;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.connectLogs(id);
|
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.error('Ошибка загрузки данных проекта:', err);
|
console.error('Помилка завантаження проекту:', err);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Опитування стану контейнерів кожні 5 секунд
|
||||||
private startMonitoring(id: string): void {
|
private startMonitoring(id: string): void {
|
||||||
// Опрос состояния контейнеров каждые 5 секунд[cite: 5, 6]
|
|
||||||
this.pollingSub = timer(0, 5000).pipe(
|
this.pollingSub = timer(0, 5000).pipe(
|
||||||
switchMap(() => this.projectService.getProjectContainers(id))
|
switchMap(() => this.projectService.getProjectContainers(id).pipe(
|
||||||
|
catchError(() => of([])) // У разі помилки повертаємо порожній список
|
||||||
|
))
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: (data) => this.containers = data,
|
next: (data: DockerContainer[]) => {
|
||||||
error: (err) => console.error('Ошибка мониторинга контейнеров:', err)
|
this.containers = data;
|
||||||
|
|
||||||
|
// Якщо з'явилися контейнери і ми ще не підключені до логів — підключаємося
|
||||||
|
if (this.containers.length > 0 && !this.logSub) {
|
||||||
|
const mainContainer = this.containers.find(c => c.state === 'running') || this.containers[0];
|
||||||
|
this.connectLogs(mainContainer.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => console.error('Помилка моніторингу:', err)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private connectLogs(id: string): void {
|
// Підключення до трансляції логів
|
||||||
|
connectLogs(id: string): void {
|
||||||
|
console.log(`Підключення до логів контейнера: ${id}`);
|
||||||
|
|
||||||
|
// Очищаємо попередню підписку перед створенням нової
|
||||||
|
if (this.logSub) {
|
||||||
|
this.logSub.unsubscribe();
|
||||||
|
this.projectService.disconnectLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logs = []; // Очищаємо консоль при зміні контейнера
|
||||||
|
|
||||||
this.logSub = this.projectService.connectToDockerLogs(id).subscribe({
|
this.logSub = this.projectService.connectToDockerLogs(id).subscribe({
|
||||||
next: (msg) => this.logs.push(msg),
|
next: (msg) => {
|
||||||
error: (err) => console.error('WS Error:', err)
|
this.logs.push(msg);
|
||||||
|
// Обмежуємо кількість рядків у пам'яті (останні 200)
|
||||||
|
if (this.logs.length > 200) this.logs.shift();
|
||||||
|
},
|
||||||
|
error: (err) => console.error('STOMP Error:', err)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy(): void {
|
deploy(): void {
|
||||||
if (this.project) this.projectService.deployProject(this.project.id).subscribe();
|
if (this.project) {
|
||||||
|
this.loading = true;
|
||||||
|
this.projectService.deployProject(this.project.id).subscribe({
|
||||||
|
next: () => this.loading = false,
|
||||||
|
error: () => this.loading = false
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
if (this.project) this.projectService.stopProject(this.project.id).subscribe();
|
if (this.project) this.projectService.stopProject(this.project.id).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confirmDelete() {
|
||||||
|
if (confirm("Знищити проект назавжди?")) {
|
||||||
|
this.pollingSub?.unsubscribe(); // Зупиняємо оновлення інтерфейсу
|
||||||
|
|
||||||
|
if (this.project) {
|
||||||
|
this.projectService.deleteProject(this.project.id).subscribe({
|
||||||
|
next: () => this.router.navigate(['/main/user/projects']),
|
||||||
|
error: () => this.router.navigate(['/main/user/projects'])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewChecked(): void {
|
ngAfterViewChecked(): void {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
@@ -103,44 +148,23 @@ export class ProjectDetailsComponent implements OnInit, OnDestroy, AfterViewChec
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.pollingSub?.unsubscribe();
|
this.pollingSub?.unsubscribe();
|
||||||
this.logSub?.unsubscribe();
|
this.logSub?.unsubscribe();
|
||||||
|
this.projectService.disconnectLogs(); // Закриваємо з'єднання з брокером
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmDelete() {
|
// Форматування для відображення кольорів у терміналі
|
||||||
if (confirm("Уничтожить проект навсегда?")) {
|
|
||||||
// 1. Сначала «ослепляем» компонент, чтобы он перестал слать запросы
|
|
||||||
this.pollingSub?.unsubscribe(); // Твой метод, который делает subscription.unsubscribe()
|
|
||||||
|
|
||||||
// 2. Только теперь отправляем команду на ликвидацию
|
|
||||||
if (this.project) this.projectService.deleteProject(this.project.id).subscribe({
|
|
||||||
next: () => {
|
|
||||||
console.log("Проект стерт, уходим на дашборд");
|
|
||||||
this.router.navigate(['/main/user/projects']);
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
console.error("Ошибка при удалении, но проект мог уже исчезнуть", err);
|
|
||||||
// Всё равно уходим, так как оставаться на странице 404 нет смысла
|
|
||||||
this.router.navigate(['/main/user/projects']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Метод для окрашивания логов в терминале
|
|
||||||
formatLogLine(line: string): string {
|
formatLogLine(line: string): string {
|
||||||
if (!line) return '';
|
if (!line) return '';
|
||||||
|
|
||||||
// Окрашиваем ключевые слова для лучшей читаемости
|
|
||||||
let formatted = line;
|
let formatted = line;
|
||||||
|
|
||||||
if (line.includes('ERROR') || line.includes('Error') || line.includes('failed')) {
|
if (line.includes('ERROR') || line.includes('Error') || line.includes('failed')) {
|
||||||
formatted = `<span style="color: #ff5f56; font-weight: bold;">${line}</span>`;
|
formatted = `<span style="color: #ff5f56; font-weight: bold;">${line}</span>`;
|
||||||
} else if (line.includes('WARN') || line.includes('Warning') || line.includes('Conflict')) {
|
} else if (line.includes('WARN') || line.includes('Warning')) {
|
||||||
formatted = `<span style="color: #ffbd2e;">${line}</span>`;
|
formatted = `<span style="color: #ffbd2e;">${line}</span>`;
|
||||||
} else if (line.includes('INFO') || line.includes('success') || line.includes('Started')) {
|
} else if (line.includes('INFO') || line.includes('success')) {
|
||||||
formatted = `<span style="color: #27c93f;">${line}</span>`;
|
formatted = `<span style="color: #27c93f;">${line}</span>`;
|
||||||
} else if (line.includes('DEBUG')) {
|
} else if (line.includes('DEBUG')) {
|
||||||
formatted = `<span style="color: #ae81ff;">${line}</span>`;
|
formatted = `<span style="color: #ae81ff;">${line}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import {Observable, Subject} from 'rxjs';
|
||||||
import { GlobalConstants } from '../global-constants';
|
import { GlobalConstants } from '../global-constants';
|
||||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
|
||||||
|
import SockJS from 'sockjs-client';
|
||||||
|
import { Client, IMessage } from '@stomp/stompjs';
|
||||||
|
|
||||||
// Унифицируем название интерфейса для списка контейнеров
|
// Унифицируем название интерфейса для списка контейнеров
|
||||||
export interface DockerContainer {
|
export interface DockerContainer {
|
||||||
@@ -44,6 +46,7 @@ export interface ProjectDetailResponse {
|
|||||||
export class ProjectService {
|
export class ProjectService {
|
||||||
// Используем baseUrl, как определено в начале класса
|
// Используем baseUrl, как определено в начале класса
|
||||||
private baseUrl = GlobalConstants.API_URL;
|
private baseUrl = GlobalConstants.API_URL;
|
||||||
|
private stompClient: Client | null = null;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
@@ -80,12 +83,40 @@ export class ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket для трансляции логов в реальном времени[cite: 3, 5]
|
// WebSocket для трансляции логов в реальном времени[cite: 3, 5]
|
||||||
public connectToDockerLogs(projectId: string): WebSocketSubject<any> {
|
public connectToDockerLogs(containerId: string): Observable<string> {
|
||||||
const socketUrl = this.baseUrl.replace('http', 'ws') + '/ws-jambotron/' + projectId;
|
const logSubject = new Subject<string>();
|
||||||
return webSocket({
|
|
||||||
url: socketUrl,
|
// Твой эндпоинт из WebSocketConfig.java
|
||||||
deserializer: (msg) => msg.data
|
const socket = new SockJS(`${this.baseUrl}/ws-jambotron`);
|
||||||
|
|
||||||
|
this.stompClient = new Client({
|
||||||
|
webSocketFactory: () => socket,
|
||||||
|
debug: (str) => console.log(str),
|
||||||
|
onConnect: () => {
|
||||||
|
// 1. Подписываемся на логи конкретного контейнера
|
||||||
|
this.stompClient?.subscribe(`/topic/logs/${containerId}`, (message: IMessage) => {
|
||||||
|
logSubject.next(message.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Отправляем сигнал бэкенду начать стриминг (согласно LogController)
|
||||||
|
this.stompClient?.publish({
|
||||||
|
destination: '/app/start-logs',
|
||||||
|
body: containerId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onStompError: (frame) => {
|
||||||
|
logSubject.error('Broker error: ' + frame.headers['message']);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.stompClient.activate();
|
||||||
|
return logSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectLogs(): void {
|
||||||
|
if (this.stompClient) {
|
||||||
|
this.stompClient.deactivate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ИСПРАВЛЕНО: возвращаем DockerContainer и используем правильный baseUrl[cite: 5]
|
// ИСПРАВЛЕНО: возвращаем DockerContainer и используем правильный baseUrl[cite: 5]
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
package com.jambotronGroup.jambotron.WebSocket;
|
package com.jambotronGroup.jambotron.WebSocket;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
import org.springframework.web.socket.config.annotation.*;
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSocket
|
@EnableWebSocketMessageBroker
|
||||||
public class WebSocketConfig implements WebSocketConfigurer {
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||||
|
|
||||||
private final DockerLogsHandler dockerLogsHandler;
|
@Override
|
||||||
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||||
public WebSocketConfig(DockerLogsHandler dockerLogsHandler) {
|
// Брокер для сообщений клиенту
|
||||||
this.dockerLogsHandler = dockerLogsHandler;
|
config.enableSimpleBroker("/topic");
|
||||||
|
// Префикс для запросов от клиента к серверу
|
||||||
|
config.setApplicationDestinationPrefixes("/app");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||||
registry.addHandler(dockerLogsHandler, "/api/ws-jambotron/{projectId}")
|
registry.addEndpoint("/api/ws-jambotron")
|
||||||
// Поскольку есть proxy.conf.json, запросы приходят как Same-Origin.
|
|
||||||
// Но для стабильности разработки лучше явно указать localhost.
|
|
||||||
.setAllowedOriginPatterns("https://*.jambotron.com", "https://jambotron.com")
|
.setAllowedOriginPatterns("https://*.jambotron.com", "https://jambotron.com")
|
||||||
|
.setAllowedOrigins("http://localhost:8082", "http://localhost:4200")
|
||||||
.setAllowedOrigins("http://localhost:8082", "http://localhost:4200");
|
.withSockJS(); // Для стабильности соединения
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.jambotronGroup.jambotron.console;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync // <--- Обязательно добавь это
|
||||||
|
public class AsyncConfig {
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.jambotronGroup.jambotron.console;
|
||||||
|
|
||||||
|
import com.github.dockerjava.api.DockerClient;
|
||||||
|
import com.github.dockerjava.api.async.ResultCallback;
|
||||||
|
import com.github.dockerjava.api.model.Frame;
|
||||||
|
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class DockerLogService {
|
||||||
|
|
||||||
|
private final SimpMessagingTemplate messagingTemplate;
|
||||||
|
private final DockerClient dockerClient;
|
||||||
|
|
||||||
|
// Spring сам подставит сюда dockerClient из твоего DockerConfig
|
||||||
|
public DockerLogService(SimpMessagingTemplate messagingTemplate, DockerClient dockerClient) {
|
||||||
|
this.messagingTemplate = messagingTemplate;
|
||||||
|
this.dockerClient = dockerClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
public void streamLogs(String containerId) {
|
||||||
|
dockerClient.logContainerCmd(containerId)
|
||||||
|
.withStdOut(true)
|
||||||
|
.withStdErr(true)
|
||||||
|
.withFollowStream(true)
|
||||||
|
.withTail(100)
|
||||||
|
.exec(new ResultCallback.Adapter<Frame>() {
|
||||||
|
@Override
|
||||||
|
public void onNext(Frame frame) {
|
||||||
|
String logLine = new String(frame.getPayload());
|
||||||
|
// Отправляем в топик, на который подпишется фронтенд
|
||||||
|
messagingTemplate.convertAndSend("/topic/logs/" + containerId, logLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.jambotronGroup.jambotron.controllers;
|
||||||
|
|
||||||
|
import com.jambotronGroup.jambotron.console.DockerLogService;
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class LogController {
|
||||||
|
|
||||||
|
private final DockerLogService dockerLogService;
|
||||||
|
|
||||||
|
public LogController(DockerLogService dockerLogService) {
|
||||||
|
this.dockerLogService = dockerLogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Когда клиент шлет сообщение на /app/start-logs
|
||||||
|
@MessageMapping("/start-logs")
|
||||||
|
public void handleStartLogs(String containerId) {
|
||||||
|
dockerLogService.streamLogs(containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user