diff --git a/package.json b/package.json index 27d4539..cf21293 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,18 @@ "axios": "^1.7.9", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.1.3" + "react-router-dom": "^7.1.3", + "rsocket-core": "^0.0.27", + "rsocket-flowable": "^0.0.27", + "rsocket-websocket-client": "^0.0.27" }, "devDependencies": { "@eslint/js": "^9.17.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", + "@types/rsocket-core": "^0.0.11", + "@types/rsocket-flowable": "^0.0.8", + "@types/rsocket-websocket-client": "^0.0.7", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ea186d..4bf43b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: axios: specifier: ^1.7.9 version: 1.7.9 + buffer: + specifier: ^6.0.3 + version: 6.0.3 react: specifier: ^18.3.1 version: 18.3.1 @@ -26,19 +29,40 @@ importers: react-router-dom: specifier: ^7.1.3 version: 7.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rsocket-core: + specifier: ^0.0.27 + version: 0.0.27 + rsocket-flowable: + specifier: ^0.0.27 + version: 0.0.27 + rsocket-websocket-client: + specifier: ^0.0.27 + version: 0.0.27 devDependencies: '@eslint/js': specifier: ^9.17.0 version: 9.19.0 + '@rollup/plugin-inject': + specifier: ^5.0.5 + version: 5.0.5(rollup@4.32.0) '@types/react': specifier: ^18.3.18 version: 18.3.18 '@types/react-dom': specifier: ^18.3.5 version: 18.3.5(@types/react@18.3.18) + '@types/rsocket-core': + specifier: ^0.0.11 + version: 0.0.11 + '@types/rsocket-flowable': + specifier: ^0.0.8 + version: 0.0.8 + '@types/rsocket-websocket-client': + specifier: ^0.0.7 + version: 0.0.7 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.0.11) + version: 4.3.4(vite@6.0.11(@types/node@22.13.0)) eslint: specifier: ^9.17.0 version: 9.19.0 @@ -59,7 +83,7 @@ importers: version: 8.21.0(eslint@9.19.0)(typescript@5.6.3) vite: specifier: ^6.0.5 - version: 6.0.11 + version: 6.0.11(@types/node@22.13.0) packages: @@ -479,6 +503,24 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@rollup/plugin-inject@5.0.5': + resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.32.0': resolution: {integrity: sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg==} cpu: [arm] @@ -595,6 +637,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@22.13.0': + resolution: {integrity: sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==} + '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} @@ -606,6 +651,18 @@ packages: '@types/react@18.3.18': resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==} + '@types/rsocket-core@0.0.11': + resolution: {integrity: sha512-hCM5786b0OIwEfyMgM2EepnZaNZNp4be8NFwB+ilFyGDJDizhgog80e3tw/Os2sdd5ky8aIRTGeOFSZXXmNUrA==} + + '@types/rsocket-flowable@0.0.8': + resolution: {integrity: sha512-It+fbW3fiUJLsL1IkvWL8K18gt4oMpXGgjbSA3hhRLdnkPlBCCqpPzT4WlD8rQtojSoer6bcSVjqQQQQCds+fQ==} + + '@types/rsocket-types@0.0.6': + resolution: {integrity: sha512-l9ZKGUc9OWcF0BJvhl3d6nFEVudOeB4BVrXupu+oOYQWYPNGARSTMrw9ZzBQsRR+PKZvGevpQLTPY17n80Cgsg==} + + '@types/rsocket-websocket-client@0.0.7': + resolution: {integrity: sha512-G4j1P4T67hDyV8Ah1ooqqfy0YV0DqVi4s60BjB4gtQIcZPY6d4AA9Ld2T+1iUEQQCTugkoQkt50mKWB2RwPyvA==} + '@typescript-eslint/eslint-plugin@8.21.0': resolution: {integrity: sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -694,6 +751,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -709,6 +769,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -841,6 +904,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -929,6 +995,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1006,6 +1075,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1074,6 +1146,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + postcss@8.5.1: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} @@ -1372,6 +1448,18 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rsocket-core@0.0.27: + resolution: {integrity: sha512-q/+8PY0BIPvwXb6jPsnKCdyUKFusrm4JDzWnxbW1Ywu2sG/suv28/+mHC7kRGtMnRsxsLAWnqsmI6EznTabItA==} + + rsocket-flowable@0.0.27: + resolution: {integrity: sha512-I/1vETXP+qCfGFeMY8i+QLhyBpcEj0nDmWnP2vh7lDyOBNT9LjAiY6kigUzEHbfhoNe3vnJWYK6uCzxIsi622Q==} + + rsocket-types@0.0.27: + resolution: {integrity: sha512-88vgA+d31a6wW5YP63jQZ0CqZ3DALtH4oCbuMtIcNy28m1kDYGPZunhsnw+YC/XtjOqb5hKhn10ZQm7OK2xc1g==} + + rsocket-websocket-client@0.0.27: + resolution: {integrity: sha512-1BX2O1C1R5W78kI7J5E/nbzxawUOceLggeG1b7rnIGZgCbnftGoIOF5xU/Q268UCE4NNtaDRED4xSHJpdAVFBw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1455,6 +1543,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + update-browserslist-db@1.1.2: resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} hasBin: true @@ -1922,6 +2013,22 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@rollup/plugin-inject@5.0.5(rollup@4.32.0)': + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.32.0) + estree-walker: 2.0.2 + magic-string: 0.30.17 + optionalDependencies: + rollup: 4.32.0 + + '@rollup/pluginutils@5.1.4(rollup@4.32.0)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.32.0 + '@rollup/rollup-android-arm-eabi@4.32.0': optional: true @@ -2006,6 +2113,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@22.13.0': + dependencies: + undici-types: 6.20.0 + '@types/prop-types@15.7.14': {} '@types/react-dom@18.3.5(@types/react@18.3.18)': @@ -2017,6 +2128,27 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/rsocket-core@0.0.11': + dependencies: + '@types/node': 22.13.0 + '@types/rsocket-flowable': 0.0.8 + '@types/rsocket-types': 0.0.6 + + '@types/rsocket-flowable@0.0.8': + dependencies: + '@types/rsocket-types': 0.0.6 + + '@types/rsocket-types@0.0.6': + dependencies: + '@types/node': 22.13.0 + '@types/rsocket-flowable': 0.0.8 + + '@types/rsocket-websocket-client@0.0.7': + dependencies: + '@types/rsocket-core': 0.0.11 + '@types/rsocket-flowable': 0.0.8 + '@types/rsocket-types': 0.0.6 + '@typescript-eslint/eslint-plugin@8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.19.0)(typescript@5.6.3))(eslint@9.19.0)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -2094,14 +2226,14 @@ snapshots: '@typescript-eslint/types': 8.21.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-react@4.3.4(vite@6.0.11)': + '@vitejs/plugin-react@4.3.4(vite@6.0.11(@types/node@22.13.0))': dependencies: '@babel/core': 7.26.7 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.7) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.7) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.0.11 + vite: 6.0.11(@types/node@22.13.0) transitivePeerDependencies: - supports-color @@ -2194,6 +2326,8 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -2214,6 +2348,11 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + callsites@3.1.0: {} caniuse-lite@1.0.30001695: {} @@ -2371,6 +2510,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + esutils@2.0.3: {} fast-deep-equal@3.1.3: {} @@ -2442,6 +2583,8 @@ snapshots: has-flag@4.0.0: {} + ieee754@1.2.1: {} + ignore@5.3.2: {} import-fresh@3.3.0: @@ -2504,6 +2647,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + merge2@1.4.1: {} micromatch@4.0.8: @@ -2562,6 +2709,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.2: {} + postcss@8.5.1: dependencies: nanoid: 3.3.8 @@ -2957,6 +3106,23 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.32.0 fsevents: 2.3.3 + rsocket-core@0.0.27: + dependencies: + rsocket-flowable: 0.0.27 + rsocket-types: 0.0.27 + + rsocket-flowable@0.0.27: {} + + rsocket-types@0.0.27: + dependencies: + rsocket-flowable: 0.0.27 + + rsocket-websocket-client@0.0.27: + dependencies: + rsocket-core: 0.0.27 + rsocket-flowable: 0.0.27 + rsocket-types: 0.0.27 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3023,6 +3189,8 @@ snapshots: typescript@5.6.3: {} + undici-types@6.20.0: {} + update-browserslist-db@1.1.2(browserslist@4.24.4): dependencies: browserslist: 4.24.4 @@ -3033,12 +3201,13 @@ snapshots: dependencies: punycode: 2.3.1 - vite@6.0.11: + vite@6.0.11(@types/node@22.13.0): dependencies: esbuild: 0.24.2 postcss: 8.5.1 rollup: 4.32.0 optionalDependencies: + '@types/node': 22.13.0 fsevents: 2.3.3 which@2.0.2: diff --git a/src/App.tsx b/src/App.tsx index 418d559..4a719ce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import { Layout, Menu } from "antd"; import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"; import Dashboard from "./pages/Dashboard"; @@ -8,11 +8,30 @@ import Images from "./pages/Images"; const { Header, Content, Footer, Sider } = Layout; const App: React.FC = () => { + const [collapsed, setCollapsed] = useState(false); + + const toggleSider = () => { + setCollapsed((prevCollapsed) => !prevCollapsed); + }; + return ( - - + setCollapsed(collapse)} + > +
+ Dashboard @@ -25,7 +44,27 @@ const App: React.FC = () => { -
+
+ {/* Логотип, по клику на который переключается состояние Sider */} +
+ My Logo +
+
} /> @@ -33,7 +72,9 @@ const App: React.FC = () => { } /> -
Docker Admin ©2025
+
+ Docker Admin ©2025 +
diff --git a/src/api/apiClient.ts b/src/api/apiClient.ts index 590232d..8f88593 100644 --- a/src/api/apiClient.ts +++ b/src/api/apiClient.ts @@ -59,56 +59,6 @@ export const diffContainer = (id: string) => export const statsContainer = (id: string) => apiClientContainer.get(`/stats?id=${id}`, { responseType: "stream" }); -export const streamLogsContainer = async ( - id: string, - follow: boolean = true, - tail: number | null = null, - onData: (log: any) => void -): Promise => { - const params = new URLSearchParams(); - params.append("id", id); - params.append("follow", String(follow)); - if (tail !== null) { - params.append("tail", String(tail)); - } - - const response = await fetch(`/api/docker/container/local/logContainer?${params.toString()}`, { - method: "GET", - headers: { - "Accept": "application/x-ndjson", - }, - }); - - if (!response.body) { - throw new Error("ReadableStream not supported in this environment."); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let buffer = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - - const lines = buffer.split("\n"); - buffer = lines.pop()!; - - for (const line of lines) { - if (line.trim()) { - try { - const log = JSON.parse(line); - onData(log); - } catch (e) { - console.error("Failed to parse JSON line:", line, e); - } - } - } - } -}; - export const createContainer = (data: any) => apiClientContainer.post(`/createContainer`, data); diff --git a/src/api/rsocketApi.ts b/src/api/rsocketApi.ts new file mode 100644 index 0000000..0c92c98 --- /dev/null +++ b/src/api/rsocketApi.ts @@ -0,0 +1,76 @@ +import { + encodeCompositeMetadata, + encodeRoute, + MESSAGE_RSOCKET_ROUTING, + RSocketClient, +} from "rsocket-core"; +import RSocketWebSocketClient from "rsocket-websocket-client"; +import {Flowable} from "rsocket-flowable"; + +const rsocketClient = new RSocketClient({ + setup: { + keepAlive: 60000, + lifetime: 180000, + dataMimeType: "application/json", + metadataMimeType: "message/x.rsocket.composite-metadata.v0", + }, + transport: new RSocketWebSocketClient({ + url: "/rsocket", + wsCreator: (url) => new WebSocket(url) as any, + }), +}); + +const connectionPromise: Promise = new Promise((resolve, reject) => { + rsocketClient.connect().subscribe({ + onComplete: (socket: any) => { + resolve(socket); + }, + onError: (error: any) => { + console.error(error); + reject(error); + }, + onSubscribe: (cancel: any) => { + console.log("Подписка на соединение:", cancel); + }, + }); +}); + +function requestStream(route: string, data: string): Flowable { + return new Flowable((subscriber) => { + connectionPromise.then((socket) => { + const metadata = encodeCompositeMetadata([[MESSAGE_RSOCKET_ROUTING, encodeRoute(route)]]); + socket + .requestStream({ + data, + metadata, + }) + .subscribe({ + onSubscribe: (subscription: any) => { + subscription.request(1); + }, + onNext: (payload: any) => { + subscriber.onNext(payload.data); + }, + onError: (error: any) => { + console.error(error); + subscriber.onError(error); + }, + onComplete: () => subscriber.onComplete(), + }); + }); + }); +} + +export function pullImage(configId: string, request: any): Flowable { + const route = `docker.image.${configId}.pullImage`; + return requestStream(route, JSON.stringify(request)); +} + +export function logContainer( + configId: string, + request: { id: string; follow: boolean; tail?: number } +): Flowable { + const route = `docker.client.${configId}.logContainer`; + + return requestStream(route, JSON.stringify(request)); +} diff --git a/src/pages/Containers.tsx b/src/pages/Containers.tsx index 9f4b282..d5d12d2 100644 --- a/src/pages/Containers.tsx +++ b/src/pages/Containers.tsx @@ -1,25 +1,40 @@ -import React, {useEffect, useState} from "react"; -import {Table, Button, message, Space, Modal} from "antd"; +import React, { useEffect, useState, useRef } from "react"; +import { Table, Button, message, Space, Modal } from "antd"; +import { ColumnsType } from "antd/es/table"; +import type { Breakpoint } from "antd/es/_util/responsiveObserve"; import { fetchContainers, removeContainer, startContainer, stopContainer, restartContainer, - streamLogsContainer } from "../api/apiClient"; +import { logContainer as rsocketLogContainer } from "../api/rsocketApi"; + +interface ContainerData { + id: string; + image: string; + state: string; + name: string; +} + +interface RSocketSubscription { + request: (n: number) => void; + cancel: () => void; +} const Containers: React.FC = () => { - const [containers, setContainers] = useState([]); + const [containers, setContainers] = useState([]); const [loading, setLoading] = useState(false); const [logsVisible, setLogsVisible] = useState(false); const [logsContent, setLogsContent] = useState(""); + const logSubscriptionRef = useRef(null); const loadContainers = async () => { setLoading(true); try { - const {data} = await fetchContainers(true); - const formattedContainers = data.map((container: any) => ({ + const { data } = await fetchContainers(true); + const formattedContainers: ContainerData[] = data.map((container: any) => ({ id: container.Id, image: container.Image, state: container.State, @@ -33,6 +48,12 @@ const Containers: React.FC = () => { } }; + const shortenId = (id: string): string => { + const prefix = "sha256:"; + const trimmed = id.startsWith(prefix) ? id.slice(prefix.length) : id; + return trimmed.slice(0, 8); + }; + const handleStart = async (id: string) => { try { await startContainer(id); @@ -73,44 +94,75 @@ const Containers: React.FC = () => { } }; - const handleViewLogs = async (id: string) => { + const handleViewLogs = (id: string) => { setLogsContent(""); setLogsVisible(true); - try { - await streamLogsContainer(id, true, 100, (log) => { + rsocketLogContainer("local", { + id: id, + follow: true, + tail: 100, + }).subscribe({ + onSubscribe: (subscription: RSocketSubscription) => { + logSubscriptionRef.current = subscription; + subscription.request(2147483647); + }, + onNext: (log: unknown) => { setLogsContent((prev) => `${prev}${JSON.stringify(log)}\n`); - }); - } catch (err) { - message.error("Failed to fetch container logs"); - setLogsVisible(false); + }, + onError: (err: Error) => { + message.error("Failed to fetch container logs"); + console.error(err) + setLogsVisible(false); + }, + onComplete: () => { + console.log("Logs stream completed"); + }, + }); + }; + + const handleCloseLogs = () => { + setLogsVisible(false); + if (logSubscriptionRef.current) { + logSubscriptionRef.current.cancel(); + logSubscriptionRef.current = null; } }; useEffect(() => { loadContainers(); + + return () => { + if (logSubscriptionRef.current) { + logSubscriptionRef.current.cancel(); + } + }; }, []); - const columns = [ + const columns: ColumnsType = [ { title: "ID", dataIndex: "id", key: "id", + render: (id: string) => shortenId(id), }, { title: "Name", dataIndex: "name", key: "name", + responsive: ["md"] as Breakpoint[], }, { title: "Image", dataIndex: "image", key: "image", + responsive: ["md"] as Breakpoint[], }, { title: "State", dataIndex: "state", key: "state", + responsive: ["md"] as Breakpoint[], render: (state: string) => ( { fontWeight: "bold", }} > - {state} - + {state} + ), }, { title: "Actions", key: "actions", - render: (_: any, record: any) => ( + render: (_: unknown, record: ContainerData) => ( {record.state !== "running" && ( @@ -83,7 +100,14 @@ const Images: React.FC = () => { width={800} > {modalData ? ( -
+                    
             {JSON.stringify(modalData, null, 2)}
           
) : ( diff --git a/vite.config.ts b/vite.config.ts index e9f0e18..1aecdfc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,19 @@ -import { defineConfig } from 'vite' +import {defineConfig} from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ - plugins: [react()], - server: { - proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true, - }, + plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + '/rsocket': { + target: 'ws://localhost:8080', + ws: true, + changeOrigin: true, + }, + }, }, - }, });