소스 검색

update plugins

qiaofeng1227 2 년 전
부모
커밋
3bebd8b680
32개의 변경된 파일396개의 추가작업 그리고 131개의 파일을 삭제
  1. 5 5
      cockpit/appstore/build/asset-manifest.json
  2. 1 1
      cockpit/appstore/build/index.html
  3. 0 0
      cockpit/appstore/build/static/js/688.694c9b06.chunk.js
  4. 0 0
      cockpit/appstore/build/static/js/688.694c9b06.chunk.js.map
  5. 0 0
      cockpit/appstore/build/static/js/688.bf21350d.chunk.js
  6. 0 0
      cockpit/appstore/build/static/js/688.bf21350d.chunk.js.map
  7. 0 1
      cockpit/appstore/build/static/js/main.8d3e5d4b.js
  8. 0 0
      cockpit/appstore/build/static/js/main.8d3e5d4b.js.LICENSE.txt
  9. 0 0
      cockpit/appstore/build/static/js/main.8d3e5d4b.js.map
  10. 44 8
      cockpit/appstore/src/pages/appstore.js
  11. 10 10
      cockpit/myapps/build/asset-manifest.json
  12. 1 1
      cockpit/myapps/build/index.html
  13. 0 0
      cockpit/myapps/build/static/css/main.1e5ef24c.css
  14. 0 0
      cockpit/myapps/build/static/css/main.1e5ef24c.css.map
  15. 0 0
      cockpit/myapps/build/static/css/main.751babb1.css.map
  16. 0 0
      cockpit/myapps/build/static/js/145.2fc71954.chunk.js
  17. 0 0
      cockpit/myapps/build/static/js/145.2fc71954.chunk.js.map
  18. 1 0
      cockpit/myapps/build/static/js/888.41d89770.chunk.js
  19. 0 0
      cockpit/myapps/build/static/js/888.41d89770.chunk.js.LICENSE.txt
  20. 0 0
      cockpit/myapps/build/static/js/888.41d89770.chunk.js.map
  21. 0 0
      cockpit/myapps/build/static/js/927.1961e223.chunk.js
  22. 0 0
      cockpit/myapps/build/static/js/927.1961e223.chunk.js.map
  23. 0 1
      cockpit/myapps/build/static/js/96.2a832dc4.chunk.js
  24. 0 0
      cockpit/myapps/build/static/js/96.2a832dc4.chunk.js.map
  25. 0 1
      cockpit/myapps/build/static/js/main.bc6762b5.js
  26. 0 0
      cockpit/myapps/build/static/js/main.bc6762b5.js.LICENSE.txt
  27. 0 0
      cockpit/myapps/build/static/js/main.bc6762b5.js.map
  28. 18 1
      cockpit/myapps/src/assets/scss/app.scss
  29. 121 2
      cockpit/myapps/src/pages/appdetail.js
  30. 94 95
      cockpit/myapps/src/pages/appdetailtabs/appcontainer.js
  31. 94 0
      cockpit/myapps/src/pages/appdetailtabs/appterminal.js
  32. 7 5
      cockpit/myapps/src/pages/myapps.js

+ 5 - 5
cockpit/appstore/build/asset-manifest.json

@@ -1,8 +1,8 @@
 {
   "files": {
     "main.css": "./static/css/main.751babb1.css",
-    "main.js": "./static/js/main.a27efeeb.js",
-    "static/js/688.bf21350d.chunk.js": "./static/js/688.bf21350d.chunk.js",
+    "main.js": "./static/js/main.8d3e5d4b.js",
+    "static/js/688.694c9b06.chunk.js": "./static/js/688.694c9b06.chunk.js",
     "static/js/376.0505e571.chunk.js": "./static/js/376.0505e571.chunk.js",
     "static/js/426.910887ac.chunk.js": "./static/js/426.910887ac.chunk.js",
     "static/js/912.833f32c9.chunk.js": "./static/js/912.833f32c9.chunk.js",
@@ -43,8 +43,8 @@
     "static/media/logo-sm.svg": "./static/media/logo-sm.53b8ca70620b0b2968874a3660f195dd.svg",
     "index.html": "./index.html",
     "main.751babb1.css.map": "./static/css/main.751babb1.css.map",
-    "main.a27efeeb.js.map": "./static/js/main.a27efeeb.js.map",
-    "688.bf21350d.chunk.js.map": "./static/js/688.bf21350d.chunk.js.map",
+    "main.8d3e5d4b.js.map": "./static/js/main.8d3e5d4b.js.map",
+    "688.694c9b06.chunk.js.map": "./static/js/688.694c9b06.chunk.js.map",
     "376.0505e571.chunk.js.map": "./static/js/376.0505e571.chunk.js.map",
     "426.910887ac.chunk.js.map": "./static/js/426.910887ac.chunk.js.map",
     "912.833f32c9.chunk.js.map": "./static/js/912.833f32c9.chunk.js.map",
@@ -53,6 +53,6 @@
   },
   "entrypoints": [
     "static/css/main.751babb1.css",
-    "static/js/main.a27efeeb.js"
+    "static/js/main.8d3e5d4b.js"
   ]
 }

+ 1 - 1
cockpit/appstore/build/index.html

@@ -1 +1 @@
-<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><script type="text/javascript" src="../base1/cockpit.js"></script><script type="text/javascript" src="../*/po.js"></script><link rel="manifest" href="./manifest.json"/><title>App Store</title><script defer="defer" src="./static/js/main.a27efeeb.js"></script><link href="./static/css/main.751babb1.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" style="height:100%"></div></body></html>
+<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><script type="text/javascript" src="../base1/cockpit.js"></script><script type="text/javascript" src="../*/po.js"></script><link rel="manifest" href="./manifest.json"/><title>App Store</title><script defer="defer" src="./static/js/main.8d3e5d4b.js"></script><link href="./static/css/main.751babb1.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" style="height:100%"></div></body></html>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/appstore/build/static/js/688.694c9b06.chunk.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/appstore/build/static/js/688.694c9b06.chunk.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/appstore/build/static/js/688.bf21350d.chunk.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/appstore/build/static/js/688.bf21350d.chunk.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
cockpit/appstore/build/static/js/main.8d3e5d4b.js


+ 0 - 0
cockpit/appstore/build/static/js/main.a27efeeb.js.LICENSE.txt → cockpit/appstore/build/static/js/main.8d3e5d4b.js.LICENSE.txt


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/appstore/build/static/js/main.8d3e5d4b.js.map


+ 44 - 8
cockpit/appstore/src/pages/appstore.js

@@ -26,6 +26,7 @@ const getContentfulData = gql`
                 id 
             }
             key
+            hot
             trademark
             summary
             overview
@@ -39,11 +40,11 @@ const getContentfulData = gql`
             logo {
                 imageurl
             }
-            catalogCollection(limit:20) {
+            catalogCollection(limit:15) {
                 items {
                 key
                 title
-                catalogCollection(limit:1){
+                catalogCollection(limit:5){
                     items{
                         key
                         title
@@ -58,12 +59,14 @@ const getContentfulData = gql`
             catalogCollection(limit:20) {
                 items {
                 key
+                position
                 title
                 linkedFrom(allowedLocales:["en-US"]) {
                     catalogCollection(limit:20) {
                     items {
                         key
                         title
+                        position
                     }
                     }
                 }
@@ -301,8 +304,18 @@ const AppStore = (): React$Element<React$FragmentType> => {
         }
     };
 
-    const mainCatalogs = allData?.catalog.linkedFrom.catalogCollection.items; //主目录数据
-    //const apps = allData?.productCollection?.items;//所有应用数据
+    //主目录数据
+    const mainCatalogs = allData?.catalog.linkedFrom.catalogCollection.items?.sort(function (a, b) {
+        if (a.position === null && b.position === null) {
+            return 0;
+        } else if (a.position === null) {
+            return 1;
+        } else if (b.position === null) {
+            return -1;
+        } else {
+            return a.position - b.position;
+        }
+    });
 
     const [apps, setApps] = useState(null); //用于存储通过目录筛选出来的数据
     const [appList, setAppList] = useState(null); //用于存储通过目录筛选出来的数据
@@ -314,8 +327,20 @@ const AppStore = (): React$Element<React$FragmentType> => {
             skipCount += allData.productCollection.items.length;
             // 调用fetchMoreProducts函数来获取更多的产品,如果有的话
             fetchMoreProducts();
-            setAppList(allData.productCollection?.items);
-            setApps(allData.productCollection?.items);
+            //对产品根据hot排序:降序
+            const data = allData.productCollection?.items?.sort(function (a, b) {
+                if (a.hot === null && b.hot === null) {
+                    return 0;
+                } else if (a.hot === null) {
+                    return 1;
+                } else if (b.hot === null) {
+                    return -1;
+                } else {
+                    return b.hot - a.hot;
+                }
+            });
+            setAppList(data);
+            setApps(data);
         }
     }, [allData])
 
@@ -345,7 +370,18 @@ const AppStore = (): React$Element<React$FragmentType> => {
             selectedMainCatalog === 'All'
                 ? []
                 : mainCatalogs.filter(c => c.key === selectedMainCatalog)?.[0]?.linkedFrom?.catalogCollection?.items;
-        setSubCatalogs(updatedData);
+        const data = updatedData.sort(function (a, b) {
+            if (a.position === null && b.position === null) {
+                return 0;
+            } else if (a.position === null) {
+                return 1;
+            } else if (b.position === null) {
+                return -1;
+            } else {
+                return a.position - b.position;
+            }
+        });
+        setSubCatalogs(data);
 
         //根据主目录过滤app数据
         let subCatalogApps = null;
@@ -379,7 +415,7 @@ const AppStore = (): React$Element<React$FragmentType> => {
         updatedData =
             searchString === ""
                 ? apps
-                : apps.filter(app => { return app.trademark.toLowerCase().includes(searchString) || app.key.toLowerCase().includes(searchString) });
+                : apps.filter(app => { return app.trademark.toLowerCase().includes(searchString) || app.key.toLowerCase().includes(searchString) || app.summary.toLowerCase().includes(searchString) });
 
         setAppList(updatedData);
         setIsAllSelected(true);

+ 10 - 10
cockpit/myapps/build/asset-manifest.json

@@ -1,15 +1,15 @@
 {
   "files": {
-    "main.css": "./static/css/main.751babb1.css",
-    "main.js": "./static/js/main.495e2b6f.js",
-    "static/js/145.2fc71954.chunk.js": "./static/js/145.2fc71954.chunk.js",
+    "main.css": "./static/css/main.1e5ef24c.css",
+    "main.js": "./static/js/main.bc6762b5.js",
+    "static/js/927.1961e223.chunk.js": "./static/js/927.1961e223.chunk.js",
     "static/js/376.bc0e5568.chunk.js": "./static/js/376.bc0e5568.chunk.js",
     "static/js/426.46c5e949.chunk.js": "./static/js/426.46c5e949.chunk.js",
     "static/js/603.8e18e7fa.chunk.js": "./static/js/603.8e18e7fa.chunk.js",
     "static/css/836.5576a615.chunk.css": "./static/css/836.5576a615.chunk.css",
     "static/js/836.06772eb6.chunk.js": "./static/js/836.06772eb6.chunk.js",
     "static/js/912.1f46d2af.chunk.js": "./static/js/912.1f46d2af.chunk.js",
-    "static/js/96.2a832dc4.chunk.js": "./static/js/96.2a832dc4.chunk.js",
+    "static/js/888.41d89770.chunk.js": "./static/js/888.41d89770.chunk.js",
     "static/css/769.c11b83c2.chunk.css": "./static/css/769.c11b83c2.chunk.css",
     "static/js/769.3b95354d.chunk.js": "./static/js/769.3b95354d.chunk.js",
     "static/media/materialdesignicons-webfont.eot": "./static/media/materialdesignicons-webfont.e044ed23c047e571c550.eot",
@@ -63,21 +63,21 @@
     "static/media/status-icon-sprite.svg": "./static/media/status-icon-sprite.4fee9fefc3971799d2dd.svg",
     "static/media/logo-sm.svg": "./static/media/logo-sm.53b8ca70620b0b2968874a3660f195dd.svg",
     "index.html": "./index.html",
-    "main.751babb1.css.map": "./static/css/main.751babb1.css.map",
-    "main.495e2b6f.js.map": "./static/js/main.495e2b6f.js.map",
-    "145.2fc71954.chunk.js.map": "./static/js/145.2fc71954.chunk.js.map",
+    "main.1e5ef24c.css.map": "./static/css/main.1e5ef24c.css.map",
+    "main.bc6762b5.js.map": "./static/js/main.bc6762b5.js.map",
+    "927.1961e223.chunk.js.map": "./static/js/927.1961e223.chunk.js.map",
     "376.bc0e5568.chunk.js.map": "./static/js/376.bc0e5568.chunk.js.map",
     "426.46c5e949.chunk.js.map": "./static/js/426.46c5e949.chunk.js.map",
     "603.8e18e7fa.chunk.js.map": "./static/js/603.8e18e7fa.chunk.js.map",
     "836.5576a615.chunk.css.map": "./static/css/836.5576a615.chunk.css.map",
     "836.06772eb6.chunk.js.map": "./static/js/836.06772eb6.chunk.js.map",
     "912.1f46d2af.chunk.js.map": "./static/js/912.1f46d2af.chunk.js.map",
-    "96.2a832dc4.chunk.js.map": "./static/js/96.2a832dc4.chunk.js.map",
+    "888.41d89770.chunk.js.map": "./static/js/888.41d89770.chunk.js.map",
     "769.c11b83c2.chunk.css.map": "./static/css/769.c11b83c2.chunk.css.map",
     "769.3b95354d.chunk.js.map": "./static/js/769.3b95354d.chunk.js.map"
   },
   "entrypoints": [
-    "static/css/main.751babb1.css",
-    "static/js/main.495e2b6f.js"
+    "static/css/main.1e5ef24c.css",
+    "static/js/main.bc6762b5.js"
   ]
 }

+ 1 - 1
cockpit/myapps/build/index.html

@@ -1 +1 @@
-<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><script type="text/javascript" src="../base1/cockpit.js"></script><script type="text/javascript" src="../*/po.js"></script><link rel="manifest" href="./manifest.json"/><title>App Store</title><script defer="defer" src="./static/js/main.495e2b6f.js"></script><link href="./static/css/main.751babb1.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" style="height:100%"></div></body></html>
+<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><script type="text/javascript" src="../base1/cockpit.js"></script><script type="text/javascript" src="../*/po.js"></script><link rel="manifest" href="./manifest.json"/><title>App Store</title><script defer="defer" src="./static/js/main.bc6762b5.js"></script><link href="./static/css/main.1e5ef24c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" style="height:100%"></div></body></html>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/css/main.1e5ef24c.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/css/main.1e5ef24c.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/css/main.751babb1.css.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/js/145.2fc71954.chunk.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/js/145.2fc71954.chunk.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
cockpit/myapps/build/static/js/888.41d89770.chunk.js


+ 0 - 0
cockpit/myapps/build/static/js/96.2a832dc4.chunk.js.LICENSE.txt → cockpit/myapps/build/static/js/888.41d89770.chunk.js.LICENSE.txt


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/js/888.41d89770.chunk.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/js/927.1961e223.chunk.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/js/927.1961e223.chunk.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
cockpit/myapps/build/static/js/96.2a832dc4.chunk.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/js/96.2a832dc4.chunk.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
cockpit/myapps/build/static/js/main.bc6762b5.js


+ 0 - 0
cockpit/myapps/build/static/js/main.495e2b6f.js.LICENSE.txt → cockpit/myapps/build/static/js/main.bc6762b5.js.LICENSE.txt


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
cockpit/myapps/build/static/js/main.bc6762b5.js.map


+ 18 - 1
cockpit/myapps/src/assets/scss/app.scss

@@ -108,4 +108,21 @@ File: Main Css File
 @import "custom/plugins/react-table"; // react table style
 @import "custom/plugins/ion-rangeslider";
 @import "custom/plugins/jstree";
-@import "custom/plugins/frappe-gantt";
+@import "custom/plugins/frappe-gantt";
+
+
+.myProtainerTerminal {
+    overflow: hidden;
+    /* 16:9 aspect ratio */
+    padding-top: 56.25%;
+    position: relative;
+}
+
+.myProtainerTerminal iframe {
+    border: 0;
+    height: 100%;
+    left: 0;
+    position: absolute;
+    top: 0;
+    width: 100%;
+}

+ 121 - 2
cockpit/myapps/src/pages/appdetail.js

@@ -1,3 +1,4 @@
+import axios from 'axios';
 import classnames from "classnames";
 import cockpit from 'cockpit';
 import React, { useEffect, useRef, useState } from 'react';
@@ -9,6 +10,7 @@ import { AppRestart, AppStart, AppStop } from '../helpers';
 import AppAccess from './appdetailtabs/appaccess';
 import AppContainer from './appdetailtabs/appcontainer';
 import AppOverview from './appdetailtabs/appoverview';
+import AppTerminal from './appdetailtabs/appterminal';
 import Uninstall from './appdetailtabs/appuninstall';
 
 const _ = cockpit.gettext;
@@ -22,6 +24,105 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
     const [restartAppLoading, setRestartAppLoading] = useState(false); //用户显示重启时应用的加载状态
     const navigate = useNavigate(); //用于页面跳转
     const childRef = useRef();
+    const [containersInfo, setContainersInfo] = useState([]);
+    const customer_name = props.current_app.customer_name;
+    const [endpointsId, setEndpointsId] = useState(null);
+    const [mainContainerId, setMainContainerId] = useState(null);
+
+    //通过Portainer的接口获取容器数据
+    const getContainersData = async () => {
+        try {
+            let jwt = window.localStorage.getItem("portainer.JWT"); //获取存储在本地的JWT数据 
+            let id = null;
+
+            //如果获取不到jwt,则模拟登录并写入新的jwt
+            if (jwt === null) {
+                const response = await axios.get('./config.json'); //从项目下读取配置文件
+                if (response.status === 200) {
+                    let config = response.data.PORTAINER;
+                    const { PORTAINER_USERNAME, PORTAINER_PASSWORD, PORTAINER_AUTH_URL, PORTAINER_HOME_PAGE } = config;
+
+                    //调用portainer的登录API,模拟登录
+                    const authResponse = await axios.post(PORTAINER_AUTH_URL, {
+                        username: PORTAINER_USERNAME,
+                        password: PORTAINER_PASSWORD
+                    });
+                    if (authResponse.status === 200) {
+                        jwt = "\"" + authResponse.data.jwt + "\"";
+                        //jwt = authResponse.data.jwt
+                        window.localStorage.setItem('portainer\.JWT', jwt); //关键是将通过API登录后获取的jwt,存储到本地localStorage
+                    } else {
+                        console.error('Error:', authResponse);
+                    }
+                }
+                else {
+                    console.error('Error:', response);
+                }
+            }
+
+            //从portainer接口获取endpoints
+            const endpointsData = await axios.get('/portainer/api/endpoints', {
+                headers: {
+                    'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
+                }
+            });
+            if (endpointsData.status === 200) {
+                //先判断是否获取了“本地”endpoint
+                if (endpointsData.data.length == 0) { //没有“本地”endpoint
+                    //调用添加"本地"环境的接口
+                    const addEndpoint = await axios.post('/portainer/api/endpoints', {},
+                        {
+                            params: {
+                                Name: "local",
+                                EndpointCreationType: 1
+                            },
+                            headers: {
+                                'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
+                            }
+                        }
+                    );
+                    if (addEndpoint.status === 200) {
+                        id = addEndpoint.data?.Id;
+                        setEndpointsId(id);
+                    }
+                    else {
+                        console.error('Error:', addEndpoint);
+                    }
+                }
+                else {
+                    //应该可能会返回“远程”的endpoint,这里只获取“本地”endpoint,条件为URL包含'/var/run/docker.sock'
+                    id = endpointsData.data.find(({ URL }) => URL.includes('/var/run/docker.sock')).Id;
+                    setEndpointsId(id);
+                }
+
+                //调用接口获取
+                const containersData = await axios.get(`/portainer/api/endpoints/${id}/docker/containers/json`, {
+                    headers: {
+                        'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
+                    },
+                    params: {
+                        all: true,
+                        filters: JSON.stringify({ "label": [`com.docker.compose.project=${customer_name}`] })
+                    }
+                })
+                if (containersData.status === 200) {
+                    const data = containersData.data;
+                    const id = data.find(container => container.Names?.[0]?.replace(/^\/|\/$/g, '') === customer_name)?.Id;
+                    setMainContainerId(id);
+                    setContainersInfo(data);
+                }
+                else {
+                    console.error('Error:', containersData);
+                }
+            }
+            else {
+                console.error('Error:', endpointsData);
+            }
+        }
+        catch (error) {
+            console.error('Error:', error);
+        }
+    }
 
     //设置卸载页面的按钮禁用
     const setUninstallButtonDisable = () => {
@@ -50,6 +151,10 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
         setCurrentApp(props.current_app);
     }, [props.current_app]);
 
+    useEffect(() => {
+        getContainersData();
+    }, []);
+
     const tabContents = [
         {
             id: '1',
@@ -67,10 +172,16 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
             id: '3',
             title: _("Container"),
             icon: 'mdi dripicons-stack',
-            text: <AppContainer data={currentApp} />,
+            text: <AppContainer customer_name={customer_name} endpointsId={endpointsId} containersInfo={containersInfo} />,
         },
         {
             id: '4',
+            title: _("Terminal"),
+            icon: 'mdi dripicons-stack',
+            text: <AppTerminal endpointsId={endpointsId} containerId={mainContainerId} />
+        },
+        {
+            id: '5',
             title: _("Uninstall"),
             icon: 'mdi mdi-cog-outline',
             text: <Uninstall data={currentApp} ref={childRef} disabledButton={setAppdetailButtonDisable} enableButton={setAppdetailButtonEnable}
@@ -122,6 +233,7 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
                                             }
                                             else {
                                                 props.onDataChange();
+                                                getContainersData(); //刷新容器数据
                                             }
                                         }
                                         catch (error) {
@@ -166,6 +278,7 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
                                             }
                                             else {
                                                 props.onDataChange();
+                                                getContainersData(); //刷新容器数据
                                             }
                                         }
                                         catch (error) {
@@ -208,6 +321,7 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
                                         }
                                         else {
                                             props.onDataChange();
+                                            getContainersData(); //刷新容器数据
                                         }
                                     }
                                     catch (error) {
@@ -238,6 +352,11 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
                                         {_("Terminal")}
                                     </Tooltip>
                                 }>
+                                {/* <Link to={{ pathname: '/terminal', search: `?id=${currentApp.customer_name}` }}
+                                    style={{ color: "#fff", backgroundColor: "#727cf5", padding: "5px 10px", borderRadius: "3px", borderColor: "#727cf5", marginRight: "10px" }}
+                                    target="_blank">
+                                    <i className="dripicons-code noti-icon"></i>{' '}
+                                </Link> */}
                                 <Link to={{ pathname: '/terminal', search: `?id=${currentApp.customer_name}` }}
                                     style={{ color: "#fff", backgroundColor: "#727cf5", padding: "5px 10px", borderRadius: "3px", borderColor: "#727cf5", marginRight: "10px" }}
                                     target="_blank">
@@ -301,7 +420,7 @@ const AppDetailModal = (props): React$Element<React$FragmentType> => {
                                 return (
                                     <Tab.Pane eventKey={tab.title} id={tab.id} key={index} style={{ height: "100%" }}>
                                         <Row style={{ height: "100%" }}>
-                                            <Col sm="12" style={{ height: tab.title === "Terminal" ? "600px" : "" }}>
+                                            <Col sm="12" /*style={{ height: tab.title === "Terminal" ? "600px" : "" }}*/>
                                                 {tab.text}
                                             </Col>
                                         </Row>

+ 94 - 95
cockpit/myapps/src/pages/appdetailtabs/appcontainer.js

@@ -1,6 +1,5 @@
-import axios from 'axios';
 import cockpit from "cockpit";
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 import { Badge, Button, Card, Col, Row, Table } from 'react-bootstrap';
 import { useNavigate } from 'react-router-dom';
 
@@ -8,106 +7,106 @@ const _ = cockpit.gettext;
 
 const AppContainer = (props): React$Element<React$FragmentType> => {
     const navigate = useNavigate(); //用于页面跳转
-    const [containersInfo, setContainersInfo] = useState([]);
-    const customer_name = props.data.customer_name;
-    const [endpointsId, setEndpointsId] = useState(null);
+    const containersInfo = props.containersInfo;
+    const customer_name = props.customer_name;
+    const endpointsId = props.endpointsId;
 
     //通过Portainer的接口获取容器数据
-    const getContainersData = async () => {
-        try {
-            let jwt = window.localStorage.getItem("portainer.JWT2"); //获取存储在本地的JWT数据 
-            let id = null;
+    // const getContainersData = async () => {
+    //     try {
+    //         let jwt = window.localStorage.getItem("portainer.JWT"); //获取存储在本地的JWT数据 
+    //         let id = null;
 
-            //如果获取不到jwt,则模拟登录并写入新的jwt
-            if (jwt === null) {
-                const response = await axios.get('./config.json'); //从项目下读取配置文件
-                if (response.status === 200) {
-                    let config = response.data.PORTAINER;
-                    const { PORTAINER_USERNAME, PORTAINER_PASSWORD, PORTAINER_AUTH_URL, PORTAINER_HOME_PAGE } = config;
+    //         //如果获取不到jwt,则模拟登录并写入新的jwt
+    //         if (jwt === null) {
+    //             const response = await axios.get('./config.json'); //从项目下读取配置文件
+    //             if (response.status === 200) {
+    //                 let config = response.data.PORTAINER;
+    //                 const { PORTAINER_USERNAME, PORTAINER_PASSWORD, PORTAINER_AUTH_URL, PORTAINER_HOME_PAGE } = config;
 
-                    //调用portainer的登录API,模拟登录
-                    const authResponse = await axios.post(PORTAINER_AUTH_URL, {
-                        username: PORTAINER_USERNAME,
-                        password: PORTAINER_PASSWORD
-                    });
-                    if (authResponse.status === 200) {
-                        jwt = "\"" + authResponse.data.jwt + "\"";
-                        //jwt = authResponse.data.jwt
-                        window.localStorage.setItem('portainer\.JWT2', jwt); //关键是将通过API登录后获取的jwt,存储到本地localStorage
-                    } else {
-                        console.error('Error:', authResponse);
-                    }
-                }
-                else {
-                    console.error('Error:', response);
-                }
-            }
+    //                 //调用portainer的登录API,模拟登录
+    //                 const authResponse = await axios.post(PORTAINER_AUTH_URL, {
+    //                     username: PORTAINER_USERNAME,
+    //                     password: PORTAINER_PASSWORD
+    //                 });
+    //                 if (authResponse.status === 200) {
+    //                     jwt = "\"" + authResponse.data.jwt + "\"";
+    //                     //jwt = authResponse.data.jwt
+    //                     window.localStorage.setItem('portainer\.JWT', jwt); //关键是将通过API登录后获取的jwt,存储到本地localStorage
+    //                 } else {
+    //                     console.error('Error:', authResponse);
+    //                 }
+    //             }
+    //             else {
+    //                 console.error('Error:', response);
+    //             }
+    //         }
 
-            //从portainer接口获取endpoints
-            const endpointsData = await axios.get('/portainer/api/endpoints', {
-                headers: {
-                    'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
-                }
-            });
-            if (endpointsData.status === 200) {
-                //先判断是否获取了“本地”endpoint
-                if (endpointsData.data.length == 0) { //没有“本地”endpoint
-                    //调用添加"本地"环境的接口
-                    const addEndpoint = await axios.post('/portainer/api/endpoints', {},
-                        {
-                            params: {
-                                Name: "websoft9-local",
-                                EndpointCreationType: 1
-                            },
-                            headers: {
-                                'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
-                            }
-                        }
-                    );
-                    if (addEndpoint.status === 200) {
-                        id = addEndpoint.data?.Id;
-                        setEndpointsId(id);
-                    }
-                    else {
-                        console.error('Error:', addEndpoint);
-                    }
-                }
-                else {
-                    //应该可能会返回“远程”的endpoint,这里只获取“本地”endpoint,条件为URL包含'/var/run/docker.sock'
-                    id = endpointsData.data.find(({ URL }) => URL.includes('/var/run/docker.sock')).Id;
-                    setEndpointsId(id);
-                }
+    //         //从portainer接口获取endpoints
+    //         const endpointsData = await axios.get('/portainer/api/endpoints', {
+    //             headers: {
+    //                 'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
+    //             }
+    //         });
+    //         if (endpointsData.status === 200) {
+    //             //先判断是否获取了“本地”endpoint
+    //             if (endpointsData.data.length == 0) { //没有“本地”endpoint
+    //                 //调用添加"本地"环境的接口
+    //                 const addEndpoint = await axios.post('/portainer/api/endpoints', {},
+    //                     {
+    //                         params: {
+    //                             Name: "websoft9-local",
+    //                             EndpointCreationType: 1
+    //                         },
+    //                         headers: {
+    //                             'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
+    //                         }
+    //                     }
+    //                 );
+    //                 if (addEndpoint.status === 200) {
+    //                     id = addEndpoint.data?.Id;
+    //                     setEndpointsId(id);
+    //                 }
+    //                 else {
+    //                     console.error('Error:', addEndpoint);
+    //                 }
+    //             }
+    //             else {
+    //                 //应该可能会返回“远程”的endpoint,这里只获取“本地”endpoint,条件为URL包含'/var/run/docker.sock'
+    //                 id = endpointsData.data.find(({ URL }) => URL.includes('/var/run/docker.sock')).Id;
+    //                 setEndpointsId(id);
+    //             }
 
-                //调用接口获取
-                const containersData = await axios.get(`/portainer/api/endpoints/${id}/docker/containers/json`, {
-                    headers: {
-                        'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
-                    },
-                    params: {
-                        all: true,
-                        filters: JSON.stringify({ "label": [`com.docker.compose.project=${customer_name}`] })
-                    }
-                })
-                if (containersData.status === 200) {
-                    setContainersInfo(containersData.data);
-                }
-                else {
-                    console.error('Error:', containersData);
-                }
-            }
-            else {
-                console.error('Error:', endpointsData);
-            }
-        }
-        catch (error) {
-            console.error('Error:', error);
-            //navigate("/error-500");
-        }
-    }
+    //             //调用接口获取
+    //             const containersData = await axios.get(`/portainer/api/endpoints/${id}/docker/containers/json`, {
+    //                 headers: {
+    //                     'Authorization': 'Bearer ' + jwt.replace(/"/g, '')
+    //                 },
+    //                 params: {
+    //                     all: true,
+    //                     filters: JSON.stringify({ "label": [`com.docker.compose.project=${customer_name}`] })
+    //                 }
+    //             })
+    //             if (containersData.status === 200) {
+    //                 setContainersInfo(containersData.data);
+    //             }
+    //             else {
+    //                 console.error('Error:', containersData);
+    //             }
+    //         }
+    //         else {
+    //             console.error('Error:', endpointsData);
+    //         }
+    //     }
+    //     catch (error) {
+    //         console.error('Error:', error);
+    //         //navigate("/error-500");
+    //     }
+    // }
 
-    useEffect(() => {
-        getContainersData();
-    }, []);
+    // useEffect(() => {
+    //     getContainersData();
+    // }, []);
 
     return (
         <Row>

+ 94 - 0
cockpit/myapps/src/pages/appdetailtabs/appterminal.js

@@ -0,0 +1,94 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import Spinner from 'react-bootstrap/Spinner';
+
+// 把handleIframe函数提取成一个自定义hook,并使用useCallback来缓存它
+const useHandleIframe = () => {
+    return useCallback((iframe) => {
+        var iframeWindow = iframe.contentWindow;
+
+        var pageWrapper = iframeWindow.document.getElementById("page-wrapper");
+        if (pageWrapper) {
+            var sideview = pageWrapper.querySelector("#sideview");
+            if (sideview) {
+                pageWrapper.removeChild(sideview);
+            }
+
+            var pageHeaders = pageWrapper.querySelectorAll("page-header");
+            for (var i = 0; i < pageHeaders.length; i++) {
+                var pageHeader = pageHeaders[i];
+                var parent = pageHeader.parentNode;
+                parent.removeChild(pageHeader);
+            }
+
+            var rdWidgetHeaders = pageWrapper.querySelectorAll("rd-widget-header");
+            for (var i = 0; i < rdWidgetHeaders.length; i++) {
+                var rdWidgetHeade = rdWidgetHeaders[i];
+                var parent = rdWidgetHeade.parentNode;
+                parent.removeChild(rdWidgetHeade);
+            }
+
+            pageWrapper.style.setProperty("padding-left", "0px");
+            pageWrapper.removeAttribute("ng-class");
+            pageWrapper.className = "";
+
+            iframe.style.display = "block";
+        }
+    }, []);
+};
+
+const AppTerminal = (props): React$Element<React$FragmentType> => {
+    const iframeRef = useRef(null);
+    const [loading, setLoading] = useState(true);
+    const endpointsId = props.endpointsId;
+    const containerId = props.containerId;
+
+    // 使用自定义hook来获取handleIframe函数
+    const handleIframe = useHandleIframe();
+
+    useEffect(() => {
+        if (iframeRef.current) {
+            // 创建一个MutationObserver来监听iframe中的内容的变化
+            const observer = new MutationObserver(() => {
+                // 当变化发生时,执行handleIframe函数
+                handleIframe(iframeRef.current);
+                setLoading(false);
+            });
+            // 设置观察选项,观察子节点和属性的变化
+            const config = { childList: true, attributes: true };
+            // 判断iframe的contentDocument是否不为null
+            if (iframeRef.current.contentDocument) {
+                // 开始观察iframe中的文档根节点
+                observer.observe(iframeRef.current.contentDocument.documentElement, config);
+            }
+            return () => {
+                // 停止观察
+                observer.disconnect();
+            };
+        }
+    }, [iframeRef.current, handleIframe]);
+
+    return (
+        <>
+            {
+                loading && (
+                    <div className="d-flex align-items-center justify-content-center m-5">
+                        <Spinner animation="border" variant="secondary" />
+                    </div>
+                )
+            }
+            <div class="myProtainerTerminal" key="myProtainerTerminal" >
+                <iframe
+                    id="myIframe"
+                    title="myProtainerTerminal"
+                    src={`/portainer/#!/${endpointsId}/docker/containers/${containerId}/exec`}
+                    style={{ display: "none" }}
+                    ref={iframeRef}
+                    sandbox='allow-scripts allow-modal allow-same-origin'
+                    loading='eager'
+                />
+            </div >
+        </>
+    );
+};
+
+export default AppTerminal;

+ 7 - 5
cockpit/myapps/src/pages/myapps.js

@@ -228,8 +228,10 @@ const MyApps = (): React$Element<React$FragmentType> => {
 
     //用于用户点击应用详情
     const handleClick = (app) => {
-        setSelectedApp(app);
-        setShowModal(true);
+        if (app.status === "running" || app.status === "exited") {
+            setSelectedApp(app);
+            setShowModal(true);
+        }
     };
 
     //用于应用为failed时删除应用
@@ -308,7 +310,7 @@ const MyApps = (): React$Element<React$FragmentType> => {
             else {
                 setShowAlert(true);
                 setAlertType("success")
-                setAlertMessage("执行成功");
+                setAlertMessage(_("Success"));
                 handleDataChange();
             }
         }
@@ -347,7 +349,7 @@ const MyApps = (): React$Element<React$FragmentType> => {
                     <FormInput
                         type="text"
                         name="search"
-                        placeholder={_("Search for apps like WordPress, Dropbox, Slack, Trello, …")}
+                        placeholder={_("Search for apps like WordPress, MySQL, GitLab, …")}
                         onChange={(e) => handleInputChange(e.target.value)}
                     />
                 </Col>
@@ -377,7 +379,7 @@ const MyApps = (): React$Element<React$FragmentType> => {
                             <h4 style={official_app ? {} : { paddingTop: "10px" }}>{official_app ? _("Websoft9's Apps") : _("Other Apps")}</h4>
                             {filteredApps.map((app, i) => (
                                 <Col xxl={2} md={3} key={app.app_id + i} className="appstore-item">
-                                    <div className='appstore-item-content highlight text-align-center'>
+                                    <div className='appstore-item-content highlight text-align-center' onClick={() => { handleClick(app) }}>
                                         {
                                             (!official_app && (app.status === "running" || app.status === "exited")) &&
                                             <Dropdown className="float-end">

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.