feat: 资源操作统一管理&容器操作
@@ -13,7 +13,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@logicflow/core": "^2.1.1",
|
"@logicflow/core": "^2.1.1",
|
||||||
"@logicflow/extension": "^2.1.2",
|
"@logicflow/extension": "^2.1.2",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.8.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.10.7",
|
"element-plus": "^2.11.1",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.12",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.1",
|
"prettier": "^3.6.1",
|
||||||
"sass": "^1.90.0",
|
"sass": "^1.90.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.12",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vite-plugin-progress": "0.0.7",
|
"vite-plugin-progress": "0.0.7",
|
||||||
|
|||||||
1
frontend/src/assets/icon/db/db.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756305127175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22356" width="48" height="48"><path d="M959.718832 123.963683C872.444401 50.185297 704.593576 0.299912 511.850044 0.299912S151.255687 50.185297 63.981255 123.963683C23.193205 158.453578 0 198.04198 0 240.22962v543.840672c0 132.461193 229.132871 239.929708 511.850044 239.929708s511.850044-107.468515 511.850044-239.929708v-543.840672c0-42.18764-23.193205-81.776042-63.981256-116.265937zM87.774285 189.64444c19.794201-21.893586 50.685151-43.087377 89.373816-61.182075 42.287611-19.794201 92.073025-35.489603 147.956653-46.586352C384.087474 70.17944 446.869081 64.281168 511.850044 64.281168s127.76257 5.898272 186.745289 17.594845c55.883628 11.096749 105.669042 26.792151 147.956654 46.586352 38.688665 18.094699 69.579615 39.28849 89.373816 61.182075 15.795372 17.494875 23.793029 34.489896 23.793029 50.48521 0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 273.319926 63.981255 256.324905 63.981255 240.22962s7.997657-33.090306 23.79303-50.58518zM63.981255 356.495558c87.274431 73.778385 255.125256 123.66377 447.868789 123.66377s360.594357-49.885385 447.868788-123.66377v155.254515c0 16.095285-7.997657 33.090306-23.793029 50.48521-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.794201-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 544.740408 63.981255 527.745387 63.981255 511.750073V356.495558z m895.737577 427.574734c0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182076-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182076C71.978912 817.160597 63.981255 800.165576 63.981255 784.070292V627.91604c87.274431 73.778385 255.125256 123.66377 447.868789 123.663771s360.594357-49.885385 447.868788-123.663771v156.154252z" p-id="22357"></path><path d="M167.950796 519.847701m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22358"></path><path d="M167.950796 791.768037m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22359"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
1
frontend/src/assets/icon/db/table.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756305474315" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24277" width="48" height="48"><path d="M960 0H0v1024h1024V0.146286h-64V0z m-640 960.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m0-256h-256V256.146286H640v192h-256V256.146286h-64v192h-256V256.146286h896v191.853714z" p-id="24278"></path></svg>
|
||||||
|
After Width: | Height: | Size: 547 B |
1
frontend/src/assets/icon/docker/docker.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756107672203" class="icon" viewBox="0 0 1472 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5144" width="48" height="48"><path d="M1449.66628 358.737a233.848 233.848 0 0 0-166.348-35.445 268.717 268.717 0 0 0-108.127-152.273l-31.158-20.026-22.265 30.455a258.736 258.736 0 0 0-22.01 265.39 177.353 177.353 0 0 1-74.28 21.241h-24.953V309.536H830.08228V0H624.44928v154.768H287.27328v154.704H118.68528V468.08H8.44728L3.26528 504.42a493.032 493.032 0 0 0 95.97 353.3c90.149 110.11 234.232 165.964 428.284 165.964a749.848 749.848 0 0 0 585.42-255.025 804.871 804.871 0 0 0 139.86-226.874c187.718-3.391 213.246-134.359 214.27-139.99l4.863-27.447-22.01-15.61z m-766.291-49.84v-92.068h87.717v92.068h-87.717z m-337.176 154.64v-92.068h87.59v92.068h-87.59z m168.588 0v-92.068h87.589v92.068h-87.589z m168.588 0v-92.068h87.717v92.068h-87.717z m170.38-92.068h87.524v92.068h-87.525v-92.068zM683.37428 62.125h87.717v92.003h-87.717V62.125zM514.78728 216.829h87.589v92.068h-87.525v-92.068z m-168.588 0h87.59v92.068h-87.59v-92.068zM177.61228 371.47h87.525v92.068H177.61228v-92.068zM527.19928 938.4a609.348 609.348 0 0 1-235-40.564 399.493 399.493 0 0 0 151.058-66.092 44.018 44.018 0 0 0 7.87-57.582 39.54 39.54 0 0 0-54.575-11.9 375.18 375.18 0 0 1-215.998 62.508 262.639 262.639 0 0 1-19.194-21.433 392.455 392.455 0 0 1-79.591-249.523h943.9a250.035 250.035 0 0 0 155.216-62.06l4.99-4.671a682.157 682.157 0 0 1-658.42 451.636z m699.432-482.412l-25.144-1.215-15.163-21.178a186.566 186.566 0 0 1-21.626-161.358 145.619 145.619 0 0 1 42.483 100.769l-1.663 60.525 54.83-18.682a205.505 205.505 0 0 1 111.07-1.664 170.123 170.123 0 0 1-144.787 42.803zM544.41028 629.31a69.738 69.738 0 1 1-66.412 69.674 68.139 68.139 0 0 1 66.412-69.674z m0 85.413a15.74 15.74 0 1 0-14.971-15.675 15.291 15.291 0 0 0 14.97 15.675z m0 0" p-id="5145"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
1
frontend/src/assets/icon/machine/machine.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756286353957" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19008" width="48" height="48"><path d="M853.333333 554.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-170.666666a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 682.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 896h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 853.333333v-170.666666a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 725.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM853.333333 42.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128V170.666667a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 170.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 384h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 341.333333V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 213.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" p-id="19009"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M475.19999999 84.5568c202.7008 0 362.6496 71.0912 373.50400001 163.6608l0.40959999 4.5568h0.5632v232.2432H795.19999999V364.288c-63.1552 48.3328-175.5648 80.5888-307.5584 82.5088l-12.4416 0.0768c-133.1968 0-247.7312-30.8224-313.93279999-78.08L155.2 364.288v136.7552c0 63.5136 128.6144 126.208 319.99999999 126.208 63.1808 0 119.5264-6.8352 166.656-18.2784-4.9408 23.552-6.4 43.5968-4.4032 60.2112-48.7936 10.6752-103.7056 16.6144-162.2528 16.6144-133.1968 0-247.7312-30.7968-313.93279999-78.08l-6.0672-4.5056v125.824c0 63.5136 128.6144 126.2336 319.99999999 126.2336 74.3168 0 139.1616-9.4464 190.6688-24.7296l15.18080001 55.5008a631.04 631.04 0 0 1-89.6256 19.584 803.8656 803.8656 0 0 1-116.22400001 8.192c-206.7456 0-369.3312-73.984-374.3488-169.1392l-0.128-4.5568V252.7744h0.56320001C107.32799999 158.0032 269.1712 84.5824 475.19999999 84.5824z m335.18080001 637.696c12.3648 0 22.4 10.0608 22.39999999 22.4256l-0.0768 74.112a22.3744 22.3744 0 0 1 8.96-9.3184c15.4112-8.704 27.0336-24.6528 33.408-46.592a22.4 22.4 0 1 1 43.008 12.4928c-9.6 33.024-28.416 58.4704-54.39999999 73.1136a22.4 22.4 0 0 1-30.92480001-9.216v40.7296a22.4 22.4 0 0 1-44.79999999 0V744.704c0-12.3648 10.0608-22.4 22.4256-22.4z m-15.6672-184.7808a22.784 22.784 0 0 1 31.51359999 0.256c9.8816 9.8816 24.6528 26.624 40.06400001 47.36 25.3184 34.048 44.2624 68.4544 53.24799999 101.9136a22.4256 22.4256 0 0 1-43.3664 11.3408c-9.8816-36.6848-35.584-76.3392-65.8432-111.488-39.7824 46.1824-69.76 97.152-69.75999999 138.5984 0 36.992 13.056 67.4048 33.89439999 81.5616l5.632 5.3248a22.4 22.4 0 0 1-30.77119999 31.7696c-33.8432-22.9376-53.5552-67.3792-53.55520001-118.656 0-39.1424 18.1248-81.8944 48.2816-125.8752a461.312 461.312 0 0 1 50.688-62.1056zM475.19999999 143.0016c-187.7504 0.0512-314.7776 60.416-319.53919999 122.7264 4.8128 62.2336 131.7888 122.5984 319.53919999 122.5984s314.7776-60.3648 319.5392-122.6496C789.92639999 203.4176 662.95039999 143.0016 475.19999999 143.0016z" ></path></svg>
|
<svg t="1756389060526" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29147" width="48" height="48"><path d="M465.454545 9.402182c245.697939 0 439.575273 86.171152 452.732122 198.376727l0.496485 5.523394h0.682666v281.506909H853.333333V348.470303c-76.551758 58.585212-212.805818 97.683394-372.79806 100.010667l-15.080728 0.093091c-161.450667 0-300.280242-37.360485-380.524606-94.642425L77.575758 348.470303v165.763879c0 76.986182 155.896242 152.979394 387.878787 152.979394 76.582788 0 144.880485-8.285091 202.007273-22.155637-5.988848 28.547879-7.757576 52.844606-5.337212 72.983273-59.143758 12.939636-125.703758 20.138667-196.670061 20.138667-161.450667 0-300.280242-37.329455-380.524606-94.642424l-7.354181-5.461334v152.51394c0 76.986182 155.896242 153.010424 387.878787 153.010424 90.08097 0 168.680727-11.450182 231.113697-29.975273l18.40097 67.273697a764.89697 764.89697 0 0 1-108.637091 23.738182 974.382545 974.382545 0 0 1-140.877576 9.929697c-250.600727 0-447.674182-89.677576-453.756121-205.017212l-0.155151-5.523394V213.302303h0.682666C19.549091 98.428121 215.722667 9.433212 465.454545 9.433212z m406.279758 772.964848c14.987636 0 27.151515 12.194909 27.151515 27.182546l-0.093091 89.832727a27.120485 27.120485 0 0 1 10.860606-11.29503c18.680242-10.550303 32.768-29.882182 40.494546-56.475152a27.151515 27.151515 0 1 1 52.130909 15.142788c-11.636364 40.029091-34.443636 70.873212-65.939394 88.622546a27.151515 27.151515 0 0 1-37.484606-11.17091v49.369213a27.151515 27.151515 0 0 1-54.30303 0V809.580606c0-14.987636 12.194909-27.151515 27.182545-27.151515z m-18.990545-223.976727a27.61697 27.61697 0 0 1 38.198303 0.310303c11.977697 11.977697 29.882182 32.271515 48.562424 57.406061 30.68897 41.270303 53.651394 82.97503 64.54303 123.531636a27.182545 27.182545 0 0 1-52.565333 13.746424c-11.977697-44.466424-43.132121-92.532364-79.80994-135.136969-48.221091 55.978667-84.557576 117.76-84.557575 167.99806 0 44.838788 15.825455 81.702788 41.084121 98.862546l6.826667 6.454303a27.151515 27.151515 0 0 1-37.298425 38.508606c-41.022061-27.803152-64.915394-81.671758-64.915394-143.825455 0-47.445333 21.969455-99.265939 58.523152-152.576a559.166061 559.166061 0 0 1 61.44-75.279515zM465.454545 80.244364C237.878303 80.306424 83.905939 153.475879 78.134303 229.003636 83.968 304.407273 237.878303 377.607758 465.454545 377.607758S847.003152 304.407273 852.774788 228.941576C846.941091 153.475879 693.030788 80.244364 465.454545 80.244364z" p-id="29148"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M897.8125003 599.75c-0.37500029 8.58750029-11.73750029 18.18749971-35.06250058 30.375-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.95000029-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.3125-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.7375-23.81249971-11.40000029-35.96249971-20.99999971-36.37499942-30.07500029v90.97499971c0 9.07499971 12.52500029 18.71250029 36.37499942 30.11250058 47.7 22.79999971 312.48749971 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.35000029 52.94999971-27.60000029 301.76250029-129.89999971 349.79999942-154.95000029 24.4125-12.7125 35.25000029-22.6125 35.25000029-31.57499971v-89.70000029l-0.18749971-0.07499971z" fill="" ></path><path d="M897.77500001 451.43749971c-0.37500029 8.58750029-11.73750029 18.15000029-35.02500029 30.33750058-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.94999942-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.35000029-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.77500029-23.81249971-11.3625-35.96249971-20.99999971-36.37499942-30.0375v90.97500058c0 9.07499971 12.52500029 18.675 36.37499942 30.07499942 47.7 22.79999971 312.45000029 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.3125 52.94999971-27.60000029 301.76250029-129.9375 349.79999942-154.94999942 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125v-89.70000029l-0.225-0.03750029z" fill="" ></path><path d="M897.77500001 297.61250029c0.45-9.15000029-11.51250029-17.17499971-35.58750029-26.02500029-46.8-17.13750029-294.11250029-115.57500029-341.47499942-132.93749971-47.3625-17.325-66.63750029-16.61249971-122.25000058 3.375C342.7375003 161.93750029 79.41249972 265.24999971 32.5750003 283.55000029c-23.43750029 9.225-34.875 17.73749971-34.50000058 26.81249942V401.37499971c0 9.07499971 12.52500029 18.675 36.37500029 30.07500029 47.7 22.79999971 312.45000029 129.78749971 354.30000029 149.77500029 41.85 19.98749971 71.25000029 20.25 124.23749942-7.35000029 52.94999971-27.60000029 301.76250029-129.9375 349.80000029-154.95000029 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125V297.61250029h-0.30000058zM320.31250001 383.75l208.53749971-32.02499971-63 92.3625-145.49999942-60.33750029z m461.25-83.17500029l-123.33750029 48.75000029-13.3875 5.24999971-123.26249971-48.74999942 136.575-54 123.37499971 48.74999942z m-362.09999971-89.36249942l-20.17500029-37.20000058 62.92500029 24.60000058 59.32499942-19.42500058-16.04999971 38.43750058 60.45000029 22.64999942-77.9625 8.1-17.47500029 42.00000029-28.19999971-46.83750029-90-8.1 67.1625-24.22499942z m-155.3625 52.49999971c61.57500029 0 111.44999971 19.31249971 111.44999971 43.16249971s-49.87500029 43.2-111.44999971 43.2-111.4875-19.38750029-111.4875-43.2c0-23.85 49.91249971-43.2 111.4875-43.2z" fill="" ></path></svg>
|
<svg t="1756388835244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25729" width="48" height="48"><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="25730"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -16,12 +16,12 @@ export const LinkTypeEnum = {
|
|||||||
|
|
||||||
// 资源类型
|
// 资源类型
|
||||||
export const ResourceTypeEnum = {
|
export const ResourceTypeEnum = {
|
||||||
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
Machine: EnumValue.of(1, 'tag.machine').setExtra({ icon: 'icon machine/machine', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
||||||
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
Db: EnumValue.of(2, 'tag.db').setExtra({ icon: 'icon db/db', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||||
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
||||||
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||||
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||||
Es: EnumValue.of(6, 'ES实例').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标签关联的资源类型
|
// 标签关联的资源类型
|
||||||
@@ -36,7 +36,7 @@ export const TagResourceTypeEnum = {
|
|||||||
Mongo: ResourceTypeEnum.Mongo,
|
Mongo: ResourceTypeEnum.Mongo,
|
||||||
AuthCert: ResourceTypeEnum.AuthCert,
|
AuthCert: ResourceTypeEnum.AuthCert,
|
||||||
|
|
||||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标签关联的资源类型路径
|
// 标签关联的资源类型路径
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useUserInfo } from '@/store/userInfo';
|
|||||||
* @param code 权限code
|
* @param code 权限code
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function hasPerm(code: string) {
|
export function hasPerm(code: string): boolean {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export function hasPerm(code: string) {
|
|||||||
* @returns {"xxx:save": true} key->permission code
|
* @returns {"xxx:save": true} key->permission code
|
||||||
* @param permCodes
|
* @param permCodes
|
||||||
*/
|
*/
|
||||||
export function hasPerms(permCodes: any[]) {
|
export function hasPerms(permCodes: any[]): Record<string, boolean> {
|
||||||
const res = {} as { [key: string]: boolean };
|
const res = {} as { [key: string]: boolean };
|
||||||
for (let permCode of permCodes) {
|
for (let permCode of permCodes) {
|
||||||
if (hasPerm(permCode)) {
|
if (hasPerm(permCode)) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition @enter="onEnter" name="el-zoom-in-center">
|
<transition @enter="onEnter" name="el-zoom-in-center">
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
:aria-hidden="state.isShow ? 'false' : 'true'"
|
||||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
data-popper-placement="bottom"
|
data-popper-placement="bottom"
|
||||||
@@ -126,7 +126,7 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
|||||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerContextmenuClick = (event: any, data: any) => {
|
const headerContextmenuClick = (event: any) => {
|
||||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
67
frontend/src/components/monaco/RealLogViewer.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<monaco-editor
|
||||||
|
ref="editorRef"
|
||||||
|
:height="props.height"
|
||||||
|
class="editor"
|
||||||
|
language="text"
|
||||||
|
v-model="modelValue"
|
||||||
|
:options="{
|
||||||
|
readOnly: true,
|
||||||
|
}"
|
||||||
|
:can-change-mode="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, useTemplateRef, watch } from 'vue';
|
||||||
|
import { useWebSocket } from '@vueuse/core';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: 'calc(100vh - 200px)',
|
||||||
|
},
|
||||||
|
wsUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const websocketUrl = ref(props.wsUrl);
|
||||||
|
|
||||||
|
const { data } = useWebSocket(websocketUrl);
|
||||||
|
|
||||||
|
const editorRef: any = useTemplateRef('editorRef');
|
||||||
|
|
||||||
|
const modelValue = defineModel<string>('modelValue', {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(data, (value) => {
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
||||||
|
setTimeout(() => {
|
||||||
|
editorRef.value?.revealLastLine();
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reload = (wsUrl: string) => {
|
||||||
|
modelValue.value = '';
|
||||||
|
editorRef.value?.revealLastLine();
|
||||||
|
websocketUrl.value = wsUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reload,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.editor {
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -146,7 +146,7 @@ const initSocket = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
socket = await createWebSocket(`${props.socketUrl}?rows=${term?.rows}&cols=${term?.cols}`);
|
socket = await createWebSocket(`${props.socketUrl}${props.socketUrl.includes('?') ? '&' : '?'}rows=${term?.rows}&cols=${term?.cols}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
|
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
|
||||||
state.status = TerminalStatus.Error;
|
state.status = TerminalStatus.Error;
|
||||||
|
|||||||
20
frontend/src/hooks/useDataState.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export function useDataState<KeyType, ValueType extends number | boolean | string>() {
|
||||||
|
const dataState = ref(new Map<KeyType, ValueType>());
|
||||||
|
|
||||||
|
const setState = (key: KeyType, value: ValueType) => {
|
||||||
|
dataState.value.set(key, value as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getState = (key: KeyType): ValueType => {
|
||||||
|
const result = dataState.value.get(key);
|
||||||
|
return result as ValueType;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataState,
|
||||||
|
setState,
|
||||||
|
getState,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -191,145 +191,6 @@ export default {
|
|||||||
btnTwo: 'Update now',
|
btnTwo: 'Update now',
|
||||||
btnTwoLoading: 'Updating',
|
btnTwoLoading: 'Updating',
|
||||||
},
|
},
|
||||||
menu: {
|
|
||||||
index: 'Home Page',
|
|
||||||
personalCenter: 'Personal Center',
|
|
||||||
|
|
||||||
tag: 'Tag',
|
|
||||||
tagTree: 'Tag Tree',
|
|
||||||
tagSave: 'Save Tag',
|
|
||||||
tagDelete: 'Delete Tag',
|
|
||||||
authorization: 'Authorization',
|
|
||||||
authorizationBase: 'Base Permission',
|
|
||||||
authorizationSave: 'Save Authorization',
|
|
||||||
authorizationDelete: 'Delete Authorization',
|
|
||||||
team: 'Team',
|
|
||||||
teamSave: 'Save Team',
|
|
||||||
teamDelete: 'Delete Team',
|
|
||||||
teamMemberAdd: 'Add Member',
|
|
||||||
teamMemberDelete: 'Delete Member',
|
|
||||||
teamTagSave: 'Save Team Tag',
|
|
||||||
|
|
||||||
machine: 'Machine',
|
|
||||||
machineOp: 'Machine Operation',
|
|
||||||
machineOpBase: 'Base Permission',
|
|
||||||
machineList: 'Machine List',
|
|
||||||
machineBase: 'Base Permission',
|
|
||||||
machineCreate: 'Create Machine',
|
|
||||||
machineEdit: 'Edit Machine',
|
|
||||||
machineDelete: 'Delete Machine',
|
|
||||||
machineTerminal: 'Machine Terminal',
|
|
||||||
machineFileConf: 'File',
|
|
||||||
machineFileConfCreate: 'File-Add Config',
|
|
||||||
machineFileConfDelete: 'File-Delete Config',
|
|
||||||
machineFileCreate: 'File-Create',
|
|
||||||
machineFileDelete: 'File-Delete',
|
|
||||||
machineFileWrite: 'File-Write',
|
|
||||||
machineFileUpload: 'File-Upload',
|
|
||||||
machineScript: 'Script',
|
|
||||||
machineScriptSave: 'Script-Save',
|
|
||||||
machineScriptDelete: 'Script-Delete',
|
|
||||||
machineScriptRun: 'Script-Run',
|
|
||||||
machineKillprocess: 'Kill Process',
|
|
||||||
machineCronJob: 'Cron Job',
|
|
||||||
machineCronJobSvae: 'Cron Job-Save',
|
|
||||||
machineCronJobDelete: 'Cron Job-Delete',
|
|
||||||
machineSecurityConfig: 'Security Config',
|
|
||||||
machineSecurityCmdSvae: 'Cmd Config-Save',
|
|
||||||
machineSecurityCmdDelete: 'Cmd Config-Delete',
|
|
||||||
|
|
||||||
dbms: 'DBMS',
|
|
||||||
dbDataOp: 'Data Operation',
|
|
||||||
dbDataOpBase: 'Base Permission',
|
|
||||||
dbDataOpSqlScriptRun: 'SQL Script Run',
|
|
||||||
dbInstance: 'DB Instance',
|
|
||||||
dbInstanceBase: 'Base Permission',
|
|
||||||
dbInstanceSave: 'Save Instance',
|
|
||||||
dbInstanceDelete: 'Delete Instance',
|
|
||||||
dbBase: 'Db Base Permission',
|
|
||||||
dbSave: 'Save Db',
|
|
||||||
dbDelete: 'Delete Db',
|
|
||||||
dbDataSync: 'Data Sync',
|
|
||||||
dbDataSyncBase: 'Base Permission',
|
|
||||||
dbDataSyncSave: 'Save Sync Task',
|
|
||||||
dbDataSyncDelete: 'Delete Sync Task',
|
|
||||||
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
|
|
||||||
dbDataSyncLog: 'Sync Log',
|
|
||||||
dbTransfer: 'DB Transfer',
|
|
||||||
dbTransferBase: 'Base Permission',
|
|
||||||
dbTransferSave: 'Save Transfer Task',
|
|
||||||
dbTransferDelete: 'Delete Transfer Task',
|
|
||||||
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
|
|
||||||
dbTransferRun: 'Run Transfer Task',
|
|
||||||
dbTransferRunLog: 'Transfer Log',
|
|
||||||
dbTransferFileShow: 'ransfer File-Show',
|
|
||||||
dbTransferFileDelete: 'Transfer File-Delete',
|
|
||||||
dbTransferFileDownload: 'Transfer File-Download',
|
|
||||||
dbTransferFileRun: 'Transfer File-Run',
|
|
||||||
|
|
||||||
redis: 'Redis',
|
|
||||||
redisDataOp: 'Data Operation',
|
|
||||||
redisDataOpBase: 'Base Permission',
|
|
||||||
redisDataOpSave: 'Save Data',
|
|
||||||
redisDataOpDelete: 'Delete Data',
|
|
||||||
redisManage: 'Redis Manage',
|
|
||||||
redisManageBase: 'Base Permission',
|
|
||||||
|
|
||||||
mongo: 'Mongo',
|
|
||||||
mongoDataOp: 'Data Operation',
|
|
||||||
mongoDataOpBase: 'Base Permission',
|
|
||||||
mongoDataOpSave: 'Save Data',
|
|
||||||
mongoDataOpDelete: 'Delete Data',
|
|
||||||
mongoManage: 'Mongo Manage',
|
|
||||||
mongoManageBase: 'Base Permission',
|
|
||||||
|
|
||||||
flow: 'Flow',
|
|
||||||
myTask: 'My Task',
|
|
||||||
myFlow: 'My Flow',
|
|
||||||
flowProcDef: 'Process Define',
|
|
||||||
flowProcDefSave: 'Save Process Define',
|
|
||||||
flowProcDefDelete: 'Delete Process Define',
|
|
||||||
|
|
||||||
msgManage: 'Message',
|
|
||||||
channel: 'Message Channel',
|
|
||||||
msgChannelBase: 'Base Permission',
|
|
||||||
saveMsgChannel: 'Save Message Channel',
|
|
||||||
delMsgChannel: 'Delete Message Channel',
|
|
||||||
msgTmpl: 'Message Template',
|
|
||||||
msgTmplBase: 'Base Permission',
|
|
||||||
saveMsgTmpl: 'Save Message Template',
|
|
||||||
delMsgTmpl: 'Delete Message Template',
|
|
||||||
sendMsg: 'Send Message',
|
|
||||||
|
|
||||||
system: 'System',
|
|
||||||
menuPermission: 'Menu & Permission',
|
|
||||||
menuPermissionBase: 'Base Permission',
|
|
||||||
menuPermissionAdd: 'Add Menu Permission',
|
|
||||||
menuPermissionEdit: 'Edit Menu Permission',
|
|
||||||
menuPermissionDelete: 'Delete Menu Permission',
|
|
||||||
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
|
|
||||||
account: 'Account',
|
|
||||||
accountBase: 'Base Permission',
|
|
||||||
accountAdd: 'Add Account',
|
|
||||||
accountEdit: 'Edit Account',
|
|
||||||
accountDelete: 'Delete Account',
|
|
||||||
accountEnableDisable: 'Enable/Disable Account',
|
|
||||||
accountRoleAllocation: 'Role Allocation',
|
|
||||||
role: 'Role',
|
|
||||||
roleBase: 'Base Permission',
|
|
||||||
roleAdd: 'Add Role',
|
|
||||||
roleEdit: 'Edit Role',
|
|
||||||
roleDelete: 'Delete Role',
|
|
||||||
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
|
|
||||||
sysConf: 'System Config',
|
|
||||||
sysConfBase: 'Base Permission',
|
|
||||||
sysConfSave: 'Save System Config',
|
|
||||||
opLog: 'Operation Log',
|
|
||||||
opLogBase: 'Base Permission',
|
|
||||||
|
|
||||||
noPagePermission: 'No Page Permission',
|
|
||||||
authcertShowciphertext: 'Show Ciphertext',
|
|
||||||
},
|
|
||||||
home: {
|
home: {
|
||||||
personalInfo: 'Personal Information',
|
personalInfo: 'Personal Information',
|
||||||
welcomeMsg: `Hello, {name}, no matter how bad life gets, it doesn't prevent me from getting better!`,
|
welcomeMsg: `Hello, {name}, no matter how bad life gets, it doesn't prevent me from getting better!`,
|
||||||
|
|||||||
79
frontend/src/i18n/en/docker.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export default {
|
||||||
|
docker: {
|
||||||
|
container: 'Container',
|
||||||
|
containerName: 'Container Name',
|
||||||
|
running: 'Running',
|
||||||
|
stopped: 'Stopped',
|
||||||
|
name: 'Container Name',
|
||||||
|
ip: 'IP Address',
|
||||||
|
status: 'Status',
|
||||||
|
stats: 'Stats',
|
||||||
|
memory: 'Memory',
|
||||||
|
stop: 'Stop',
|
||||||
|
stopContainerConfirm: 'Are you sure to stop container [{name}] ?',
|
||||||
|
removeContainerConfirm: 'Are you sure to remove container [{name}] ?',
|
||||||
|
restart: 'Restart',
|
||||||
|
createContainer: 'Create Container',
|
||||||
|
mount: 'Mount',
|
||||||
|
hostDir: 'Host Directory',
|
||||||
|
containerDir: 'Container Directory',
|
||||||
|
permission: 'Permission',
|
||||||
|
rw: 'RW',
|
||||||
|
ro: 'RO',
|
||||||
|
port: 'Port',
|
||||||
|
|
||||||
|
image: 'Image',
|
||||||
|
tag: 'Tag',
|
||||||
|
size: 'Size',
|
||||||
|
used: 'Used',
|
||||||
|
unUsed: 'UnUsed',
|
||||||
|
imageName: 'Image Name',
|
||||||
|
log: 'Log',
|
||||||
|
lines: 'Lines',
|
||||||
|
follow: 'Follow',
|
||||||
|
stopImageConfirm: 'Are you sure to stop image [{name}] ?',
|
||||||
|
export: 'Export',
|
||||||
|
imageUploading: 'Image uploading, please wait...',
|
||||||
|
imageTips: 'Support manual input and select',
|
||||||
|
forcePull: 'Force Pull Image',
|
||||||
|
hostPortPlaceholder: '80',
|
||||||
|
forcePullTips: 'Ignore the server existing image, pull again',
|
||||||
|
server: 'Server',
|
||||||
|
protocol: 'Protocol',
|
||||||
|
networkMode: 'Network Mode',
|
||||||
|
consoleTerminal: 'Console Terminal',
|
||||||
|
otherOption: 'Other Option',
|
||||||
|
tty: 'tty',
|
||||||
|
openStdin: 'stdin (-i)',
|
||||||
|
privileged: 'Privileged',
|
||||||
|
restartPolicy: 'Restart Policy',
|
||||||
|
noRestart: 'No Restart',
|
||||||
|
alwaysRestart: 'Always Restart',
|
||||||
|
onFailure: 'On Failure',
|
||||||
|
unlessStopped: 'Unless Stopped',
|
||||||
|
cpuShare: 'CPU Share',
|
||||||
|
cpuShareTips: 'The default container share is 1024 cpus, and increasing it will give the current container more CPU time',
|
||||||
|
cpuQuota: 'CPU Quota',
|
||||||
|
cpuLimitTips: 'A CPU limit of 0 turns off the limit',
|
||||||
|
cpuCanUseTips: 'The maximum available is {cpuTotal} cores',
|
||||||
|
core: 'Core',
|
||||||
|
memoryLimit: 'Memory Limit',
|
||||||
|
memoryLimitTips: 'A memory limit of 0 turns off the limit',
|
||||||
|
shmSize: 'Shm Size',
|
||||||
|
memoryCanUseTips: 'Maximum available {memTotal}',
|
||||||
|
tagTips: `One in a row, for example:
|
||||||
|
tag1=value1
|
||||||
|
tag2=value2`,
|
||||||
|
envParam: 'Env Param',
|
||||||
|
envParamTips: `One in a row, for example:
|
||||||
|
env1=value1
|
||||||
|
env2=value2`,
|
||||||
|
device: 'Device',
|
||||||
|
driver: 'Driver',
|
||||||
|
driverTips: 'Device drivers to be used by the container, e.g. : nvidia, etc',
|
||||||
|
count: 'Count',
|
||||||
|
capabilitie: 'Capabilitie',
|
||||||
|
deviceId: 'Device ID',
|
||||||
|
capabilitiePlaceholder: 'eg: gpu',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -16,11 +16,11 @@ export default {
|
|||||||
connSuccess: 'be connected successfully',
|
connSuccess: 'be connected successfully',
|
||||||
shouldTestConn: 'please test connection first',
|
shouldTestConn: 'please test connection first',
|
||||||
instance: 'ES Instance',
|
instance: 'ES Instance',
|
||||||
instanceSave: 'Save Instance',
|
instanceSave: 'ES-Save Instance',
|
||||||
instanceDel: 'Delete Instance',
|
instanceDel: 'Es-Delete Instance',
|
||||||
operation: 'Data Operation',
|
operation: 'Es-Data Operation',
|
||||||
dataSave: 'Data Save',
|
dataSave: 'Es-Data Save',
|
||||||
dataDel: 'Data Del',
|
dataDel: 'Es-Data Del',
|
||||||
indexName: 'Index Name',
|
indexName: 'Index Name',
|
||||||
requireIndexName: 'Index Name Is Required',
|
requireIndexName: 'Index Name Is Required',
|
||||||
indexDetail: 'Index Detail',
|
indexDetail: 'Index Detail',
|
||||||
|
|||||||
142
frontend/src/i18n/en/menu.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
export default {
|
||||||
|
menu: {
|
||||||
|
index: 'Home',
|
||||||
|
personalCenter: 'Personal Center',
|
||||||
|
myResource: 'Resource',
|
||||||
|
|
||||||
|
tag: 'Tag',
|
||||||
|
tagTree: 'Tag Tree',
|
||||||
|
tagSave: 'Save Tag',
|
||||||
|
tagDelete: 'Delete Tag',
|
||||||
|
authorization: 'Authorization',
|
||||||
|
authorizationBase: 'Base Permission',
|
||||||
|
authorizationSave: 'Save Authorization',
|
||||||
|
authorizationDelete: 'Delete Authorization',
|
||||||
|
team: 'Team',
|
||||||
|
teamSave: 'Save Team',
|
||||||
|
teamDelete: 'Delete Team',
|
||||||
|
teamMemberAdd: 'Add Member',
|
||||||
|
teamMemberDelete: 'Delete Member',
|
||||||
|
teamTagSave: 'Save Team Tag',
|
||||||
|
|
||||||
|
machine: 'Machine',
|
||||||
|
machineOp: 'Machine Operation',
|
||||||
|
machineOpBase: 'Base Permission',
|
||||||
|
machineList: 'Machine List',
|
||||||
|
machineBase: 'Base Permission',
|
||||||
|
machineCreate: 'Create Machine',
|
||||||
|
machineEdit: 'Edit Machine',
|
||||||
|
machineDelete: 'Delete Machine',
|
||||||
|
machineTerminal: 'Machine Terminal',
|
||||||
|
machineFileConf: 'File',
|
||||||
|
machineFileConfCreate: 'File-Add Config',
|
||||||
|
machineFileConfDelete: 'File-Delete Config',
|
||||||
|
machineFileCreate: 'File-Create',
|
||||||
|
machineFileDelete: 'File-Delete',
|
||||||
|
machineFileWrite: 'File-Write',
|
||||||
|
machineFileUpload: 'File-Upload',
|
||||||
|
machineScript: 'Script',
|
||||||
|
machineScriptSave: 'Script-Save',
|
||||||
|
machineScriptDelete: 'Script-Delete',
|
||||||
|
machineScriptRun: 'Script-Run',
|
||||||
|
machineKillprocess: 'Kill Process',
|
||||||
|
machineCronJob: 'Cron Job',
|
||||||
|
machineCronJobSvae: 'Cron Job-Save',
|
||||||
|
machineCronJobDelete: 'Cron Job-Delete',
|
||||||
|
machineSecurityConfig: 'Security Config',
|
||||||
|
machineSecurityCmdSvae: 'Cmd Config-Save',
|
||||||
|
machineSecurityCmdDelete: 'Cmd Config-Delete',
|
||||||
|
|
||||||
|
dbms: 'DBMS',
|
||||||
|
dbDataOp: 'Data Operation',
|
||||||
|
dbDataOpBase: 'Base Permission',
|
||||||
|
dbDataOpSqlScriptRun: 'SQL Script Run',
|
||||||
|
dbInstance: 'DB Instance',
|
||||||
|
dbInstanceBase: 'Base Permission',
|
||||||
|
dbInstanceSave: 'Save Instance',
|
||||||
|
dbInstanceDelete: 'Delete Instance',
|
||||||
|
dbBase: 'Db Base Permission',
|
||||||
|
dbSave: 'Save Db',
|
||||||
|
dbDelete: 'Delete Db',
|
||||||
|
dbDataSync: 'Data Sync',
|
||||||
|
dbDataSyncBase: 'Base Permission',
|
||||||
|
dbDataSyncSave: 'Save Sync Task',
|
||||||
|
dbDataSyncDelete: 'Delete Sync Task',
|
||||||
|
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
|
||||||
|
dbDataSyncLog: 'Sync Log',
|
||||||
|
dbTransfer: 'DB Transfer',
|
||||||
|
dbTransferBase: 'Base Permission',
|
||||||
|
dbTransferSave: 'Save Transfer Task',
|
||||||
|
dbTransferDelete: 'Delete Transfer Task',
|
||||||
|
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
|
||||||
|
dbTransferRun: 'Run Transfer Task',
|
||||||
|
dbTransferRunLog: 'Transfer Log',
|
||||||
|
dbTransferFileShow: 'ransfer File-Show',
|
||||||
|
dbTransferFileDelete: 'Transfer File-Delete',
|
||||||
|
dbTransferFileDownload: 'Transfer File-Download',
|
||||||
|
dbTransferFileRun: 'Transfer File-Run',
|
||||||
|
|
||||||
|
redis: 'Redis',
|
||||||
|
redisDataOp: 'Data Operation',
|
||||||
|
redisDataOpBase: 'Base Permission',
|
||||||
|
redisDataOpSave: 'Save Data',
|
||||||
|
redisDataOpDelete: 'Delete Data',
|
||||||
|
redisManage: 'Redis Manage',
|
||||||
|
redisManageBase: 'Base Permission',
|
||||||
|
|
||||||
|
mongo: 'Mongo',
|
||||||
|
mongoDataOp: 'Data Operation',
|
||||||
|
mongoDataOpBase: 'Base Permission',
|
||||||
|
mongoDataOpSave: 'Save Data',
|
||||||
|
mongoDataOpDelete: 'Delete Data',
|
||||||
|
mongoManage: 'Mongo Manage',
|
||||||
|
mongoManageBase: 'Base Permission',
|
||||||
|
|
||||||
|
flow: 'Flow',
|
||||||
|
myTask: 'My Task',
|
||||||
|
myFlow: 'My Flow',
|
||||||
|
flowProcDef: 'Process Define',
|
||||||
|
flowProcDefSave: 'Save Process Define',
|
||||||
|
flowProcDefDelete: 'Delete Process Define',
|
||||||
|
|
||||||
|
msgManage: 'Message',
|
||||||
|
channel: 'Message Channel',
|
||||||
|
msgChannelBase: 'Base Permission',
|
||||||
|
saveMsgChannel: 'Save Message Channel',
|
||||||
|
delMsgChannel: 'Delete Message Channel',
|
||||||
|
msgTmpl: 'Message Template',
|
||||||
|
msgTmplBase: 'Base Permission',
|
||||||
|
saveMsgTmpl: 'Save Message Template',
|
||||||
|
delMsgTmpl: 'Delete Message Template',
|
||||||
|
sendMsg: 'Send Message',
|
||||||
|
|
||||||
|
system: 'System',
|
||||||
|
menuPermission: 'Menu & Permission',
|
||||||
|
menuPermissionBase: 'Base Permission',
|
||||||
|
menuPermissionAdd: 'Add Menu Permission',
|
||||||
|
menuPermissionEdit: 'Edit Menu Permission',
|
||||||
|
menuPermissionDelete: 'Delete Menu Permission',
|
||||||
|
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
|
||||||
|
account: 'Account',
|
||||||
|
accountBase: 'Base Permission',
|
||||||
|
accountAdd: 'Add Account',
|
||||||
|
accountEdit: 'Edit Account',
|
||||||
|
accountDelete: 'Delete Account',
|
||||||
|
accountEnableDisable: 'Enable/Disable Account',
|
||||||
|
accountRoleAllocation: 'Role Allocation',
|
||||||
|
role: 'Role',
|
||||||
|
roleBase: 'Base Permission',
|
||||||
|
roleAdd: 'Add Role',
|
||||||
|
roleEdit: 'Edit Role',
|
||||||
|
roleDelete: 'Delete Role',
|
||||||
|
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
|
||||||
|
sysConf: 'System Config',
|
||||||
|
sysConfBase: 'Base Permission',
|
||||||
|
sysConfSave: 'Save System Config',
|
||||||
|
opLog: 'Operation Log',
|
||||||
|
opLogBase: 'Base Permission',
|
||||||
|
|
||||||
|
noPagePermission: 'No Page Permission',
|
||||||
|
authcertShowciphertext: 'Show Ciphertext',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -14,6 +14,12 @@ export default {
|
|||||||
createSubTagTitle: 'Creates a child tag for {codePath}',
|
createSubTagTitle: 'Creates a child tag for {codePath}',
|
||||||
rootTag: 'Root Tag',
|
rootTag: 'Root Tag',
|
||||||
selectTagPlaceholder: 'Select the associated tag',
|
selectTagPlaceholder: 'Select the associated tag',
|
||||||
|
machineOp: 'Machine Operation',
|
||||||
|
dbDataOp: 'Db Operation',
|
||||||
|
redisDataOp: 'Redis Operation',
|
||||||
|
esDataOp: 'Es Operation',
|
||||||
|
mongoDataOp: 'Mongo Operation',
|
||||||
|
allResource: 'All Resource',
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
team: 'Team',
|
team: 'Team',
|
||||||
|
|||||||
@@ -201,145 +201,6 @@ export default {
|
|||||||
btnTwo: '马上更新',
|
btnTwo: '马上更新',
|
||||||
btnTwoLoading: '更新中',
|
btnTwoLoading: '更新中',
|
||||||
},
|
},
|
||||||
menu: {
|
|
||||||
index: '首页',
|
|
||||||
personalCenter: '个人中心',
|
|
||||||
|
|
||||||
tag: '标签管理',
|
|
||||||
tagTree: '标签树',
|
|
||||||
tagSave: '保存标签',
|
|
||||||
tagDelete: '删除标签',
|
|
||||||
authorization: '授权凭证',
|
|
||||||
authorizationBase: '基础权限',
|
|
||||||
authorizationSave: '保存权限',
|
|
||||||
authorizationDelete: '删除权限',
|
|
||||||
team: '团队管理',
|
|
||||||
teamSave: '保存团队',
|
|
||||||
teamDelete: '删除团队',
|
|
||||||
teamMemberAdd: '添加成员',
|
|
||||||
teamMemberDelete: '删除成员',
|
|
||||||
teamTagSave: '保存团队标签',
|
|
||||||
|
|
||||||
machine: '机器管理',
|
|
||||||
machineOp: '机器操作',
|
|
||||||
machineOpBase: '基本权限',
|
|
||||||
machineList: '机器列表',
|
|
||||||
machineBase: '基本权限',
|
|
||||||
machineCreate: '创建机器',
|
|
||||||
machineEdit: '编辑机器',
|
|
||||||
machineDelete: '删除机器',
|
|
||||||
machineTerminal: '机器终端',
|
|
||||||
machineFileConf: '文件管理',
|
|
||||||
machineFileConfCreate: '文件-添加配置',
|
|
||||||
machineFileConfDelete: '文件-删除配置',
|
|
||||||
machineFileCreate: '文件-创建',
|
|
||||||
machineFileDelete: '文件-删除',
|
|
||||||
machineFileWrite: '文件-写入',
|
|
||||||
machineFileUpload: '文件-上传',
|
|
||||||
machineScript: '脚本管理',
|
|
||||||
machineScriptSave: '脚本-保存',
|
|
||||||
machineScriptDelete: '脚本-删除',
|
|
||||||
machineScriptRun: '脚本-执行',
|
|
||||||
machineKillprocess: '终止进程',
|
|
||||||
machineCronJob: '计划任务',
|
|
||||||
machineCronJobSvae: '计划任务-保存',
|
|
||||||
machineCronJobDelete: '计划任务-删除',
|
|
||||||
machineSecurityConfig: '安全配置',
|
|
||||||
machineSecurityCmdSvae: '命令配置-保存',
|
|
||||||
machineSecurityCmdDelete: '命令配置-删除',
|
|
||||||
|
|
||||||
dbms: 'DBMS',
|
|
||||||
dbDataOp: '数据操作',
|
|
||||||
dbDataOpBase: '基本权限',
|
|
||||||
dbDataOpSqlScriptRun: 'SQL脚本执行',
|
|
||||||
dbInstance: '数据库实例',
|
|
||||||
dbInstanceBase: '基本权限',
|
|
||||||
dbInstanceSave: '保存实例',
|
|
||||||
dbInstanceDelete: '删除实例',
|
|
||||||
dbBase: '数据库基本权限',
|
|
||||||
dbSave: '保存数据库',
|
|
||||||
dbDelete: '删除数据库',
|
|
||||||
dbDataSync: '数据同步',
|
|
||||||
dbDataSyncBase: '基本权限',
|
|
||||||
dbDataSyncSave: '保存同步',
|
|
||||||
dbDataSyncDelete: '删除同步',
|
|
||||||
dbDataSyncChangeStatus: '启用停用',
|
|
||||||
dbDataSyncLog: '同步日志',
|
|
||||||
dbTransfer: '数据库迁移',
|
|
||||||
dbTransferBase: '基本权限',
|
|
||||||
dbTransferSave: '保存迁移任务',
|
|
||||||
dbTransferDelete: '删除迁移任务',
|
|
||||||
dbTransferChangeStatus: '启用停用',
|
|
||||||
dbTransferRun: '执行迁移任务',
|
|
||||||
dbTransferRunLog: '迁移日志查看',
|
|
||||||
dbTransferFileShow: '迁移文件-查看',
|
|
||||||
dbTransferFileDelete: '迁移文件-删除',
|
|
||||||
dbTransferFileDownload: '迁移文件-下载',
|
|
||||||
dbTransferFileRun: '迁移文件-执行',
|
|
||||||
|
|
||||||
redis: 'Redis',
|
|
||||||
redisDataOp: '数据操作',
|
|
||||||
redisDataOpBase: '基本权限',
|
|
||||||
redisDataOpSave: '数据保存',
|
|
||||||
redisDataOpDelete: '数据删除',
|
|
||||||
redisManage: 'Redis管理',
|
|
||||||
redisManageBase: '基本权限',
|
|
||||||
|
|
||||||
mongo: 'Mongo',
|
|
||||||
mongoDataOp: '数据操作',
|
|
||||||
mongoDataOpBase: '基本权限',
|
|
||||||
mongoDataOpSave: '数据保存',
|
|
||||||
mongoDataOpDelete: '数据删除',
|
|
||||||
mongoManage: 'Mongo管理',
|
|
||||||
mongoManageBase: '基本权限',
|
|
||||||
|
|
||||||
flow: '工单流程',
|
|
||||||
myTask: '我的任务',
|
|
||||||
myFlow: '我的流程',
|
|
||||||
flowProcDef: '流程定义',
|
|
||||||
flowProcDefSave: '保存流程定义',
|
|
||||||
flowProcDefDelete: '删除流程定义',
|
|
||||||
|
|
||||||
msgManage: '消息管理',
|
|
||||||
channel: '消息渠道',
|
|
||||||
msgChannelBase: '基础权限',
|
|
||||||
saveMsgChannel: '保存消息渠道',
|
|
||||||
delMsgChannel: '删除消息渠道',
|
|
||||||
msgTmpl: '消息模板',
|
|
||||||
msgTmplBase: '基础权限',
|
|
||||||
saveMsgTmpl: '保存消息模板',
|
|
||||||
delMsgTmpl: '删除消息模板',
|
|
||||||
sendMsg: '发送消息',
|
|
||||||
|
|
||||||
system: '系统管理',
|
|
||||||
menuPermission: '菜单权限',
|
|
||||||
menuPermissionBase: '基本权限',
|
|
||||||
menuPermissionAdd: '添加菜单权限',
|
|
||||||
menuPermissionEdit: '编辑菜单权限',
|
|
||||||
menuPermissionDelete: '删除菜单权限',
|
|
||||||
menuPermissionEnableDisable: '启用/禁用菜单权限',
|
|
||||||
account: '账号管理',
|
|
||||||
accountBase: '基本权限',
|
|
||||||
accountAdd: '添加账号',
|
|
||||||
accountEdit: '编辑账号',
|
|
||||||
accountDelete: '删除账号',
|
|
||||||
accountEnableDisable: '启用/禁用账号',
|
|
||||||
accountRoleAllocation: '角色分配',
|
|
||||||
role: '角色管理',
|
|
||||||
roleBase: '基本权限',
|
|
||||||
roleAdd: '添加角色',
|
|
||||||
roleEdit: '编辑角色',
|
|
||||||
roleDelete: '删除角色',
|
|
||||||
roleMenuPermissionAllocation: '菜单权限分配',
|
|
||||||
sysConf: '系统配置',
|
|
||||||
sysConfBase: '基本权限',
|
|
||||||
sysConfSave: '保存配置',
|
|
||||||
opLog: '操作日志',
|
|
||||||
opLogBase: '基本权限',
|
|
||||||
|
|
||||||
noPagePermission: '无页面权限',
|
|
||||||
authcertShowciphertext: '授权凭证密文查看',
|
|
||||||
},
|
|
||||||
home: {
|
home: {
|
||||||
personalInfo: '个人信息',
|
personalInfo: '个人信息',
|
||||||
welcomeMsg: '您好, {name},生活变的再糟糕,也不妨碍我变得更好!',
|
welcomeMsg: '您好, {name},生活变的再糟糕,也不妨碍我变得更好!',
|
||||||
|
|||||||
79
frontend/src/i18n/zh-cn/docker.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export default {
|
||||||
|
docker: {
|
||||||
|
container: '容器',
|
||||||
|
containerName: '容器名',
|
||||||
|
running: '运行中',
|
||||||
|
stopped: '已停止',
|
||||||
|
name: '容器名',
|
||||||
|
ip: 'IP地址',
|
||||||
|
status: '状态',
|
||||||
|
stats: '资源使用率',
|
||||||
|
memory: '内存',
|
||||||
|
stop: '停止',
|
||||||
|
stopContainerConfirm: '确定停止容器 [{name}] ?',
|
||||||
|
removeContainerConfirm: '确定删除容器 [{name}] ?',
|
||||||
|
restart: '重启',
|
||||||
|
createContainer: '创建容器',
|
||||||
|
mount: '挂载',
|
||||||
|
hostDir: '本机目录',
|
||||||
|
containerDir: '容器目录',
|
||||||
|
permission: '权限',
|
||||||
|
rw: '读写',
|
||||||
|
ro: '只读',
|
||||||
|
port: '端口',
|
||||||
|
|
||||||
|
image: '镜像',
|
||||||
|
tag: '标签',
|
||||||
|
size: '大小',
|
||||||
|
used: '已使用',
|
||||||
|
unUsed: '未使用',
|
||||||
|
imageName: '镜像名',
|
||||||
|
log: '日志',
|
||||||
|
lines: '行数',
|
||||||
|
follow: '实时',
|
||||||
|
stopImageConfirm: '确定删除该镜像?',
|
||||||
|
export: '导出',
|
||||||
|
imageUploading: '镜像导入中,请稍后...',
|
||||||
|
imageTips: '支持手动输入并选择',
|
||||||
|
forcePull: '强制拉取镜像',
|
||||||
|
hostPortPlaceholder: '80',
|
||||||
|
forcePullTips: '忽略服务器已存在的镜像,重新拉取一次',
|
||||||
|
server: '服务器',
|
||||||
|
protocol: '协议',
|
||||||
|
networkMode: '网络模式',
|
||||||
|
consoleTerminal: '控制台交互',
|
||||||
|
otherOption: '其他可选项',
|
||||||
|
tty: '伪终端 (-t)',
|
||||||
|
openStdin: '标准输入 (-i)',
|
||||||
|
privileged: '特权模式',
|
||||||
|
restartPolicy: '重启策略',
|
||||||
|
noRestart: '不重启',
|
||||||
|
alwaysRestart: '一直重启',
|
||||||
|
onFailure: '失败后重启',
|
||||||
|
unlessStopped: '未手动停止则重启',
|
||||||
|
cpuShare: 'CPU权重',
|
||||||
|
cpuShareTips: '容器默认份额为 1024 个 CPU,增大可使当前容器获得更多的 CPU 时间',
|
||||||
|
cpuQuota: 'CPU 限制',
|
||||||
|
cpuLimitTips: 'CPU限制为 0 则关闭限制',
|
||||||
|
cpuCanUseTips: '最大可用为{cpuTotal}核',
|
||||||
|
core: '核',
|
||||||
|
memoryLimit: '内存限制',
|
||||||
|
memoryLimitTips: '内存限制为 0 则关闭限制',
|
||||||
|
shmSize: '共享内存',
|
||||||
|
memoryCanUseTips: '最大可用为{memTotal}',
|
||||||
|
tagTips: `一行一个,例如:
|
||||||
|
tag1=value1
|
||||||
|
tag2=value2`,
|
||||||
|
envParam: '环境变量',
|
||||||
|
envParamTips: `一行一个,例如:
|
||||||
|
env1=value1
|
||||||
|
env2=value2`,
|
||||||
|
device: '设备',
|
||||||
|
driver: '驱动',
|
||||||
|
driverTips: '容器需要使用的设备驱动程序,如: nvidia 等',
|
||||||
|
count: '数量',
|
||||||
|
capabilitie: '能力',
|
||||||
|
deviceId: '设备ID',
|
||||||
|
capabilitiePlaceholder: '如: gpu',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -16,11 +16,11 @@ export default {
|
|||||||
connSuccess: '连接成功',
|
connSuccess: '连接成功',
|
||||||
shouldTestConn: '请先测试连接可用性',
|
shouldTestConn: '请先测试连接可用性',
|
||||||
instance: 'ES实例',
|
instance: 'ES实例',
|
||||||
instanceSave: '实例保存',
|
instanceSave: 'Es-实例保存',
|
||||||
instanceDel: '实例删除',
|
instanceDel: 'Es-实例删除',
|
||||||
operation: '数据操作',
|
operation: 'Es-数据操作',
|
||||||
dataSave: '数据保存',
|
dataSave: 'Es-数据保存',
|
||||||
dataDel: '数据删除',
|
dataDel: 'Es-数据删除',
|
||||||
indexName: '索引名',
|
indexName: '索引名',
|
||||||
requireIndexName: '请填写索引名',
|
requireIndexName: '请填写索引名',
|
||||||
indexDetail: '索引详情',
|
indexDetail: '索引详情',
|
||||||
|
|||||||
142
frontend/src/i18n/zh-cn/menu.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
export default {
|
||||||
|
menu: {
|
||||||
|
index: '首页',
|
||||||
|
personalCenter: '个人中心',
|
||||||
|
myResource: '我的资源',
|
||||||
|
|
||||||
|
tag: '标签',
|
||||||
|
tagTree: '标签树',
|
||||||
|
tagSave: '保存标签',
|
||||||
|
tagDelete: '删除标签',
|
||||||
|
authorization: '授权凭证',
|
||||||
|
authorizationBase: '基础权限',
|
||||||
|
authorizationSave: '保存权限',
|
||||||
|
authorizationDelete: '删除权限',
|
||||||
|
team: '团队',
|
||||||
|
teamSave: '保存团队',
|
||||||
|
teamDelete: '删除团队',
|
||||||
|
teamMemberAdd: '添加成员',
|
||||||
|
teamMemberDelete: '删除成员',
|
||||||
|
teamTagSave: '保存团队标签',
|
||||||
|
|
||||||
|
machine: '机器',
|
||||||
|
machineOp: '机器操作',
|
||||||
|
machineOpBase: '机器操作-基本权限',
|
||||||
|
machineList: '机器列表',
|
||||||
|
machineBase: '机器-基本权限',
|
||||||
|
machineCreate: '机器-创建机器',
|
||||||
|
machineEdit: '机器-编辑机器',
|
||||||
|
machineDelete: '机器-删除机器',
|
||||||
|
machineTerminal: '机器-机器终端',
|
||||||
|
machineFileConf: '机器-文件管理',
|
||||||
|
machineFileConfCreate: '机器-文件-添加配置',
|
||||||
|
machineFileConfDelete: '机器-文件-删除配置',
|
||||||
|
machineFileCreate: '机器-文件-创建',
|
||||||
|
machineFileDelete: '机器-文件-删除',
|
||||||
|
machineFileWrite: '机器-文件-写入',
|
||||||
|
machineFileUpload: '机器-文件-上传',
|
||||||
|
machineScript: '机器-脚本管理',
|
||||||
|
machineScriptSave: '机器-脚本-保存',
|
||||||
|
machineScriptDelete: '机器-脚本-删除',
|
||||||
|
machineScriptRun: '机器-脚本-执行',
|
||||||
|
machineKillprocess: '机器-终止进程',
|
||||||
|
machineCronJob: '计划任务',
|
||||||
|
machineCronJobSvae: '机器-计划任务-保存',
|
||||||
|
machineCronJobDelete: '机器-计划任务-删除',
|
||||||
|
machineSecurityConfig: '安全配置',
|
||||||
|
machineSecurityCmdSvae: '机器-命令配置-保存',
|
||||||
|
machineSecurityCmdDelete: '机器-命令配置-删除',
|
||||||
|
|
||||||
|
dbms: 'DBMS',
|
||||||
|
dbDataOp: '数据操作',
|
||||||
|
dbDataOpBase: 'Db-数据操作-基本权限',
|
||||||
|
dbDataOpSqlScriptRun: 'Db-SQL脚本执行',
|
||||||
|
dbInstance: '数据库实例',
|
||||||
|
dbInstanceBase: 'Db-基本权限',
|
||||||
|
dbInstanceSave: 'Db-保存实例',
|
||||||
|
dbInstanceDelete: 'Db-删除实例',
|
||||||
|
dbBase: '数据库基本权限',
|
||||||
|
dbSave: 'Db-保存数据库',
|
||||||
|
dbDelete: 'Db-删除数据库',
|
||||||
|
dbDataSync: '数据同步',
|
||||||
|
dbDataSyncBase: '基本权限',
|
||||||
|
dbDataSyncSave: '保存同步',
|
||||||
|
dbDataSyncDelete: '删除同步',
|
||||||
|
dbDataSyncChangeStatus: '启用停用',
|
||||||
|
dbDataSyncLog: '同步日志',
|
||||||
|
dbTransfer: '数据库迁移',
|
||||||
|
dbTransferBase: '基本权限',
|
||||||
|
dbTransferSave: '保存迁移任务',
|
||||||
|
dbTransferDelete: '删除迁移任务',
|
||||||
|
dbTransferChangeStatus: '启用停用',
|
||||||
|
dbTransferRun: '执行迁移任务',
|
||||||
|
dbTransferRunLog: '迁移日志查看',
|
||||||
|
dbTransferFileShow: '迁移文件-查看',
|
||||||
|
dbTransferFileDelete: '迁移文件-删除',
|
||||||
|
dbTransferFileDownload: '迁移文件-下载',
|
||||||
|
dbTransferFileRun: '迁移文件-执行',
|
||||||
|
|
||||||
|
redis: 'Redis',
|
||||||
|
redisDataOp: 'Redis-数据操作',
|
||||||
|
redisDataOpBase: 'Redis-数据操作-基本权限',
|
||||||
|
redisDataOpSave: 'Redis-数据操作-数据保存',
|
||||||
|
redisDataOpDelete: 'Redis-数据操作-数据删除',
|
||||||
|
redisManage: 'Redis管理',
|
||||||
|
redisManageBase: 'Redis-管理-基本权限',
|
||||||
|
|
||||||
|
mongo: 'Mongo',
|
||||||
|
mongoDataOp: '数据操作',
|
||||||
|
mongoDataOpBase: 'Mongo-数据操作-基本权限',
|
||||||
|
mongoDataOpSave: 'Mongo-数据操作-数据保存',
|
||||||
|
mongoDataOpDelete: 'Mongo-数据操作-数据删除',
|
||||||
|
mongoManage: 'Mongo管理',
|
||||||
|
mongoManageBase: 'Mongo-管理-基本权限',
|
||||||
|
|
||||||
|
flow: '工单流程',
|
||||||
|
myTask: '我的任务',
|
||||||
|
myFlow: '我的流程',
|
||||||
|
flowProcDef: '流程定义',
|
||||||
|
flowProcDefSave: '保存流程定义',
|
||||||
|
flowProcDefDelete: '删除流程定义',
|
||||||
|
|
||||||
|
msgManage: '消息',
|
||||||
|
channel: '消息渠道',
|
||||||
|
msgChannelBase: '基础权限',
|
||||||
|
saveMsgChannel: '保存消息渠道',
|
||||||
|
delMsgChannel: '删除消息渠道',
|
||||||
|
msgTmpl: '消息模板',
|
||||||
|
msgTmplBase: '基础权限',
|
||||||
|
saveMsgTmpl: '保存消息模板',
|
||||||
|
delMsgTmpl: '删除消息模板',
|
||||||
|
sendMsg: '发送消息',
|
||||||
|
|
||||||
|
system: '系统管理',
|
||||||
|
menuPermission: '菜单权限',
|
||||||
|
menuPermissionBase: '基本权限',
|
||||||
|
menuPermissionAdd: '添加菜单权限',
|
||||||
|
menuPermissionEdit: '编辑菜单权限',
|
||||||
|
menuPermissionDelete: '删除菜单权限',
|
||||||
|
menuPermissionEnableDisable: '启用/禁用菜单权限',
|
||||||
|
account: '账号管理',
|
||||||
|
accountBase: '基本权限',
|
||||||
|
accountAdd: '添加账号',
|
||||||
|
accountEdit: '编辑账号',
|
||||||
|
accountDelete: '删除账号',
|
||||||
|
accountEnableDisable: '启用/禁用账号',
|
||||||
|
accountRoleAllocation: '角色分配',
|
||||||
|
role: '角色管理',
|
||||||
|
roleBase: '基本权限',
|
||||||
|
roleAdd: '添加角色',
|
||||||
|
roleEdit: '编辑角色',
|
||||||
|
roleDelete: '删除角色',
|
||||||
|
roleMenuPermissionAllocation: '菜单权限分配',
|
||||||
|
sysConf: '系统配置',
|
||||||
|
sysConfBase: '基本权限',
|
||||||
|
sysConfSave: '保存配置',
|
||||||
|
opLog: '操作日志',
|
||||||
|
opLogBase: '基本权限',
|
||||||
|
|
||||||
|
noPagePermission: '无页面权限',
|
||||||
|
authcertShowciphertext: '授权凭证密文查看',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -15,6 +15,12 @@ export default {
|
|||||||
createSubTagTitle: '创建【{codePath}】的子标签',
|
createSubTagTitle: '创建【{codePath}】的子标签',
|
||||||
rootTag: '根标签',
|
rootTag: '根标签',
|
||||||
selectTagPlaceholder: '请选择关联标签',
|
selectTagPlaceholder: '请选择关联标签',
|
||||||
|
machineOp: '机器操作',
|
||||||
|
dbDataOp: '数据库操作',
|
||||||
|
redisDataOp: 'Redis操作',
|
||||||
|
esDataOp: 'ES操作',
|
||||||
|
mongoDataOp: 'Mongo操作',
|
||||||
|
allResource: '所有资源',
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
team: '团队',
|
team: '团队',
|
||||||
|
|||||||
@@ -133,12 +133,14 @@ onBeforeRouteUpdate((to) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 菜单项基础样式
|
// 菜单项基础样式 - 统一一级菜单和子菜单目录的宽度
|
||||||
.horizontal-menu :deep(.el-menu-item),
|
.horizontal-menu :deep(.el-menu-item),
|
||||||
.horizontal-menu :deep(.el-sub-menu__title) {
|
.horizontal-menu :deep(.el-sub-menu__title) {
|
||||||
margin: 0 5px !important;
|
margin: 0 5px !important;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 160px;
|
max-width: 150px;
|
||||||
min-width: 100px;
|
min-width: 120px; // 统一最小宽度
|
||||||
|
text-align: center; // 使文字居中对齐
|
||||||
|
padding: 0 8px !important; // 统一内边距
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,24 +6,12 @@ import { defineStore } from 'pinia';
|
|||||||
export const useAutoOpenResource = defineStore('autoOpenResource', {
|
export const useAutoOpenResource = defineStore('autoOpenResource', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
autoOpenResource: {
|
autoOpenResource: {
|
||||||
machineCodePath: '',
|
codePath: '',
|
||||||
dbCodePath: '',
|
|
||||||
redisCodePath: '',
|
|
||||||
mongoCodePath: '',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setMachineCodePath(codePath: string) {
|
setCodePath(codePath: string) {
|
||||||
this.autoOpenResource.machineCodePath = codePath;
|
this.autoOpenResource.codePath = codePath;
|
||||||
},
|
|
||||||
setDbCodePath(codePath: string) {
|
|
||||||
this.autoOpenResource.dbCodePath = codePath;
|
|
||||||
},
|
|
||||||
setRedisCodePath(codePath: string) {
|
|
||||||
this.autoOpenResource.redisCodePath = codePath;
|
|
||||||
},
|
|
||||||
setMongoCodePath(codePath: string) {
|
|
||||||
this.autoOpenResource.mongoCodePath = codePath;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,41 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 淡入淡出滑动效果
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
transform: translateX(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 水平滑动效果
|
||||||
|
.slide-x-enter-active,
|
||||||
|
.slide-x-leave-active {
|
||||||
|
transition: all 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-x-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-x-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Breadcrumb 面包屑过渡动画
|
/* Breadcrumb 面包屑过渡动画
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.breadcrumb-enter-active,
|
.breadcrumb-enter-active,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@use 'mixins/index' as mixins;
|
|
||||||
|
|
||||||
/* NavMenu 导航菜单
|
/* NavMenu 导航菜单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
$radius: 6px;
|
$radius: 6px;
|
||||||
@@ -24,12 +22,6 @@ $spacing: 8px;
|
|||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
color: #5a5a5a; // 统一调整菜单字体颜色为更舒适的深灰色
|
color: #5a5a5a; // 统一调整菜单字体颜色为更舒适的深灰色
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
// 第三方图标字体间距/大小设置
|
|
||||||
.icon,
|
|
||||||
.fa {
|
|
||||||
@include mixins.generalIcon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
/* 第三方图标字体间距/大小设置
|
|
||||||
------------------------------- */
|
|
||||||
@mixin generalIcon {
|
|
||||||
font-size: 14px !important;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 5px;
|
|
||||||
width: 24px;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文本不换行
|
/* 文本不换行
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@mixin text-no-wrap() {
|
@mixin text-no-wrap() {
|
||||||
|
|||||||
@@ -365,30 +365,14 @@ const initData = async () => {
|
|||||||
|
|
||||||
const toPage = (item: any, codePath = '') => {
|
const toPage = (item: any, codePath = '') => {
|
||||||
let path;
|
let path;
|
||||||
|
useAutoOpenResource().setCodePath(codePath);
|
||||||
switch (item) {
|
switch (item) {
|
||||||
case 'personal': {
|
case 'personal': {
|
||||||
router.push('/personal');
|
router.push('/personal');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'mongo': {
|
default: {
|
||||||
useAutoOpenResource().setMongoCodePath(codePath);
|
path = '/my-resource';
|
||||||
path = '/mongo/mongo-data-operation';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'machine': {
|
|
||||||
useAutoOpenResource().setMachineCodePath(codePath);
|
|
||||||
path = '/machine/machines-op';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'db': {
|
|
||||||
useAutoOpenResource().setDbCodePath(codePath);
|
|
||||||
path = '/dbms/sql-exec';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'redis': {
|
|
||||||
useAutoOpenResource().setRedisCodePath(codePath);
|
|
||||||
path = '/redis/data-operation';
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-splitter @resize="handleResize">
|
<el-splitter @resize="handleResize">
|
||||||
<el-splitter-panel :size="leftPaneSize + '%'" max="30%">
|
<el-splitter-panel :size="leftPaneSize + '%'" max="40%">
|
||||||
<slot name="left"></slot>
|
<slot name="left"></slot>
|
||||||
</el-splitter-panel>
|
</el-splitter-panel>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,49 @@ import { OptionsApi, SearchItem } from '@/components/pagetable/SearchForm';
|
|||||||
import { ContextmenuItem } from '@/components/contextmenu';
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '../tag/api';
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
|
// 资源配置
|
||||||
|
export interface ResourceConfig {
|
||||||
|
order?: number;
|
||||||
|
resourceType: number; // 资源类型
|
||||||
|
rootNodeType: NodeType; // 资源根节点类型
|
||||||
|
|
||||||
|
// 资源管理组件配置
|
||||||
|
manager?: {
|
||||||
|
componentConf: ResourceComponentConfig; // 组件
|
||||||
|
countKey?: string; // 统计数key,tab展示的数字对象key
|
||||||
|
permCode?: string; // 权限码
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceComponentConfig {
|
||||||
|
name: string; // 名称
|
||||||
|
component?: any; // 组件
|
||||||
|
icon?: {
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceOpCtx {
|
||||||
|
/**
|
||||||
|
* 添加资源相关组件
|
||||||
|
* @param component 资源相关组件配置
|
||||||
|
* @returns 组件引用
|
||||||
|
*/
|
||||||
|
addResourceComponent(component: ResourceComponentConfig): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取树节点
|
||||||
|
* @param nodeKey 节点key
|
||||||
|
*/
|
||||||
|
getTreeNode(nodeKey: string): any;
|
||||||
|
|
||||||
|
setCurrentTreeKey(nodeKey: string): void;
|
||||||
|
|
||||||
|
reloadTreeNode(nodeKey: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
export class TagTreeNode {
|
export class TagTreeNode {
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +84,14 @@ export class TagTreeNode {
|
|||||||
|
|
||||||
icon: any;
|
icon: any;
|
||||||
|
|
||||||
|
// 节点组件
|
||||||
|
nodeComponent?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点上下文
|
||||||
|
*/
|
||||||
|
ctx?: ResourceOpCtx;
|
||||||
|
|
||||||
static TagPath = -1;
|
static TagPath = -1;
|
||||||
|
|
||||||
constructor(key: any, label: string, type?: NodeType) {
|
constructor(key: any, label: string, type?: NodeType) {
|
||||||
@@ -49,6 +100,10 @@ export class TagTreeNode {
|
|||||||
this.type = type || new NodeType(TagTreeNode.TagPath);
|
this.type = type || new NodeType(TagTreeNode.TagPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static new(parent: TagTreeNode, key: any, label: string, type?: NodeType) {
|
||||||
|
return new TagTreeNode(key, label, type).withContext(parent.ctx);
|
||||||
|
}
|
||||||
|
|
||||||
withLabelRemark(labelRemark: any) {
|
withLabelRemark(labelRemark: any) {
|
||||||
this.labelRemark = labelRemark;
|
this.labelRemark = labelRemark;
|
||||||
return this;
|
return this;
|
||||||
@@ -74,6 +129,16 @@ export class TagTreeNode {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withNodeComponent(component: any) {
|
||||||
|
this.nodeComponent = markRaw(component);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(ctx: ResourceOpCtx | undefined) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
||||||
* @returns 子节点信息
|
* @returns 子节点信息
|
||||||
@@ -108,7 +173,7 @@ export class NodeType {
|
|||||||
nodeClickFunc: (node: TagTreeNode) => void;
|
nodeClickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
// 节点双击事件
|
// 节点双击事件
|
||||||
nodeDblclickFunc: (node: TagTreeNode) => void;
|
nodeDblclickFunc?: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
constructor(value: number) {
|
constructor(value: number) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
lazy
|
lazy
|
||||||
>
|
>
|
||||||
<template #tableHeader>
|
<template #tableHeader>
|
||||||
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">{{ $t('common.create') }}</el-button>
|
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)" plain>{{ $t('common.create') }}</el-button>
|
||||||
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">
|
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete" plain>
|
||||||
{{ $t('common.delete') }}
|
{{ $t('common.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
trigger="click"
|
trigger="click"
|
||||||
v-if="column.key !== rowNoColumn.key"
|
v-if="column.key !== rowNoColumn.key"
|
||||||
size="small"
|
size="small"
|
||||||
|
placement="bottom-start"
|
||||||
>
|
>
|
||||||
<span class="column-actions-trigger">
|
<span class="column-actions-trigger">
|
||||||
<!-- 排序箭头图标 -->
|
<!-- 排序箭头图标 -->
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
>
|
>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-row :gutter="10" justify="left">
|
<el-row :gutter="10" justify="start">
|
||||||
<el-link class="op-page" underline="never" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" :title="$t('db.homePage')" />
|
<el-link class="op-page" underline="never" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" :title="$t('db.homePage')" />
|
||||||
<el-link
|
<el-link
|
||||||
class="op-page"
|
class="op-page"
|
||||||
|
|||||||
@@ -506,8 +506,9 @@ export class DbInst {
|
|||||||
|
|
||||||
// 获取该列中最长的数据(内容)
|
// 获取该列中最长的数据(内容)
|
||||||
let maxWidthText = '';
|
let maxWidthText = '';
|
||||||
|
const length = tableData.length > 10 ? 10 : tableData.length; // 只取前几条数据计算宽度
|
||||||
// 获取该列中最长的数据(内容)
|
// 获取该列中最长的数据(内容)
|
||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
let nowValue = tableData[i][prop];
|
let nowValue = tableData[i][prop];
|
||||||
if (!nowValue) {
|
if (!nowValue) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
690
frontend/src/views/ops/db/resource/DbDataOp.vue
Normal file
@@ -0,0 +1,690 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-sql-exec h-full">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24" v-if="state.db">
|
||||||
|
<el-descriptions :column="4" size="small" border>
|
||||||
|
<el-descriptions-item label-align="right" :label="$t('common.operation')">
|
||||||
|
<el-button
|
||||||
|
:disabled="!state.db || !nowDbInst.id"
|
||||||
|
type="primary"
|
||||||
|
icon="Search"
|
||||||
|
link
|
||||||
|
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases, nodeKey: getSqlMenuNodeKey(nowDbInst.id, state.db) }, state.db)"
|
||||||
|
:title="$t('db.newQuery')"
|
||||||
|
>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<template v-if="!dbConfig.locationTreeNode">
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<el-button @click="locationNowTreeNode(null)" :title="$t('db.locationTagTree')" icon="Location" link></el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<!-- 数据库展示配置 -->
|
||||||
|
<el-popover
|
||||||
|
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||||
|
placement="bottom"
|
||||||
|
width="auto"
|
||||||
|
:title="$t('db.dbShowSetting')"
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<el-row>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="dbConfig.showColumnComment"
|
||||||
|
:label="$t('db.showFieldComments')"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="dbConfig.locationTreeNode"
|
||||||
|
:label="$t('db.autoLocationTagTree')"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-checkbox v-model="dbConfig.cacheTable" :label="$t('db.cacheTableInfo')" :true-value="1" :false-value="0" size="small" />
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<template #reference>
|
||||||
|
<el-link type="primary" icon="setting" underline="never"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label-align="right">
|
||||||
|
<template #label>
|
||||||
|
<div>
|
||||||
|
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
|
||||||
|
{{ $t('db.dbInst') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ nowDbInst.id }}
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ nowDbInst.name }}
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ nowDbInst.host }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :label="$t('db.dbName')" label-align="right">{{ state.db }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div id="data-exec" class="mt-1">
|
||||||
|
<el-tabs
|
||||||
|
v-if="state.tabs.size > 0"
|
||||||
|
type="card"
|
||||||
|
@tab-remove="onRemoveTab"
|
||||||
|
@tab-change="onTabChange"
|
||||||
|
v-model="state.activeName"
|
||||||
|
class="!h-full w-full"
|
||||||
|
>
|
||||||
|
<el-tab-pane class="!h-full" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||||
|
<template #label>
|
||||||
|
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="!text-[12px]">{{ dt.label }}</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item label="tagPath">
|
||||||
|
{{ dt.params.tagPath }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ dt.params.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Host">
|
||||||
|
<SvgIcon :name="getDbDialect(dt.params.type).getInfo().icon" :size="18" />
|
||||||
|
{{ dt.params.host }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('db.dbName')">
|
||||||
|
{{ dt.params.dbName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<db-table-data-op
|
||||||
|
v-if="dt.type === TabType.TableData"
|
||||||
|
:db-id="dt.dbId"
|
||||||
|
:db-name="dt.db"
|
||||||
|
:table-name="dt.params.table"
|
||||||
|
:table-height="state.dataTabsTableHeight"
|
||||||
|
:ref="(el: any) => (dt.componentRef = el)"
|
||||||
|
></db-table-data-op>
|
||||||
|
|
||||||
|
<db-sql-editor
|
||||||
|
v-if="dt.type === TabType.Query"
|
||||||
|
:db-id="dt.dbId"
|
||||||
|
:db-name="dt.db"
|
||||||
|
:sql-name="dt.params.sqlName"
|
||||||
|
@save-sql-success="reloadSqls"
|
||||||
|
:ref="(el: any) => (dt.componentRef = el)"
|
||||||
|
>
|
||||||
|
</db-sql-editor>
|
||||||
|
|
||||||
|
<db-tables-op
|
||||||
|
v-if="dt.type == TabType.TablesOp"
|
||||||
|
:db-id="dt.params.id"
|
||||||
|
:db="dt.params.db"
|
||||||
|
:db-type="dt.params.type"
|
||||||
|
:height="state.tablesOpHeight"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<db-table-op
|
||||||
|
:title="tableCreateDialog.title"
|
||||||
|
:active-name="tableCreateDialog.activeName"
|
||||||
|
:dbId="tableCreateDialog.dbId"
|
||||||
|
:db="tableCreateDialog.db"
|
||||||
|
:dbType="tableCreateDialog.dbType"
|
||||||
|
:version="tableCreateDialog.version"
|
||||||
|
:data="tableCreateDialog.data"
|
||||||
|
v-model:visible="tableCreateDialog.visible"
|
||||||
|
@submit-sql="onSubmitEditTableSql"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog width="55%" :title="`'${state.chooseTableName}' DDL`" v-model="state.ddlDialog.visible">
|
||||||
|
<monaco-editor height="400px" language="sql" v-model="state.ddlDialog.ddl" :options="{ readOnly: true }" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<contextmenu ref="tabContextmenuRef" :dropdown="state.tabContextmenu.dropdown" :items="state.tabContextmenu.items" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, getCurrentInstance, h, inject, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
|
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from '../db';
|
||||||
|
import { ResourceOpCtx } from '@/views/ops/component/tag';
|
||||||
|
import { dbApi } from '../api';
|
||||||
|
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
import { getDbDialect } from '../dialect/index';
|
||||||
|
import { useEventListener, useStorage } from '@vueuse/core';
|
||||||
|
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||||
|
import { format as sqlFormatter } from 'sql-formatter';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||||
|
import { DbDataOpComp } from '@/views/ops/db/resource';
|
||||||
|
|
||||||
|
const DbTableOp = defineAsyncComponent(() => import('../component/table/DbTableOp.vue'));
|
||||||
|
const DbSqlEditor = defineAsyncComponent(() => import('../component/sqleditor/DbSqlEditor.vue'));
|
||||||
|
const DbTableDataOp = defineAsyncComponent(() => import('../component/table/DbTableDataOp.vue'));
|
||||||
|
const DbTablesOp = defineAsyncComponent(() => import('../component/table/DbTablesOp.vue'));
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||||
|
|
||||||
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
|
const tabContextmenuRef: any = useTemplateRef('tabContextmenuRef');
|
||||||
|
|
||||||
|
const tabContextmenuItems = [
|
||||||
|
new ContextmenuItem(1, 'db.close').withIcon('Close').withOnClick((data: any) => {
|
||||||
|
onRemoveTab(data.key);
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ContextmenuItem(2, 'db.closeOther').withIcon('CircleClose').withOnClick((data: any) => {
|
||||||
|
const tabName = data.key;
|
||||||
|
const tabNames = [...state.tabs.keys()];
|
||||||
|
for (let tab of tabNames) {
|
||||||
|
if (tab !== tabName) {
|
||||||
|
onRemoveTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabs: Map<string, TabInfo> = new Map();
|
||||||
|
const state = reactive({
|
||||||
|
defaultExpendKey: [] as any,
|
||||||
|
/**
|
||||||
|
* 当前操作的数据库实例
|
||||||
|
*/
|
||||||
|
nowDbInst: {} as DbInst,
|
||||||
|
db: '', // 当前操作的数据库
|
||||||
|
activeName: '',
|
||||||
|
reloadStatus: false,
|
||||||
|
tabs,
|
||||||
|
tabContextmenu: {
|
||||||
|
dropdown: { x: 0, y: 0 },
|
||||||
|
items: tabContextmenuItems,
|
||||||
|
},
|
||||||
|
dataTabsTableHeight: '600px',
|
||||||
|
tablesOpHeight: '600',
|
||||||
|
dbServerInfo: {
|
||||||
|
loading: true,
|
||||||
|
version: '',
|
||||||
|
},
|
||||||
|
tableCreateDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
activeName: '',
|
||||||
|
dbId: 0,
|
||||||
|
version: '',
|
||||||
|
db: '',
|
||||||
|
dbType: '',
|
||||||
|
data: {},
|
||||||
|
parentKey: '',
|
||||||
|
},
|
||||||
|
chooseTableName: '',
|
||||||
|
ddlDialog: {
|
||||||
|
visible: false,
|
||||||
|
ddl: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||||
|
|
||||||
|
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
state.reloadStatus = !dbConfig.value.cacheTable;
|
||||||
|
emits('init', { name: DbDataOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||||
|
setHeight();
|
||||||
|
// 监听浏览器窗口大小变化,更新对应组件高度
|
||||||
|
useEventListener(window, 'resize', setHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
dispposeCompletionItemProvider('sql');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置editor高度和数据表高度
|
||||||
|
*/
|
||||||
|
const setHeight = () => {
|
||||||
|
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
|
||||||
|
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择数据库,改变当前正在操作的数据库信息
|
||||||
|
const changeDb = async (db: any, dbName: string) => {
|
||||||
|
state.nowDbInst = await DbInst.getOrNewInst(db);
|
||||||
|
state.nowDbInst.databases = db.databases;
|
||||||
|
state.db = dbName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载选中的表数据,即新增表数据操作tab
|
||||||
|
const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||||
|
if (tableName == '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await changeDb(db, dbName);
|
||||||
|
|
||||||
|
const key = `tableData:${db.id}.${dbName}.${tableName}`;
|
||||||
|
let tab = state.tabs.get(key);
|
||||||
|
state.activeName = key;
|
||||||
|
// 如果存在该表tab,则直接返回
|
||||||
|
if (tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab = new TabInfo();
|
||||||
|
tab.label = tableName;
|
||||||
|
tab.key = key;
|
||||||
|
tab.treeNodeKey = db.nodeKey;
|
||||||
|
tab.dbId = db.id;
|
||||||
|
tab.db = dbName;
|
||||||
|
tab.type = TabType.TableData;
|
||||||
|
tab.params = {
|
||||||
|
...getNowDbInfo(),
|
||||||
|
table: tableName,
|
||||||
|
};
|
||||||
|
state.tabs.set(key, tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新建查询tab
|
||||||
|
const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||||
|
if (!dbName || !db.id) {
|
||||||
|
ElMessage.warning(t('db.noDbInstMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await changeDb(db, dbName);
|
||||||
|
|
||||||
|
const dbId = db.id;
|
||||||
|
let label;
|
||||||
|
let key;
|
||||||
|
// 存在sql模板名,则该模板名只允许一个tab
|
||||||
|
if (sqlName) {
|
||||||
|
label = `${t('db.query')}-${sqlName}`;
|
||||||
|
key = `query:${dbId}.${dbName}.${sqlName}`;
|
||||||
|
} else {
|
||||||
|
let count = 1;
|
||||||
|
state.tabs.forEach((v) => {
|
||||||
|
if (v.type == TabType.Query && !v.params.sqlName) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
label = `${t('db.nQuery')}-${count}`;
|
||||||
|
key = `query:${count}.${dbId}.${dbName}`;
|
||||||
|
}
|
||||||
|
state.activeName = key;
|
||||||
|
let tab = state.tabs.get(key);
|
||||||
|
if (tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab = new TabInfo();
|
||||||
|
tab.key = key;
|
||||||
|
tab.label = label;
|
||||||
|
tab.treeNodeKey = db.nodeKey;
|
||||||
|
tab.dbId = dbId;
|
||||||
|
tab.db = dbName;
|
||||||
|
tab.type = TabType.Query;
|
||||||
|
tab.params = {
|
||||||
|
...getNowDbInfo(),
|
||||||
|
sqlName: sqlName,
|
||||||
|
dbs: db.dbs,
|
||||||
|
};
|
||||||
|
state.tabs.set(key, tab);
|
||||||
|
// 注册当前sql编辑框提示词
|
||||||
|
registerDbCompletionItemProvider(tab.dbId, tab.db, tab.params.dbs, nowDbInst.value.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加数据操作tab
|
||||||
|
* @param inst
|
||||||
|
*/
|
||||||
|
const addTablesOpTab = async (db: any) => {
|
||||||
|
const dbName = db.db;
|
||||||
|
if (!db || !db.id) {
|
||||||
|
ElMessage.warning(t('db.noDbInstMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await changeDb(db, dbName);
|
||||||
|
|
||||||
|
const dbId = db.id;
|
||||||
|
let key = `tablesOp:${dbId}.${dbName}`;
|
||||||
|
state.activeName = key;
|
||||||
|
|
||||||
|
let tab = state.tabs.get(key);
|
||||||
|
if (tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab = new TabInfo();
|
||||||
|
tab.key = key;
|
||||||
|
tab.label = `${t('db.tableOp')}-${dbName}`;
|
||||||
|
tab.treeNodeKey = db.nodeKey;
|
||||||
|
tab.dbId = dbId;
|
||||||
|
tab.db = dbName;
|
||||||
|
tab.type = TabType.TablesOp;
|
||||||
|
tab.params = {
|
||||||
|
...getNowDbInfo(),
|
||||||
|
id: db.id,
|
||||||
|
db: dbName,
|
||||||
|
type: db.type,
|
||||||
|
};
|
||||||
|
state.tabs.set(key, tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTab = (targetName: string) => {
|
||||||
|
let activeName = state.activeName;
|
||||||
|
const tabNames = [...state.tabs.keys()];
|
||||||
|
for (let i = 0; i < tabNames.length; i++) {
|
||||||
|
const tabName = tabNames[i];
|
||||||
|
if (tabName !== targetName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tabs.delete(targetName);
|
||||||
|
if (activeName != targetName) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||||
|
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab;
|
||||||
|
} else {
|
||||||
|
activeName = '';
|
||||||
|
}
|
||||||
|
state.activeName = activeName;
|
||||||
|
onTabChange();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTabChange = () => {
|
||||||
|
if (!state.activeName) {
|
||||||
|
state.nowDbInst = {} as DbInst;
|
||||||
|
state.db = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowTab = state.tabs.get(state.activeName);
|
||||||
|
state.nowDbInst = DbInst.getInst(nowTab?.dbId);
|
||||||
|
state.db = nowTab?.db as string;
|
||||||
|
|
||||||
|
if (nowTab?.type == TabType.Query) {
|
||||||
|
// 注册sql提示
|
||||||
|
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激活当前tab(需要调用DbTableData组件的active,否则表头与数据会出现错位,暂不知为啥,先这样处理)
|
||||||
|
nowTab?.componentRef?.active();
|
||||||
|
|
||||||
|
if (dbConfig.value.locationTreeNode) {
|
||||||
|
locationNowTreeNode(nowTab);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 右键点击时:传 x,y 坐标值到子组件中(props)
|
||||||
|
const onTabContextmenu = (v: any, e: any) => {
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
state.tabContextmenu.dropdown.x = clientX;
|
||||||
|
state.tabContextmenu.dropdown.y = clientY;
|
||||||
|
tabContextmenuRef.value.openContextmenu(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位至当前树节点
|
||||||
|
*/
|
||||||
|
const locationNowTreeNode = (nowTab: any = null) => {
|
||||||
|
if (!nowTab) {
|
||||||
|
nowTab = state.tabs.get(state.activeName);
|
||||||
|
}
|
||||||
|
setTimeout(() => resourceOpCtx?.setCurrentTreeKey(nowTab?.treeNodeKey), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadSqls = (dbId: number, db: string) => {
|
||||||
|
resourceOpCtx?.reloadTreeNode(getSqlMenuNodeKey(dbId, db));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSql = async (dbId: any, db: string, sqlName: string) => {
|
||||||
|
try {
|
||||||
|
await useI18nDeleteConfirm(sqlName);
|
||||||
|
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
reloadSqls(dbId, db);
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||||
|
return `${dbId}.${db}.sql-menu`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadNode = (nodeKey: string) => {
|
||||||
|
state.reloadStatus = true;
|
||||||
|
resourceOpCtx?.reloadTreeNode(nodeKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, tableComment, type, parentKey, key, version } = data.params;
|
||||||
|
// data.label就是表名
|
||||||
|
if (tableName) {
|
||||||
|
state.tableCreateDialog.title = useI18nEditTitle('db.table');
|
||||||
|
let indexs = await dbApi.tableIndex.request({ id, db, tableName });
|
||||||
|
let columns = await dbApi.columnMetadata.request({ id, db, tableName });
|
||||||
|
let row = { tableName, tableComment };
|
||||||
|
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
|
||||||
|
state.tableCreateDialog.parentKey = parentKey;
|
||||||
|
} else {
|
||||||
|
state.tableCreateDialog.title = useI18nCreateTitle('db.table');
|
||||||
|
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||||
|
state.tableCreateDialog.parentKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tableCreateDialog.activeName = '1';
|
||||||
|
state.tableCreateDialog.dbId = id;
|
||||||
|
state.tableCreateDialog.version = version;
|
||||||
|
state.tableCreateDialog.db = db;
|
||||||
|
state.tableCreateDialog.dbType = type;
|
||||||
|
state.tableCreateDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey, schema } = data.params;
|
||||||
|
await useI18nDeleteConfirm(tableName);
|
||||||
|
|
||||||
|
// 执行sql
|
||||||
|
let dialect = getDbDialect(state.nowDbInst.type);
|
||||||
|
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
|
||||||
|
|
||||||
|
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then((res) => {
|
||||||
|
let success = true;
|
||||||
|
for (let re of res) {
|
||||||
|
if (re.errorMsg) {
|
||||||
|
success = false;
|
||||||
|
ElMessage.error(`${re.sql} -> ${re.errorMsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGenDdl = async (data: any) => {
|
||||||
|
let { db, id, tableName, type } = data.params;
|
||||||
|
state.chooseTableName = tableName;
|
||||||
|
let res = await dbApi.tableDdl.request({ id, db, tableName });
|
||||||
|
state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(type).getInfo().formatSqlDialect as any });
|
||||||
|
state.ddlDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRenameTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey } = data.params;
|
||||||
|
let tableData = { db, oldTableName: tableName, tableName };
|
||||||
|
|
||||||
|
let value = ref(tableName);
|
||||||
|
// 弹出确认框
|
||||||
|
const promptValue = await ElMessageBox.prompt('', t('db.renamePrompt', { db, tableName }), {
|
||||||
|
inputValue: value.value,
|
||||||
|
confirmButtonText: t('common.confirm'),
|
||||||
|
cancelButtonText: t('common.cancel'),
|
||||||
|
});
|
||||||
|
|
||||||
|
tableData.tableName = promptValue.value;
|
||||||
|
let sql = nowDbInst.value.getDialect().getModifyTableInfoSql(tableData);
|
||||||
|
if (!sql) {
|
||||||
|
ElMessage.warning(t('db.noChange'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SqlExecBox({
|
||||||
|
sql: sql,
|
||||||
|
dbId: id as any,
|
||||||
|
db: db as any,
|
||||||
|
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
|
||||||
|
runSuccessCallback: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopyTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey } = data.params;
|
||||||
|
|
||||||
|
let checked = ref(false);
|
||||||
|
|
||||||
|
// 弹出确认框,并选择是否复制数据
|
||||||
|
await ElMessageBox({
|
||||||
|
title: `${t('db.copyTable')}【${tableName}】`,
|
||||||
|
type: 'warning',
|
||||||
|
// icon: markRaw(Delete),
|
||||||
|
message: () =>
|
||||||
|
h(ElCheckbox, {
|
||||||
|
label: t('db.isCopyTableData'),
|
||||||
|
modelValue: checked.value,
|
||||||
|
'onUpdate:modelValue': (val: boolean | string | number) => {
|
||||||
|
if (typeof val === 'boolean') {
|
||||||
|
checked.value = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
callback: (action: string) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
// 执行sql
|
||||||
|
dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
|
||||||
|
useI18nOperateSuccessMsg();
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitEditTableSql = () => {
|
||||||
|
state.tableCreateDialog.visible = false;
|
||||||
|
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||||
|
reloadNode(state.tableCreateDialog.parentKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前操作的数据库信息
|
||||||
|
*/
|
||||||
|
const getNowDbInfo = () => {
|
||||||
|
const di = state.nowDbInst;
|
||||||
|
return {
|
||||||
|
tagPath: di.tagPath,
|
||||||
|
id: di.id,
|
||||||
|
name: di.name,
|
||||||
|
type: di.type,
|
||||||
|
host: di.host,
|
||||||
|
dbName: state.db,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTables = async (dbInfo: any) => {
|
||||||
|
if (!dbInfo || !dbInfo.id) {
|
||||||
|
ElMessage.warning(t('db.noDbInstMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let { id, db } = dbInfo;
|
||||||
|
// 获取当前库的所有表信息
|
||||||
|
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||||
|
state.reloadStatus = !dbConfig.value.cacheTable;
|
||||||
|
return tables;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onChangeDb: changeDb,
|
||||||
|
loadTables,
|
||||||
|
loadTableData,
|
||||||
|
onCopyTable,
|
||||||
|
onEditTable,
|
||||||
|
onDeleteTable,
|
||||||
|
onGenDdl,
|
||||||
|
onRenameTable,
|
||||||
|
onRemoveTab,
|
||||||
|
addQueryTab,
|
||||||
|
addTablesOpTab,
|
||||||
|
reloadSqls,
|
||||||
|
deleteSql,
|
||||||
|
reloadNode,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.db-sql-exec {
|
||||||
|
#data-exec {
|
||||||
|
::v-deep(.el-tabs) {
|
||||||
|
--el-tabs-header-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-tabs__header) {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-tabs__nav-next) {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
::v-deep(.el-tabs__nav-prev) {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update_field_active {
|
||||||
|
background-color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
53
frontend/src/views/ops/db/resource/NodeDbInst.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #prefix="{ data }">
|
||||||
|
<el-popover @show="showDbInfo(data.params)" :show-after="500" placement="right-start" :title="$t('db.dbInstInfo')" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ data.params.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Host">
|
||||||
|
{{ `${data.params.host}:${data.params.port}` }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="version">
|
||||||
|
<span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('db.acName')">
|
||||||
|
{{ data.params.authCertName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('common.remark')">
|
||||||
|
{{ data.params.remark }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { dbApi } from '../api';
|
||||||
|
import { getDbDialect } from '../dialect/index';
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
|
||||||
|
const serverInfoReqParam = ref({
|
||||||
|
instanceId: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam);
|
||||||
|
|
||||||
|
const showDbInfo = async (db: any) => {
|
||||||
|
if (dbServerInfo.value) {
|
||||||
|
dbServerInfo.value.version = '';
|
||||||
|
}
|
||||||
|
serverInfoReqParam.value.instanceId = db.instanceId;
|
||||||
|
await getDbServerInfo();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
13
frontend/src/views/ops/db/resource/NodeDbTable.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #suffix="{ data }">
|
||||||
|
<span v-if="data.params.size">{{ ` ${data.params.size}` }}</span>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
295
frontend/src/views/ops/db/resource/index.ts
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
|
||||||
|
import { NodeType, TagTreeNode, ResourceConfig } from '../../component/tag';
|
||||||
|
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { dbApi } from '../api';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
import { DbInst } from '../db';
|
||||||
|
import { schemaDbTypes } from '../dialect/index';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
|
||||||
|
const DbInstList = defineAsyncComponent(() => import('../InstanceList.vue'));
|
||||||
|
const DbDataOp = defineAsyncComponent(() => import('./DbDataOp.vue'));
|
||||||
|
const NodeDbInst = defineAsyncComponent(() => import('./NodeDbInst.vue'));
|
||||||
|
const NodeDbTable = defineAsyncComponent(() => import('./NodeDbTable.vue'));
|
||||||
|
|
||||||
|
const DbIcon = {
|
||||||
|
name: ResourceTypeEnum.Db.extra.icon,
|
||||||
|
color: ResourceTypeEnum.Db.extra.iconColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
// pgsql schema icon
|
||||||
|
const SchemaIcon = {
|
||||||
|
name: 'List',
|
||||||
|
color: '#67c23a',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableIcon = {
|
||||||
|
name: 'icon db/table',
|
||||||
|
color: '#409eff',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SqlIcon = {
|
||||||
|
name: 'icon db/sql',
|
||||||
|
color: '#f56c6c',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataOpComp = {
|
||||||
|
name: 'tag.dbDataOp',
|
||||||
|
component: DbDataOp,
|
||||||
|
icon: DbIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
// node节点点击时,触发改变db事件
|
||||||
|
const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
|
||||||
|
const params = nodeData.params;
|
||||||
|
if (params.db) {
|
||||||
|
const compRef = await nodeData.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
compRef.onChangeDb(
|
||||||
|
{
|
||||||
|
id: params.id,
|
||||||
|
host: `${params.host}`,
|
||||||
|
name: params.name,
|
||||||
|
type: params.type,
|
||||||
|
tagPath: params.tagPath,
|
||||||
|
databases: params.dbs,
|
||||||
|
},
|
||||||
|
params.db
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContextmenuItemRefresh = new ContextmenuItem('refresh', 'common.refresh')
|
||||||
|
.withIcon('RefreshRight')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).reloadNode(node.key));
|
||||||
|
|
||||||
|
// tagpath 节点类型
|
||||||
|
const NodeTypeDbTag = new NodeType(TagTreeNode.TagPath)
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
|
||||||
|
const tagPath = parentNode.params.tagPath;
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({ tagPath });
|
||||||
|
const dbInfos = dbInfoRes.list;
|
||||||
|
if (!dbInfos) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||||
|
await sleep(100);
|
||||||
|
return dbInfos?.map((x: any) => {
|
||||||
|
x.tagPath = tagPath;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbInst).withParams(x).withNodeComponent(NodeDbInst);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||||
|
|
||||||
|
// 数据库实例节点类型
|
||||||
|
const NodeTypeDbInst = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
|
// 查询数据库版本信息
|
||||||
|
const version = await dbApi.getCompatibleDbVersion.request({ id: params.id, db: dbs[0] });
|
||||||
|
|
||||||
|
return dbs.map((x: any) => {
|
||||||
|
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||||
|
.withParams({
|
||||||
|
tagPath: params.tagPath,
|
||||||
|
id: params.id,
|
||||||
|
name: params.name,
|
||||||
|
type: params.type,
|
||||||
|
version: version || 'unset',
|
||||||
|
host: `${params.host}:${params.port}`,
|
||||||
|
dbs: dbs,
|
||||||
|
db: x,
|
||||||
|
})
|
||||||
|
.withIcon(DbIcon);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据库节点
|
||||||
|
const NodeTypeDb = new NodeType(2)
|
||||||
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
|
// pg类数据库会多一层schema
|
||||||
|
if (schemaDbTypes.includes(params.type)) {
|
||||||
|
const { id, db } = params;
|
||||||
|
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||||
|
return schemaNames.map((sn: any) => {
|
||||||
|
// 将db变更为 db/schema;
|
||||||
|
const nParams = { ...params };
|
||||||
|
nParams.schema = sn;
|
||||||
|
nParams.db = nParams.db + '/' + sn;
|
||||||
|
nParams.dbs = schemaNames;
|
||||||
|
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
|
||||||
|
.withParams(nParams)
|
||||||
|
.withIcon(SchemaIcon);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getNodeTypeTables(parentNode);
|
||||||
|
})
|
||||||
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
|
const getNodeTypeTables = (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||||
|
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||||
|
return [
|
||||||
|
TagTreeNode.new(parentNode, `${params.id}.${params.db}.table-menu`, i18n.global.t('db.table'), NodeTypeTableMenu)
|
||||||
|
.withParams({
|
||||||
|
...params,
|
||||||
|
key: tableKey,
|
||||||
|
})
|
||||||
|
.withIcon(TableIcon),
|
||||||
|
|
||||||
|
TagTreeNode.new(parentNode, sqlKey, 'SQL', NodeTypeSqlMenu)
|
||||||
|
.withParams({ ...params, key: sqlKey })
|
||||||
|
.withIcon(SqlIcon),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// postgres schema模式
|
||||||
|
const NodeTypePostgresSchema = new NodeType(3)
|
||||||
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
|
return getNodeTypeTables(parentNode);
|
||||||
|
})
|
||||||
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
|
// 数据库表菜单节点
|
||||||
|
const NodeTypeTableMenu = new NodeType(4)
|
||||||
|
.withContextMenuItems([
|
||||||
|
ContextmenuItemRefresh,
|
||||||
|
new ContextmenuItem('createTable', 'db.createTable').withIcon('Plus').withOnClick(async (parentNode: TagTreeNode) => {
|
||||||
|
(await parentNode.ctx?.addResourceComponent(DbDataOpComp))?.onEditTable(parentNode);
|
||||||
|
}),
|
||||||
|
new ContextmenuItem('tablesOp', 'db.tableOp').withIcon('Setting').withOnClick(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
(await parentNode.ctx?.addResourceComponent(DbDataOpComp)).addTablesOpTab({
|
||||||
|
id: params.id,
|
||||||
|
db: params.db,
|
||||||
|
type: params.type,
|
||||||
|
nodeKey: parentNode.key,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
const params = parentNode.params;
|
||||||
|
// // 获取当前库的所有表信息
|
||||||
|
const tables = await compRef.loadTables(params);
|
||||||
|
let { id, db, type, schema, version } = params;
|
||||||
|
let dbTableSize = 0;
|
||||||
|
const tablesNode = tables.map((x: any) => {
|
||||||
|
const tableSize = x.dataLength + x.indexLength;
|
||||||
|
dbTableSize += tableSize;
|
||||||
|
const key = `${id}.${db}.${x.tableName}`;
|
||||||
|
return TagTreeNode.new(parentNode, key, x.tableName, NodeTypeTable)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({
|
||||||
|
id,
|
||||||
|
db,
|
||||||
|
type,
|
||||||
|
schema,
|
||||||
|
version,
|
||||||
|
key: key,
|
||||||
|
parentKey: parentNode.key,
|
||||||
|
tableName: x.tableName,
|
||||||
|
tableComment: x.tableComment,
|
||||||
|
size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
|
||||||
|
})
|
||||||
|
.withIcon(TableIcon)
|
||||||
|
.withNodeComponent(NodeDbTable)
|
||||||
|
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
||||||
|
});
|
||||||
|
// 设置父节点参数的表大小
|
||||||
|
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
|
||||||
|
return tablesNode;
|
||||||
|
});
|
||||||
|
// .withNodeDblclickFunc((node: TagTreeNode) => {
|
||||||
|
// const params = node.params;
|
||||||
|
// addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 数据库sql模板菜单节点
|
||||||
|
const NodeTypeSqlMenu = new NodeType(5)
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
const id = params.id;
|
||||||
|
const db = params.db;
|
||||||
|
const dbs = params.dbs;
|
||||||
|
// 加载用户保存的sql脚本
|
||||||
|
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||||
|
return sqls.map((x: any) => {
|
||||||
|
return TagTreeNode.new(parentNode, `${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({ id, db, dbs, sqlName: x.name })
|
||||||
|
.withIcon(SqlIcon);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
|
// 表节点类型
|
||||||
|
const NodeTypeTable = new NodeType(6)
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('copyTable', 'db.copyTable')
|
||||||
|
.withIcon('copyDocument')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onCopyTable(node)),
|
||||||
|
new ContextmenuItem('renameTable', 'db.renameTable')
|
||||||
|
.withIcon('edit')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onRenameTable(node)),
|
||||||
|
new ContextmenuItem('editTable', 'db.editTable')
|
||||||
|
.withIcon('edit')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onEditTable(node)),
|
||||||
|
new ContextmenuItem('delTable', 'db.delTable')
|
||||||
|
.withIcon('Delete')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onDeleteTable(node)),
|
||||||
|
new ContextmenuItem('ddl', 'DDL')
|
||||||
|
.withIcon('Document')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onGenDdl(node)),
|
||||||
|
])
|
||||||
|
.withNodeClickFunc(async (node: TagTreeNode) => {
|
||||||
|
const params = node.params;
|
||||||
|
(await node.ctx?.addResourceComponent(DbDataOpComp)).loadTableData({ id: params.id, nodeKey: node.key }, params.db, params.tableName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// sql模板节点类型
|
||||||
|
const NodeTypeSql = new NodeType(7)
|
||||||
|
.withNodeClickFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
const params = parentNode.params;
|
||||||
|
compRef.addQueryTab({ id: params.id, nodeKey: parentNode.key, dbs: params.dbs }, params.db, params.sqlName);
|
||||||
|
})
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('delSql', 'common.delete')
|
||||||
|
.withIcon('delete')
|
||||||
|
.withOnClick(async (node: TagTreeNode) =>
|
||||||
|
(await node.ctx?.addResourceComponent(DbDataOpComp)).deleteSql(node.params.id, node.params.db, node.params.sqlName)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||||
|
return `${dbId}.${db}.sql-menu`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: 2,
|
||||||
|
resourceType: ResourceTypeEnum.Db.value,
|
||||||
|
rootNodeType: NodeTypeDbTag,
|
||||||
|
manager: {
|
||||||
|
componentConf: {
|
||||||
|
component: DbInstList,
|
||||||
|
icon: DbIcon,
|
||||||
|
name: 'tag.db',
|
||||||
|
},
|
||||||
|
countKey: 'db',
|
||||||
|
permCode: 'db:instance',
|
||||||
|
},
|
||||||
|
} as ResourceConfig;
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
InstanceList: () => import('@/views/ops/db/InstanceList.vue'),
|
|
||||||
SqlExec: () => import('@/views/ops/db/SqlExec.vue'),
|
|
||||||
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
|
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
|
||||||
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
|
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
|
||||||
};
|
};
|
||||||
|
|||||||
42
frontend/src/views/ops/docker/DockerPanel.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card h-full">
|
||||||
|
<el-tabs v-model="activeName" @tab-change="handleTabChange">
|
||||||
|
<el-tab-pane :label="$t('docker.container')" :name="containerTab">
|
||||||
|
<ContainerList :host="props.host" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane :label="$t('docker.image')" :name="imageTab">
|
||||||
|
<ImageList v-if="activeName == imageTab" :host="props.host" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
|
||||||
|
|
||||||
|
const ContainerList = defineAsyncComponent(() => import('./container/ContainerList.vue'));
|
||||||
|
const ImageList = defineAsyncComponent(() => import('./image/ImageList.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
host: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerTab = 'containerTab';
|
||||||
|
const imageTab = 'imageTab';
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
activeName: containerTab,
|
||||||
|
cmdConfs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { activeName } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
state.activeName = containerTab;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTabChange = (tabName: any) => {};
|
||||||
|
</script>
|
||||||
39
frontend/src/views/ops/docker/DockerPanelDrawer.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer title="Docker" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="true" size="80%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="props.host" :back="cancel">
|
||||||
|
<template #extra>
|
||||||
|
<div class="mr20"></div>
|
||||||
|
</template>
|
||||||
|
</DrawerHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DockerPanel :host="props.host" />
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, ref, Ref } from 'vue';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
|
||||||
|
const DockerPanel = defineAsyncComponent(() => import('./DockerPanel.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
host: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogVisible = defineModel<boolean>('visible');
|
||||||
|
|
||||||
|
const emit = defineEmits(['cancel']);
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss"></style>
|
||||||
27
frontend/src/views/ops/docker/api.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import Api from '@/common/Api';
|
||||||
|
import config from '@/common/config';
|
||||||
|
import { joinClientParams } from '@/common/request';
|
||||||
|
|
||||||
|
export const dockerApi = {
|
||||||
|
info: Api.newGet('/docker/info'),
|
||||||
|
|
||||||
|
containers: Api.newGet('/docker/containers'),
|
||||||
|
containersStats: Api.newGet('/docker/containers/stats'),
|
||||||
|
containerStop: Api.newPost('/docker/containers/stop'),
|
||||||
|
containerRemove: Api.newPost('/docker/containers/remove'),
|
||||||
|
containerRestart: Api.newPost('/docker/containers/restart'),
|
||||||
|
containerCreate: Api.newPost('/docker/containers/create'),
|
||||||
|
|
||||||
|
images: Api.newGet('/docker/images'),
|
||||||
|
imageRemove: Api.newPost('/docker/images/remove'),
|
||||||
|
imageSave: Api.newPost('/docker/images/save'),
|
||||||
|
imageUpload: Api.newPost('/docker/images/load'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDockerExecSocketUrl(host: any, containerId: string) {
|
||||||
|
return `/docker/containers/exec?host=${host}&containerId=${containerId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContainerLogSocketUrl(host: any, containerId: string) {
|
||||||
|
return `${config.baseWsUrl}/docker/containers/logs?${joinClientParams()}&host=${host}&containerId=${containerId}`;
|
||||||
|
}
|
||||||
453
frontend/src/views/ops/docker/container/ContainerCreate.vue
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer v-model="dialogVisible" :append-to-body="true" :destroy-on-close="true" :close-on-click-modal="false" :before-close="cancel" size="40%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="$t('docker.createContainer')" :back="cancel">
|
||||||
|
<template #extra>
|
||||||
|
<div class="mr20"></div>
|
||||||
|
</template>
|
||||||
|
</DrawerHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-form :model="form" ref="formRef" label-position="top" :rules="rules" scroll-to-error>
|
||||||
|
<el-form-item prop="name" :label="$t('common.name')" clearable>
|
||||||
|
<el-input v-model.trim="form.name" auto-complete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="image" :label="$t('docker.image')">
|
||||||
|
<template #label>
|
||||||
|
{{ $t('docker.image') }}
|
||||||
|
<el-tooltip :content="$t('docker.imageTips')" placement="top">
|
||||||
|
<SvgIcon class="mb10" name="question-filled" />
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-select v-model="form.image" filterable allow-create>
|
||||||
|
<el-option v-for="item in state.images" :key="item.id" :label="item.tags[0]" :value="item.tags[0]"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-checkbox v-model="form.forcePull">{{ $t('docker.forcePull') }}</el-checkbox>
|
||||||
|
<el-tooltip :content="$t('docker.forcePullTips')" placement="top">
|
||||||
|
<SvgIcon class="ml-2" name="question-filled" />
|
||||||
|
</el-tooltip>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="cmdStr" :label="$t('Command')">
|
||||||
|
<el-select v-model="form.cmdStr" filterable allow-create>
|
||||||
|
<el-option v-for="item in defaultCommands" :key="item" :label="item" :value="item"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.port')">
|
||||||
|
<el-card class="w-full">
|
||||||
|
<el-table v-if="form.exposedPorts.length !== 0" :data="form.exposedPorts">
|
||||||
|
<el-table-column :label="$t('docker.server')" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input-number v-model="row.hostPort" :min="10000" :max="20000" />
|
||||||
|
<!-- <el-input v-model="row.hostPort" :placeholder="$t('docker.hostPortPlaceholder')" /> -->
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('docker.container')" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.containerPort" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('docker.protocol')" min-width="50">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-select v-model="row.protocol" style="width: 100%" :placeholder="$t('container.serverExample')">
|
||||||
|
<el-option label="tcp" value="tcp" />
|
||||||
|
<el-option label="udp" value="udp" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column min-width="35">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="primary" @click="handlePortsDelete(scope.$index)">
|
||||||
|
{{ $t('common.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-button class="ml-1 mt-1" size="small" @click="handlePortsAdd()">
|
||||||
|
{{ $t('common.add') }}
|
||||||
|
</el-button>
|
||||||
|
</el-card>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="mount" :label="$t('docker.mount')">
|
||||||
|
<el-card class="mb-1 w-full">
|
||||||
|
<el-table v-if="form.volumes.length !== 0" :data="form.volumes">
|
||||||
|
<el-table-column :label="$t('docker.hostDir')" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.hostDir" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('docker.permission')" :width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-select v-model="row.mode">
|
||||||
|
<el-option value="rw" :label="$t('docker.rw')" />
|
||||||
|
<el-option value="ro" :label="$t('docker.ro')" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('docker.containerDir')" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model="row.containerDir" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column min-width="40">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button link type="primary" @click="handleVolumesDelete(scope.$index)">
|
||||||
|
{{ $t('common.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-button @click="handleVolumesAdd()" size="small">
|
||||||
|
{{ $t('common.add') }}
|
||||||
|
</el-button>
|
||||||
|
</el-card>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.networkMode')">
|
||||||
|
<el-select v-model="form.networkMode" filterable allow-create>
|
||||||
|
<el-option label="default" value="default"></el-option>
|
||||||
|
<el-option label="host" value="host"></el-option>
|
||||||
|
<el-option label="bridge" value="bridge"></el-option>
|
||||||
|
<el-option label="none" value="none"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.otherOption')">
|
||||||
|
<el-checkbox v-model="form.tty">{{ $t('docker.tty') }}</el-checkbox>
|
||||||
|
<el-checkbox v-model="form.openStdin">
|
||||||
|
{{ $t('docker.openStdin') }}
|
||||||
|
</el-checkbox>
|
||||||
|
|
||||||
|
<el-checkbox v-model="form.privileged">
|
||||||
|
{{ $t('docker.privileged') }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.restartPolicy')" prop="restartPolicy">
|
||||||
|
<el-radio-group v-model="form.restartPolicy">
|
||||||
|
<el-radio value="no">{{ $t('docker.noRestart') }}</el-radio>
|
||||||
|
<el-radio value="always">{{ $t('docker.alwaysRestart') }}</el-radio>
|
||||||
|
<el-radio value="on-failure">{{ $t('docker.onFailure') }}</el-radio>
|
||||||
|
<el-radio value="unless-stopped">{{ $t('docker.unlessStopped') }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.cpuShare')" prop="cpuShares">
|
||||||
|
<template #label>
|
||||||
|
<el-row>
|
||||||
|
{{ $t('docker.cpuShare') }}
|
||||||
|
<el-tooltip :content="$t('docker.cpuShareTips')" placement="top">
|
||||||
|
<SvgIcon class="ml-2" name="question-filled" />
|
||||||
|
</el-tooltip>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<el-input v-model.number="form.cpuShares" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="nanoCPUs">
|
||||||
|
<template #label>
|
||||||
|
<el-row>
|
||||||
|
{{ $t('docker.cpuQuota') }}
|
||||||
|
<el-tooltip :content="$t('docker.cpuLimitTips')" placement="top">
|
||||||
|
<SvgIcon class="ml-2" name="question-filled" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-text class="ml-2" size="small">{{ $t('docker.cpuCanUseTips', { cpuTotal: dockerInfo.NCPU }) }}</el-text>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-input v-model.number="form.nanoCpus">
|
||||||
|
<template #append>
|
||||||
|
<div style="width: 35px">{{ $t('docker.core') }}</div>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.memoryLimit')" prop="memory">
|
||||||
|
<template #label>
|
||||||
|
<el-row>
|
||||||
|
{{ $t('docker.memoryLimit') }}
|
||||||
|
<el-tooltip :content="$t('docker.memoryLimitTips')" placement="top">
|
||||||
|
<SvgIcon class="ml-2" name="question-filled" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-text class="ml-2" size="small">{{ $t('docker.memoryCanUseTips', { memTotal: formatByteSize(dockerInfo.MemTotal) }) }}</el-text>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-input v-model.number="form.memory">
|
||||||
|
<template #append><div style="width: 35px">GB</div></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.shmSize')" prop="memory">
|
||||||
|
<el-input v-model.number="form.shmSize">
|
||||||
|
<template #append><div style="width: 35px">GB</div></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="device" :label="$t('docker.device')">
|
||||||
|
<el-card class="mb-1 w-full">
|
||||||
|
<el-table v-if="form.devices.length !== 0" :data="form.devices">
|
||||||
|
<el-table-column :label="$t('docker.driver')" min-width="100">
|
||||||
|
<template #header>
|
||||||
|
{{ $t('docker.driver') }}
|
||||||
|
<el-tooltip :content="$t('docker.driverTips')" placement="top">
|
||||||
|
<SvgIcon class="ml-2 mb-2" name="question-filled" />
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-select v-model="row.driver" filterable allow-create>
|
||||||
|
<el-option v-for="item in runtimeSelect" :key="item" :label="item" :value="item"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('docker.count')" :width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input v-model.number="row.count" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('docker.capabilitie')" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input-tag v-model="row.capabilities" :placeholder="$t('docker.capabilitiePlaceholder')" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('docker.deviceId')" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-input-tag v-model="row.deviceIds" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column min-width="35">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button class="mt-1" link type="primary" @click="handleDevicesDelete(scope.$index)">
|
||||||
|
{{ $t('common.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-button @click="handleDevicesAdd()" size="small">
|
||||||
|
{{ $t('common.add') }}
|
||||||
|
</el-button>
|
||||||
|
</el-card>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('capAdd')" prop="capAdd">
|
||||||
|
<el-input-tag v-model="form.capAdd" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.tag')" prop="labelsStr">
|
||||||
|
<el-input type="textarea" :placeholder="$t('docker.tagTips')" :rows="3" v-model="form.labelsStr" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="$t('docker.envParam')" prop="envStr">
|
||||||
|
<el-input type="textarea" :placeholder="$t('docker.envParamTips')" :rows="3" v-model="form.envsStr" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" :loading="createLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { computed, reactive, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
|
import { dockerApi } from '../api';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
import { Rules } from '@/common/rule';
|
||||||
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
import { deepClone } from '@/common/utils/object';
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [Rules.requiredInput('common.name')],
|
||||||
|
image: [Rules.requiredSelect('docker.image')],
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
host: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultForm = {
|
||||||
|
name: '',
|
||||||
|
image: '',
|
||||||
|
cmdStr: '',
|
||||||
|
forcePull: false,
|
||||||
|
exposedPorts: [] as any,
|
||||||
|
networkMode: 'default',
|
||||||
|
volumes: [] as any,
|
||||||
|
devices: [] as any,
|
||||||
|
capAdd: [] as any,
|
||||||
|
tty: false,
|
||||||
|
openStdin: false,
|
||||||
|
privileged: false,
|
||||||
|
restartPolicy: '',
|
||||||
|
cpuShares: 1024,
|
||||||
|
nanoCpus: 0,
|
||||||
|
memory: 0,
|
||||||
|
shmSize: 0,
|
||||||
|
labelsStr: '',
|
||||||
|
envsStr: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultCommands = ["start.sh jupyter notebook --NotebookApp.token=''"];
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
dockerInfo: {} as any,
|
||||||
|
images: [] as any,
|
||||||
|
form: defaultForm,
|
||||||
|
submitForm: {} as any,
|
||||||
|
pwd: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { dockerInfo, form, submitForm } = toRefs(state);
|
||||||
|
|
||||||
|
//定义事件
|
||||||
|
const emit = defineEmits(['cancel', 'success']);
|
||||||
|
|
||||||
|
const dialogVisible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const formRef = useTemplateRef('formRef');
|
||||||
|
|
||||||
|
const { isFetching: createLoading, execute: createExec } = dockerApi.containerCreate.useApi(submitForm);
|
||||||
|
|
||||||
|
// onMounted(async () => {
|
||||||
|
// init();
|
||||||
|
// });
|
||||||
|
|
||||||
|
watch(dialogVisible, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const runtimeSelect = computed(() => {
|
||||||
|
return state.dockerInfo ? Object.keys(state.dockerInfo?.Runtimes) : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
state.form = deepClone(defaultForm);
|
||||||
|
state.submitForm = {};
|
||||||
|
dockerApi.info.request({ host: props.host }).then((res) => {
|
||||||
|
state.dockerInfo = res;
|
||||||
|
});
|
||||||
|
state.images = await dockerApi.images.request({ host: props.host });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePortsAdd = () => {
|
||||||
|
let item = {
|
||||||
|
host: '',
|
||||||
|
hostIP: '',
|
||||||
|
containerPort: '',
|
||||||
|
hostPort: '',
|
||||||
|
protocol: 'tcp',
|
||||||
|
};
|
||||||
|
state.form.exposedPorts.push(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePortsDelete = (index: number) => {
|
||||||
|
state.form.exposedPorts.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVolumesAdd = () => {
|
||||||
|
let item = {
|
||||||
|
hostDir: '',
|
||||||
|
containerDir: '',
|
||||||
|
mode: 'rw',
|
||||||
|
};
|
||||||
|
state.form.volumes.push(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVolumesDelete = (index: number) => {
|
||||||
|
state.form.volumes.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDevicesAdd = () => {
|
||||||
|
let item = {
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
state.form.devices.push(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDevicesDelete = (index: number) => {
|
||||||
|
state.form.devices.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnOk = async () => {
|
||||||
|
await useI18nFormValidate(formRef);
|
||||||
|
|
||||||
|
state.submitForm = { ...state.form };
|
||||||
|
state.submitForm.host = props.host;
|
||||||
|
|
||||||
|
if (state.submitForm.exposedPorts) {
|
||||||
|
state.submitForm.exposedPorts = state.form.exposedPorts.map((item: any) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
hostPort: item.hostPort + '', // 转为字符串
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.form.envsStr) {
|
||||||
|
state.submitForm.envs = state.form.envsStr.split('\n');
|
||||||
|
}
|
||||||
|
if (state.form.labelsStr) {
|
||||||
|
state.submitForm.labels = state.form.labelsStr.split('\n');
|
||||||
|
}
|
||||||
|
if (state.form.cmdStr) {
|
||||||
|
let itemCmd = splitStringIgnoringQuotes(state.form.cmdStr);
|
||||||
|
const cmds = [];
|
||||||
|
for (const item of itemCmd) {
|
||||||
|
cmds.push(item.replace(/(?<!\\)"/g, '').replaceAll('\\"', '"'));
|
||||||
|
}
|
||||||
|
state.submitForm.cmd = cmds;
|
||||||
|
}
|
||||||
|
await createExec();
|
||||||
|
useI18nOperateSuccessMsg();
|
||||||
|
emit('success', submitForm);
|
||||||
|
cancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitStringIgnoringQuotes = (input: string) => {
|
||||||
|
input = input.replace(/\\"/g, '<quota>');
|
||||||
|
const regex = /"([^"]*)"|(\S+)/g;
|
||||||
|
const result = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(input)) !== null) {
|
||||||
|
if (match[1]) {
|
||||||
|
result.push(match[1].replaceAll('<quota>', '\\"'));
|
||||||
|
} else if (match[2]) {
|
||||||
|
result.push(match[2].replaceAll('<quota>', '\\"'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
354
frontend/src/views/ops/docker/container/ContainerList.vue
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card !p-2">
|
||||||
|
<el-row justify="space-between">
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-row :gutter="5">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-input :placeholder="$t('docker.containerName')" v-model="params.name" plain clearable></el-input>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<EnumSelect v-model="params.state" :enums="ContainerStateEnum" :placeholder="$t('docker.status')" clearable />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-button @click="getContainers" type="primary" icon="refresh" circle plain></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-row justify="end">
|
||||||
|
<el-button @click="openContainerCreate" type="success" icon="plus" plain>{{ $t('docker.createContainer') }}</el-button>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="filterTableDatas" v-loading="state.loadingContainers">
|
||||||
|
<el-table-column prop="name" :label="$t('docker.name')" :min-width="120" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column prop="imageName" :label="$t('docker.image')" :min-width="150" show-overflow-tooltip> </el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="state" :label="$t('common.status')" :min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-dropdown @command="handleCommand">
|
||||||
|
<el-button :type="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.tag.type" round plain size="small">
|
||||||
|
{{ $t(EnumValue.getLabelByValue(ContainerStateEnum, row.state)) || '-' }}
|
||||||
|
<SvgIcon class="ml-1" :name="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.extra.icon" />
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item :command="{ type: 'restart', row }">
|
||||||
|
{{ $t('docker.restart') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
|
||||||
|
<el-dropdown-item :disabled="row.state == ContainerStateEnum.Stop.value" :command="{ type: 'stop', row }">
|
||||||
|
{{ $t('docker.stop') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column v-loading="true" prop="stats" :label="$t('docker.stats')" :min-width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<SvgIcon v-if="getLoadingState(row.containerId)" class="is-loading" name="loading" color="var(--el-color-primary)" />
|
||||||
|
|
||||||
|
<span v-else-if="row.stats">
|
||||||
|
<el-row>
|
||||||
|
<el-text size="small" class="font11">
|
||||||
|
CPU:
|
||||||
|
<span>{{ row.stats.cpuPercent.toFixed(2) }}%</span>
|
||||||
|
</el-text>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-text size="small" class="font11">
|
||||||
|
{{ $t('docker.memory') }}:
|
||||||
|
<span>{{ row.stats.memoryPercent.toFixed(2) }}%</span>
|
||||||
|
</el-text>
|
||||||
|
|
||||||
|
<el-popover placement="right" :width="300" trigger="hover">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-statistic :title="$t('CPU使用')" :value="row.stats.cpuTotalUsage" :formatter="formatCpuValue" :precision="2">
|
||||||
|
</el-statistic>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-statistic :title="$t('CPU总计')" :value="row.stats.systemUsage" :formatter="formatCpuValue" :precision="2">
|
||||||
|
</el-statistic>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-statistic :title="$t('内存使用')" :value="row.stats.memoryUsage" :formatter="formatByteSize" :precision="2">
|
||||||
|
</el-statistic>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-statistic :title="$t('内存限额')" :value="row.stats.memoryLimit" :formatter="formatByteSize" :precision="2">
|
||||||
|
</el-statistic>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-popover>
|
||||||
|
</el-row>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="networks" :label="$t('docker.ip')" :min-width="90">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-for="network in scope.row.networks" :key="network" type="primary">{{ network || '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="ports" :label="$t('machine.port')" :min-width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-for="port in scope.row.ports" :key="port" type="primary">{{ port }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createTime" :label="$t('common.createTime')" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="运行时长" :min-width="120"> </el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('common.operation')" :min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-row>
|
||||||
|
<el-button @click="openTerminal(row)" :disabled="row.state != ContainerStateEnum.Running.value" type="primary" link plain> SSH </el-button>
|
||||||
|
|
||||||
|
<el-button @click="openLog(row)" type="success" link plain>{{ $t('docker.log') }}</el-button>
|
||||||
|
|
||||||
|
<el-dropdown @command="handleCommand">
|
||||||
|
<el-button type="primary" link plain class="ml-3"> {{ $t('common.more') }} <SvgIcon name="arrow-down" :size="12" /> </el-button>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item :command="{ type: 'remove', row }">
|
||||||
|
{{ $t('common.delete') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-if="terminalDialog.visible"
|
||||||
|
:title="terminalDialog.title"
|
||||||
|
v-model="terminalDialog.visible"
|
||||||
|
width="80%"
|
||||||
|
body-class="h-[65vh]"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:modal="false"
|
||||||
|
@close="closeTerminal"
|
||||||
|
draggable
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(params.host, terminalDialog.containerId)" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<ContainerLog v-model:visible="logDialog.visible" :host="params.host" :container-id="logDialog.containerId" />
|
||||||
|
|
||||||
|
<ContainerCreate v-model:visible="containerCreateDialog.visible" :host="params.host" @success="getContainers" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, onMounted, reactive, toRefs } from 'vue';
|
||||||
|
import { dockerApi, getDockerExecSocketUrl } from '../api';
|
||||||
|
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||||
|
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||||
|
import { ContainerStateEnum } from '../enums';
|
||||||
|
import { fuzzyMatchField } from '@/common/utils/string';
|
||||||
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
|
import { useI18nConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { useDataState } from '@/hooks/useDataState';
|
||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
|
||||||
|
const ContainerLog = defineAsyncComponent(() => import('./ContainerLog.vue'));
|
||||||
|
const ContainerCreate = defineAsyncComponent(() => import('./ContainerCreate.vue'));
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
host: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
params: {
|
||||||
|
host: props.host,
|
||||||
|
name: '',
|
||||||
|
state: null,
|
||||||
|
},
|
||||||
|
loadingContainers: false,
|
||||||
|
containers: [],
|
||||||
|
terminalDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
containerId: '',
|
||||||
|
},
|
||||||
|
logDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
containerId: '',
|
||||||
|
},
|
||||||
|
containerCreateDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
containerId: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { params, terminalDialog, logDialog, containerCreateDialog } = toRefs(state);
|
||||||
|
|
||||||
|
// 容器状态加载状态,key -> containerId, value -> loading
|
||||||
|
const { setState: setLoadingState, getState: getLoadingState } = useDataState<string, boolean>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getContainers();
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterTableDatas = computed(() => {
|
||||||
|
let tables: any = state.containers;
|
||||||
|
const nameSearch = state.params.name;
|
||||||
|
const stateSearch = state.params.state;
|
||||||
|
|
||||||
|
if (stateSearch) {
|
||||||
|
tables = tables.filter((table: any) => {
|
||||||
|
return table.state === stateSearch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameSearch) {
|
||||||
|
tables = fuzzyMatchField(nameSearch, tables, (table: any) => table.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getContainers = async () => {
|
||||||
|
state.loadingContainers = true;
|
||||||
|
try {
|
||||||
|
state.containers = await dockerApi.containers.request(state.params);
|
||||||
|
setContainersStats();
|
||||||
|
} finally {
|
||||||
|
state.loadingContainers = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setContainersStats = () => {
|
||||||
|
if (state.containers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.containers.forEach((container: any) => {
|
||||||
|
if (container.state === ContainerStateEnum.Running.value) {
|
||||||
|
setLoadingState(container.containerId, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dockerApi.containersStats
|
||||||
|
.request(state.params)
|
||||||
|
.then((res) => {
|
||||||
|
state.containers.forEach((container: any) => {
|
||||||
|
const stats = res.find((stat: any) => stat.containerId === container.containerId);
|
||||||
|
if (stats) {
|
||||||
|
container.stats = stats;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
state.containers.forEach((container: any) => {
|
||||||
|
if (container.state === ContainerStateEnum.Running.value) {
|
||||||
|
setLoadingState(container.containerId, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerRestart = async (param: any) => {
|
||||||
|
await dockerApi.containerRestart.request({ host: state.params.host, containerId: param.containerId });
|
||||||
|
useI18nOperateSuccessMsg();
|
||||||
|
getContainers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStop = async (param: any) => {
|
||||||
|
await useI18nConfirm('docker.stopContainerConfirm', { name: param.name });
|
||||||
|
await dockerApi.containerStop.request({ host: state.params.host, containerId: param.containerId });
|
||||||
|
useI18nOperateSuccessMsg();
|
||||||
|
getContainers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerRemove = async (param: any) => {
|
||||||
|
await useI18nConfirm('docker.removeContainerConfirm', { name: param.name });
|
||||||
|
await dockerApi.containerRemove.request({ host: state.params.host, containerId: param.containerId });
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
getContainers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTerminal = (row: any) => {
|
||||||
|
state.terminalDialog.containerId = row.containerId;
|
||||||
|
state.terminalDialog.title = `Terminal - ${row.name}`;
|
||||||
|
state.terminalDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTerminal = () => {
|
||||||
|
state.terminalDialog.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLog = (row: any) => {
|
||||||
|
state.logDialog.containerId = row.containerId;
|
||||||
|
state.logDialog.title = `Log - ${row.name}`;
|
||||||
|
state.logDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openUrl = (row: any) => {
|
||||||
|
const port = row.ports[0];
|
||||||
|
window.open('http://' + props.host.split('//')[1].split(':')[0] + ':' + port.split('->')[0]?.split(':')[1] + '/lab');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCommand = async (commond: any) => {
|
||||||
|
const row = commond.row;
|
||||||
|
const type = commond.type;
|
||||||
|
switch (type) {
|
||||||
|
case 'restart': {
|
||||||
|
containerRestart({ containerId: row.containerId, name: row.name });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'stop': {
|
||||||
|
containerStop({ containerId: row.containerId, name: row.name });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'remove': {
|
||||||
|
containerRemove({ containerId: row.containerId, name: row.name });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openContainerCreate = () => {
|
||||||
|
state.containerCreateDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatCpuValue(t: number) {
|
||||||
|
const num = 1000;
|
||||||
|
if (t < num) return t + ' ns';
|
||||||
|
if (t < Math.pow(num, 2)) return Number((t / num).toFixed(2)) + ' μs';
|
||||||
|
if (t < Math.pow(num, 3)) return Number((t / Math.pow(num, 2)).toFixed(2)) + ' ms';
|
||||||
|
return Number((t / Math.pow(num, 3)).toFixed(2)) + ' s';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
77
frontend/src/views/ops/docker/container/ContainerLog.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-drawer title="logs" v-model="visible" @close="close" :destroy-on-close="true" :close-on-click-modal="true" size="60%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="props.host" :back="() => (visible = false)">
|
||||||
|
<template #extra>
|
||||||
|
<div class="mr20"></div>
|
||||||
|
</template>
|
||||||
|
</DrawerHeader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-select @change="searchLog" v-model.number="state.tail">
|
||||||
|
<template #prefix>{{ $t('docker.lines') }}</template>
|
||||||
|
<el-option :value="100" :label="100" />
|
||||||
|
<el-option :value="200" :label="200" />
|
||||||
|
<el-option :value="500" :label="500" />
|
||||||
|
<el-option :value="1000" :label="1000" />
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-checkbox @change="searchLog" border v-model="state.isWatch">
|
||||||
|
{{ $t('docker.follow') }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<RealLogViewer ref="realLogViewerRef" :ws-url="wsUrl" height="calc(100vh - 200px)" />
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, reactive, useTemplateRef } from 'vue';
|
||||||
|
import RealLogViewer from '@/components/monaco/RealLogViewer.vue';
|
||||||
|
import { getContainerLogSocketUrl } from '../api';
|
||||||
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
host: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
containerId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = defineModel<boolean>('visible');
|
||||||
|
|
||||||
|
const realLogViewerRef = useTemplateRef('realLogViewerRef');
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
since: '',
|
||||||
|
tail: 100,
|
||||||
|
isWatch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const wsUrl = computed(
|
||||||
|
() => `${getContainerLogSocketUrl(props.host, props.containerId)}&tail=${state.tail}&follow=${state.isWatch ? '1' : '0'}&since=${state.since}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchLog = () => {
|
||||||
|
realLogViewerRef.value?.reload(wsUrl.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
state.tail = 100;
|
||||||
|
state.since = '';
|
||||||
|
state.isWatch = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
11
frontend/src/views/ops/docker/enums.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { EnumValue } from '@/common/Enum';
|
||||||
|
|
||||||
|
export const ContainerStateEnum = {
|
||||||
|
Running: EnumValue.of('running', 'docker.running').tagTypeSuccess().setExtra({ icon: 'VideoPlay' }),
|
||||||
|
Stop: EnumValue.of('exited', 'docker.stopped').tagTypeDanger().setExtra({ icon: 'VideoPause' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageStateEnum = {
|
||||||
|
Used: EnumValue.of(true, '已使用').tagTypeSuccess(),
|
||||||
|
UnUsed: EnumValue.of(false, '未使用').tagTypeInfo(),
|
||||||
|
};
|
||||||
209
frontend/src/views/ops/docker/image/ImageList.vue
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card !p-2">
|
||||||
|
<el-row :gutter="5">
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-input :placeholder="$t('docker.imageName')" v-model="params.name" plain clearable></el-input>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="4">
|
||||||
|
<EnumSelect v-model="params.state" :enums="ImageStateEnum" :placeholder="$t('docker.status')" clearable />
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-button @click="getImages" type="primary" icon="refresh" circle plain></el-button>
|
||||||
|
<el-upload :on-success="uploadSuccess" action="" :http-request="uploadImage" :headers="{ token }" :show-file-list="false" name="file">
|
||||||
|
<el-button type="primary" icon="upload" circle plain></el-button>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="filterTableDatas" v-loading="state.loadingImages">
|
||||||
|
<el-table-column prop="id" label="ID" :min-width="100" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" :underline="false">
|
||||||
|
{{ row.id.split(':')[1].substring(0, 12) }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="tags" :label="$t('docker.tag')" :min-width="250">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-for="tag in row.tags" :key="tag" type="primary">{{ tag || '-' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="size" :label="$t('docker.size')" :min-width="50">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatByteSize(row.size) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createTime" :label="$t('common.createTime')" width="160">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="isUse" :label="$t('common.status')" :min-width="50">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<EnumTag :enums="ImageStateEnum" :value="row.isUse" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('common.operation')" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button @click="exportImage(row)" type="warning" link plain>{{ $t('docker.export') }}</el-button>
|
||||||
|
|
||||||
|
<el-popconfirm :title="$t('docker.stopImageConfirm')" @confirm="imageRemove(row)" width="170">
|
||||||
|
<template #reference>
|
||||||
|
<el-button :disabled="row.isUse == ImageStateEnum.Used.value" type="danger" link plain>
|
||||||
|
{{ $t('common.delete') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-if="terminalDialog.visible"
|
||||||
|
:title="terminalDialog.title"
|
||||||
|
v-model="terminalDialog.visible"
|
||||||
|
width="80%"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:modal="false"
|
||||||
|
@close="closeTerminal"
|
||||||
|
draggable
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(params.host, terminalDialog.containerId)" height="560px" />
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, reactive, toRefs } from 'vue';
|
||||||
|
import { dockerApi, getDockerExecSocketUrl } from '../api';
|
||||||
|
import { formatByteSize, formatDate } from '@/common/utils/format';
|
||||||
|
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||||
|
import { ImageStateEnum } from '../enums';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import { fuzzyMatchField } from '@/common/utils/string';
|
||||||
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
|
import config from '@/common/config';
|
||||||
|
import { joinClientParams } from '@/common/request';
|
||||||
|
import { getToken } from '@/common/utils/storage';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
host: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
params: {
|
||||||
|
host: props.host,
|
||||||
|
name: '',
|
||||||
|
state: null,
|
||||||
|
},
|
||||||
|
loadingImages: false,
|
||||||
|
images: [],
|
||||||
|
terminalDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
containerId: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { params, terminalDialog } = toRefs(state);
|
||||||
|
|
||||||
|
const token = getToken();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getImages();
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterTableDatas = computed(() => {
|
||||||
|
let tables: any = state.images;
|
||||||
|
const nameSearch = state.params.name;
|
||||||
|
const stateSearch = state.params.state;
|
||||||
|
|
||||||
|
if (stateSearch != null) {
|
||||||
|
tables = tables.filter((table: any) => {
|
||||||
|
return table.isUse === stateSearch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameSearch) {
|
||||||
|
tables = fuzzyMatchField(nameSearch, tables, (table: any) => table.tags[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getImages = async () => {
|
||||||
|
state.loadingImages = true;
|
||||||
|
try {
|
||||||
|
state.images = await dockerApi.images.request(state.params);
|
||||||
|
} finally {
|
||||||
|
state.loadingImages = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportImage = async (row: any) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('href', `${config.baseApiUrl}/docker/images/save?host=${state.params.host}&tag=${row.tags[0]}&${joinClientParams()}`);
|
||||||
|
a.setAttribute('target', '_blank');
|
||||||
|
a.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadImage = (content: any) => {
|
||||||
|
const params = new FormData();
|
||||||
|
// const path = state.nowPath;
|
||||||
|
params.append('file', content.file);
|
||||||
|
params.append('host', state.params.host);
|
||||||
|
params.append('token', token);
|
||||||
|
dockerApi.imageUpload
|
||||||
|
.xhrReq(params, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
|
||||||
|
// onUploadProgress: onUploadProgress,
|
||||||
|
timeout: 3 * 60 * 60 * 1000,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success(i18n.global.t('machine.uploadSuccess'));
|
||||||
|
setTimeout(() => {
|
||||||
|
getImages();
|
||||||
|
}, 3000);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// state.uploadProgressShow = false;
|
||||||
|
});
|
||||||
|
ElMessage.info(i18n.global.t('docker.imageUploading'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadSuccess = (res: any) => {
|
||||||
|
if (res.code !== 200) {
|
||||||
|
ElMessage.error(res.msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageRemove = async (row: any) => {
|
||||||
|
await dockerApi.imageRemove.request({ host: state.params.host, imageId: row.id });
|
||||||
|
getImages();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTerminal = (row: any) => {
|
||||||
|
state.terminalDialog.containerId = row.containerId;
|
||||||
|
state.terminalDialog.title = `Terminal - ${row.name}`;
|
||||||
|
state.terminalDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTerminal = () => {
|
||||||
|
state.terminalDialog.visible = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
3
frontend/src/views/ops/docker/route.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
Container: () => import('@/views/ops/docker/DockerPanel.vue'),
|
||||||
|
};
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
lazy
|
lazy
|
||||||
>
|
>
|
||||||
<template #tableHeader>
|
<template #tableHeader>
|
||||||
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">{{ $t('common.create') }}</el-button>
|
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)" plain>{{ $t('common.create') }}</el-button>
|
||||||
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">
|
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete" plain>
|
||||||
{{ $t('common.delete') }}
|
{{ $t('common.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{{ t('es.dashboard.nodes') }}
|
{{ t('es.dashboard.nodes') }}
|
||||||
<el-button v-if="state.tabName === 'nodesStats'" icon="refresh" @click="fetchNodesStats" link type="primary" />
|
<el-button v-if="state.tabName === 'nodesStats'" icon="refresh" @click="fetchNodesStats" link type="primary" />
|
||||||
</template>
|
</template>
|
||||||
<el-descriptions class="nodes-num" column="3" border>
|
<el-descriptions class="nodes-num" :column="3" border>
|
||||||
<el-descriptions-item label="total">
|
<el-descriptions-item label="total">
|
||||||
{{ state.nodesStats._nodes?.total }}
|
{{ state.nodesStats._nodes?.total }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -52,8 +52,8 @@
|
|||||||
<el-progress
|
<el-progress
|
||||||
striped
|
striped
|
||||||
striped-flow
|
striped-flow
|
||||||
duration="50"
|
:duration="50"
|
||||||
style="width: 100%"
|
class="w-full"
|
||||||
:percentage="node.os.mem.used_percent"
|
:percentage="node.os.mem.used_percent"
|
||||||
:color="getPercentColor(node.os.mem.used_percent)"
|
:color="getPercentColor(node.os.mem.used_percent)"
|
||||||
/>
|
/>
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
<el-progress
|
<el-progress
|
||||||
striped
|
striped
|
||||||
striped-flow
|
striped-flow
|
||||||
duration="50"
|
:duration="50"
|
||||||
style="width: 100%"
|
class="w-full"
|
||||||
:percentage="node.jvm.mem.heap_used_percent"
|
:percentage="node.jvm.mem.heap_used_percent"
|
||||||
:color="getPercentColor(node.jvm.mem.heap_used_percent)"
|
:color="getPercentColor(node.jvm.mem.heap_used_percent)"
|
||||||
/>
|
/>
|
||||||
@@ -75,8 +75,8 @@
|
|||||||
<el-progress
|
<el-progress
|
||||||
striped
|
striped
|
||||||
striped-flow
|
striped-flow
|
||||||
duration="50"
|
:duration="50"
|
||||||
style="width: 100%"
|
class="w-full"
|
||||||
:percentage="node.os.cpu.percent"
|
:percentage="node.os.cpu.percent"
|
||||||
:color="getPercentColor(node.os.cpu.percent)"
|
:color="getPercentColor(node.os.cpu.percent)"
|
||||||
/>
|
/>
|
||||||
@@ -88,8 +88,8 @@
|
|||||||
<el-progress
|
<el-progress
|
||||||
striped
|
striped
|
||||||
striped-flow
|
striped-flow
|
||||||
duration="50"
|
:duration="50"
|
||||||
style="width: 100%"
|
class="w-full"
|
||||||
:percentage="
|
:percentage="
|
||||||
Math.round(((node.fs.total.total_in_bytes - node.fs.total.free_in_bytes) * 100) / node.fs.total.total_in_bytes)
|
Math.round(((node.fs.total.total_in_bytes - node.fs.total.free_in_bytes) * 100) / node.fs.total.total_in_bytes)
|
||||||
"
|
"
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('es.dashboard.text')" required prop="text">
|
<el-form-item :label="t('es.dashboard.text')" required prop="text">
|
||||||
<el-input type="textarea" rows="5" v-model="state.analyze.text" />
|
<el-input type="textarea" :rows="5" v-model="state.analyze.text" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-button @click="onAnalyze" :loading="state.analyze.loading">{{ t('es.dashboard.startAnalyze') }}</el-button>
|
<el-button @click="onAnalyze" :loading="state.analyze.loading">{{ t('es.dashboard.startAnalyze') }}</el-button>
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ const defaultSearch = {
|
|||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instId: string;
|
instId: number;
|
||||||
idxName: string;
|
idxName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1034
frontend/src/views/ops/es/resource/EsDataOp.vue
Normal file
30
frontend/src/views/ops/es/resource/NodeEs.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #prefix="{ data }">
|
||||||
|
<el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon name="icon es/es-color" />
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ data.params.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="version">
|
||||||
|
{{ data.params.version }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="uri">
|
||||||
|
{{ `${data.params.host}:${data.params.port}` }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
28
frontend/src/views/ops/es/resource/NodeEsIndex.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #prefix="{ data }">
|
||||||
|
<el-popover placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon name="Document" />
|
||||||
|
</template>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ data.params.idxName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('es.size')">
|
||||||
|
{{ data.params.size }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('es.docs')">
|
||||||
|
{{ data.params.idx['docs.count'] }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
175
frontend/src/views/ops/es/resource/index.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
import { esApi } from '@/views/ops/es/api';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { NodeType, TagTreeNode, ResourceComponentConfig } from '@/views/ops/component/tag';
|
||||||
|
import { ResourceConfig } from '../../component/tag';
|
||||||
|
|
||||||
|
const Icon = {
|
||||||
|
name: ResourceTypeEnum.Es.extra.icon,
|
||||||
|
color: ResourceTypeEnum.Es.extra.iconColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EsInstanceList = defineAsyncComponent(() => import('../EsInstanceList.vue'));
|
||||||
|
const EsDataOp = defineAsyncComponent(() => import('./EsDataOp.vue'));
|
||||||
|
|
||||||
|
const NodeEs = defineAsyncComponent(() => import('./NodeEs.vue'));
|
||||||
|
const NodeEsIndex = defineAsyncComponent(() => import('./NodeEsIndex.vue'));
|
||||||
|
|
||||||
|
export const EsOpComp: ResourceComponentConfig = {
|
||||||
|
name: 'tag.esDataOp',
|
||||||
|
component: EsDataOp,
|
||||||
|
icon: Icon,
|
||||||
|
};
|
||||||
|
|
||||||
|
// tagpath 节点类型
|
||||||
|
const NodeTypeEsTag = new NodeType(TagTreeNode.TagPath)
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('refresh', 'common.refresh')
|
||||||
|
.withIcon('refresh')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).reloadNode(nodeData.key)),
|
||||||
|
])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
parentNode.ctx?.addResourceComponent(EsOpComp);
|
||||||
|
// 加载es实例列表
|
||||||
|
const res = await esApi.instances.request({ tagPath: parentNode.params.tagPath });
|
||||||
|
if (!res.total) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const insts = res.list;
|
||||||
|
await sleep(100);
|
||||||
|
return insts?.map((x: any) => {
|
||||||
|
x.tagPath = parentNode.key;
|
||||||
|
return TagTreeNode.new(parentNode, `es.inst.${x.code}`, x.name, NodeTypeInst).withNodeComponent(NodeEs).withParams(x);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载实例列表
|
||||||
|
const NodeTypeInst = new NodeType(1)
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
|
||||||
|
let oiKey = `es.${params.id}.opIndex`;
|
||||||
|
let bsKey = `es.${params.id}.opBasicSearch`;
|
||||||
|
let ssKey = `es.${params.id}.opSeniorSearch`;
|
||||||
|
let dbKey = `es.${params.id}.opDashboard`;
|
||||||
|
let stKey = `es.${params.id}.opSettings`;
|
||||||
|
let tpKey = `es.${params.id}.optemplates`;
|
||||||
|
|
||||||
|
let nodeParams = { inst: params, instId: params.id };
|
||||||
|
|
||||||
|
return [
|
||||||
|
TagTreeNode.new(parentNode, oiKey, i18n.global.t('es.opIndex'), NodeTypeIndexs).withParams(nodeParams).withIcon({ name: 'Document' }),
|
||||||
|
// new TagTreeNode(ssKey, t('es.opSeniorSearch'), NodeTypeSeniorSearch).withParams(nodeParams).withIsLeaf(true),
|
||||||
|
// new TagTreeNode(dbKey, t('es.opDashboard'), NodeTypeDashboard).withParams(nodeParams).withIsLeaf(true),
|
||||||
|
// new TagTreeNode(stKey, t('es.opSettings'), NodeTypeSettings).withParams(nodeParams),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.withNodeClickFunc(async (nodeData: TagTreeNode) => {
|
||||||
|
// 添加一个dashboard tab
|
||||||
|
(await nodeData.ctx?.addResourceComponent(EsOpComp)).onInstClick(nodeData);
|
||||||
|
});
|
||||||
|
|
||||||
|
const NodeTypeIndexs = new NodeType(2)
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('refresh', 'common.refresh')
|
||||||
|
.withIcon('refresh')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) =>
|
||||||
|
(await nodeData.ctx?.addResourceComponent(EsOpComp)).onRefreshIndices(nodeData.params.instId, nodeData.key)
|
||||||
|
),
|
||||||
|
new ContextmenuItem('addIndex', 'es.contextmenu.index.addIndex')
|
||||||
|
.withIcon('plus')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onAddIndex(nodeData)),
|
||||||
|
new ContextmenuItem('showSys', 'es.contextmenu.index.showSys')
|
||||||
|
.withIcon('View')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onShowSysIndex(nodeData)),
|
||||||
|
new ContextmenuItem('idxTemplate', 'es.templates')
|
||||||
|
.withIcon('DocumentCopy')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onShowTemplate(nodeData)),
|
||||||
|
])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
console.log(params);
|
||||||
|
// 展示索引列表,显示索引名,文档总数
|
||||||
|
// 加载索引列表
|
||||||
|
let indicesRes = await (await parentNode.ctx?.addResourceComponent(EsOpComp)).loadIdxs(params);
|
||||||
|
|
||||||
|
let idxNodes = [];
|
||||||
|
for (let idx of indicesRes) {
|
||||||
|
idxNodes.push(
|
||||||
|
TagTreeNode.new(parentNode, idx.key, idx.idxName, NodeTypeIndex)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({
|
||||||
|
parentKey: parentNode.key,
|
||||||
|
...idx,
|
||||||
|
})
|
||||||
|
.withNodeComponent(NodeEsIndex)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return idxNodes;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 索引操作
|
||||||
|
const NodeTypeIndex = new NodeType(3)
|
||||||
|
.withContextMenuItems([
|
||||||
|
// 右键菜单支持:复制名字、新增别名、迁移索引、关闭、启用、删除、数据浏览、跳转基础查询、跳转高级查询
|
||||||
|
new ContextmenuItem('copyName', 'es.contextmenu.index.copyName')
|
||||||
|
.withIcon('copyDocument')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxCopyName(nodeData)),
|
||||||
|
new ContextmenuItem('refresh', 'es.contextmenu.index.refresh')
|
||||||
|
.withIcon('refresh')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onRefreshIdx(nodeData)),
|
||||||
|
new ContextmenuItem('clearCache', 'es.contextmenu.index.clearCache')
|
||||||
|
.withIcon('refresh')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onClearIdxCache(nodeData)),
|
||||||
|
new ContextmenuItem('flush', 'es.contextmenu.index.flush')
|
||||||
|
.withIcon('refresh')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onFlushIdx(nodeData)),
|
||||||
|
new ContextmenuItem('Reindex', 'es.Reindex')
|
||||||
|
.withIcon('Switch')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxReindex(nodeData)),
|
||||||
|
new ContextmenuItem('Close', 'es.contextmenu.index.Close')
|
||||||
|
.withIcon('Close')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxClose(nodeData))
|
||||||
|
.withHideFunc((data: any) => {
|
||||||
|
return data.params.idx.status !== 'open';
|
||||||
|
}),
|
||||||
|
new ContextmenuItem('Open', 'es.contextmenu.index.Open')
|
||||||
|
.withIcon('Select')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxOpen(nodeData))
|
||||||
|
.withHideFunc((data: any) => {
|
||||||
|
return data.params.idx.status === 'open';
|
||||||
|
}),
|
||||||
|
new ContextmenuItem('Delete', 'es.contextmenu.index.Delete')
|
||||||
|
.withIcon('Delete')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxDelete(nodeData)),
|
||||||
|
new ContextmenuItem('BaseSearch', 'es.contextmenu.index.BaseSearch')
|
||||||
|
.withIcon('Search')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxBaseSearch(nodeData)),
|
||||||
|
// new ContextmenuItem('SeniorSearch', 'es.contextmenu.index.SeniorSearch').withIcon('Search').withOnClick((data: any) => onIdxSeniorSearch(data)),
|
||||||
|
new ContextmenuItem('IndexDetail', 'es.indexDetail')
|
||||||
|
.withIcon('InfoFilled')
|
||||||
|
.withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIndexDetail(nodeData)),
|
||||||
|
])
|
||||||
|
|
||||||
|
.withNodeClickFunc(async (nodeData: TagTreeNode) => {
|
||||||
|
const params = nodeData.params;
|
||||||
|
(await nodeData.ctx?.addResourceComponent(EsOpComp)).loadIndexData(params.params.inst.id, params);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: 5,
|
||||||
|
resourceType: TagResourceTypeEnum.EsInstance.value,
|
||||||
|
rootNodeType: NodeTypeEsTag,
|
||||||
|
manager: {
|
||||||
|
componentConf: {
|
||||||
|
component: EsInstanceList,
|
||||||
|
icon: Icon,
|
||||||
|
name: 'tag.es',
|
||||||
|
},
|
||||||
|
countKey: 'es',
|
||||||
|
permCode: 'es:instance:save',
|
||||||
|
},
|
||||||
|
} as ResourceConfig;
|
||||||
@@ -1,4 +1 @@
|
|||||||
export default {
|
export default {};
|
||||||
EsInstanceList: () => import('@/views/ops/es/EsInstanceList.vue'),
|
|
||||||
EsOperation: () => import('@/views/ops/es/EsOperation.vue'),
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
>
|
>
|
||||||
<template #tableHeader>
|
<template #tableHeader>
|
||||||
<el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>{{ $t('common.create') }} </el-button>
|
<el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>{{ $t('common.create') }} </el-button>
|
||||||
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">
|
<el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete" plain>
|
||||||
{{ $t('common.delete') }}
|
{{ $t('common.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,595 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="h-full">
|
|
||||||
<ResourceOpPanel @resize="onResizeTagTree">
|
|
||||||
<template #left>
|
|
||||||
<tag-tree
|
|
||||||
ref="tagTreeRef"
|
|
||||||
:resource-type="TagResourceTypePath.MachineAuthCert"
|
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
|
||||||
:default-expanded-keys="state.defaultExpendKey"
|
|
||||||
>
|
|
||||||
<template #prefix="{ data }">
|
|
||||||
<SvgIcon
|
|
||||||
v-if="data.icon && data.params.status == 1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
|
|
||||||
:name="data.icon.name"
|
|
||||||
:color="data.icon.color"
|
|
||||||
/>
|
|
||||||
<SvgIcon
|
|
||||||
v-if="data.icon && data.params.status == -1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
|
|
||||||
:name="data.icon.name"
|
|
||||||
color="var(--el-color-danger)"
|
|
||||||
/>
|
|
||||||
<SvgIcon v-if="data.icon && data.params.protocol != MachineProtocolEnum.Ssh.value" :name="data.icon.name" :color="data.icon.color" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #suffix="{ data }">
|
|
||||||
<span v-if="data.type.value == MachineNodeType.AuthCert">{{
|
|
||||||
` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}`
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
</tag-tree>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
|
||||||
<el-card class="h-full" body-class="machine-terminal-tabs h-full !p-1 flex flex-col flex-1">
|
|
||||||
<el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" v-model="state.activeTermName" class="!h-full w-full">
|
|
||||||
<el-tab-pane class="!h-full flex flex-col" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
|
||||||
<template #label>
|
|
||||||
<el-popconfirm @confirm="handleReconnect(dt, true)" :title="$t('machine.reConnTips')">
|
|
||||||
<template #reference>
|
|
||||||
<el-icon
|
|
||||||
class="mr-1"
|
|
||||||
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
|
|
||||||
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : $t('machine.clickReConn')"
|
|
||||||
><Connection />
|
|
||||||
</el-icon>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
|
||||||
<template #reference>
|
|
||||||
<div>
|
|
||||||
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<el-descriptions :column="1" size="small">
|
|
||||||
<el-descriptions-item :label="$t('common.name')"> {{ dt.params?.name }} </el-descriptions-item>
|
|
||||||
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
|
||||||
<el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
|
|
||||||
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div :ref="(el: any) => setTerminalWrapperRef(el, dt.key)" class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
|
|
||||||
<TerminalBody
|
|
||||||
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
|
||||||
:mount-init="false"
|
|
||||||
@status-change="terminalStatusChange(dt.key, $event)"
|
|
||||||
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
|
||||||
:socket-url="dt.socketUrl"
|
|
||||||
/>
|
|
||||||
<machine-rdp
|
|
||||||
v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
|
|
||||||
:machine-id="dt.params.id"
|
|
||||||
:auth-cert="dt.authCert"
|
|
||||||
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
|
||||||
@status-change="terminalStatusChange(dt.key, $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
|
|
||||||
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
|
|
||||||
<el-descriptions :title="$t('common.detail')" :column="3" border>
|
|
||||||
<el-descriptions-item :span="1.5" label="ID">{{ infoDialog.data.id }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1.5" :label="$t('common.name')">{{ infoDialog.data.name }}</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="3" :label="$t('tag.relateTag')">
|
|
||||||
<ResourceTags :tags="infoDialog.data.tags" />
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" :label="$t('machine.port')">{{ infoDialog.data.port }}</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ infoDialog.data.remark }}</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="1.5" :label="$t('machine.sshTunnel')"
|
|
||||||
>{{ infoDialog.data.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')"
|
|
||||||
>{{ infoDialog.data.enableRecorder == 1 ? $t('common.yes') : $t('common.no') }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="2" :label="$t('common.createTime')">
|
|
||||||
{{ formatDate(infoDialog.data.createTime) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" :label="$t('common.creator')">
|
|
||||||
{{ infoDialog.data.creator }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
|
|
||||||
<el-descriptions-item :span="2" :label="$t('common.updateTime')">
|
|
||||||
{{ formatDate(infoDialog.data.updateTime) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" :label="$t('common.modifier')">
|
|
||||||
{{ infoDialog.data.modifier }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
|
||||||
|
|
||||||
<script-manage
|
|
||||||
:title="serviceDialog.title"
|
|
||||||
v-model:visible="serviceDialog.visible"
|
|
||||||
v-model:machineId="serviceDialog.machineId"
|
|
||||||
:auth-cert-name="serviceDialog.authCertName"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<file-conf-list
|
|
||||||
:title="fileDialog.title"
|
|
||||||
:auth-cert-name="fileDialog.authCertName"
|
|
||||||
v-model:visible="fileDialog.visible"
|
|
||||||
v-model:machineId="fileDialog.machineId"
|
|
||||||
:protocol="fileDialog.protocol"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
destroy-on-close
|
|
||||||
:title="state.filesystemDialog.title"
|
|
||||||
v-model="state.filesystemDialog.visible"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
width="70%"
|
|
||||||
>
|
|
||||||
<machine-file
|
|
||||||
:machine-id="state.filesystemDialog.machineId"
|
|
||||||
:auth-cert-name="state.filesystemDialog.authCertName"
|
|
||||||
:protocol="state.filesystemDialog.protocol"
|
|
||||||
:file-id="state.filesystemDialog.fileId"
|
|
||||||
:path="state.filesystemDialog.path"
|
|
||||||
/>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
|
||||||
|
|
||||||
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
|
|
||||||
</el-card>
|
|
||||||
</template>
|
|
||||||
</ResourceOpPanel>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { defineAsyncComponent, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { getMachineTerminalSocketUrl, machineApi } from './api';
|
|
||||||
import { formatDate } from '@/common/utils/format';
|
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
|
||||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
|
||||||
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
|
|
||||||
import TagTree from '../component/TagTree.vue';
|
|
||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
|
|
||||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
|
||||||
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
|
|
||||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
|
||||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
|
||||||
import ResourceTags from '../component/ResourceTags.vue';
|
|
||||||
import { MachineProtocolEnum } from './enums';
|
|
||||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import EnumValue from '@/common/Enum';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import ResourceOpPanel from '../component/ResourceOpPanel.vue';
|
|
||||||
|
|
||||||
// 组件
|
|
||||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
|
|
||||||
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
|
|
||||||
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
|
|
||||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
|
|
||||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const perms = {
|
|
||||||
addMachine: 'machine:add',
|
|
||||||
updateMachine: 'machine:update',
|
|
||||||
delMachine: 'machine:del',
|
|
||||||
terminal: 'machine:terminal',
|
|
||||||
closeCli: 'machine:close-cli',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
|
||||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
|
||||||
|
|
||||||
class MachineNodeType {
|
|
||||||
static Machine = 1;
|
|
||||||
static AuthCert = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
defaultExpendKey: [] as any,
|
|
||||||
params: {
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 0,
|
|
||||||
ip: null,
|
|
||||||
name: null,
|
|
||||||
tagPath: '',
|
|
||||||
},
|
|
||||||
infoDialog: {
|
|
||||||
visible: false,
|
|
||||||
data: null as any,
|
|
||||||
},
|
|
||||||
serviceDialog: {
|
|
||||||
visible: false,
|
|
||||||
machineId: 0,
|
|
||||||
authCertName: '',
|
|
||||||
title: '',
|
|
||||||
},
|
|
||||||
processDialog: {
|
|
||||||
visible: false,
|
|
||||||
machineId: 0,
|
|
||||||
},
|
|
||||||
fileDialog: {
|
|
||||||
visible: false,
|
|
||||||
machineId: 0,
|
|
||||||
protocol: 1,
|
|
||||||
title: '',
|
|
||||||
authCertName: '',
|
|
||||||
},
|
|
||||||
filesystemDialog: {
|
|
||||||
visible: false,
|
|
||||||
machineId: 0,
|
|
||||||
authCertName: '',
|
|
||||||
protocol: 1,
|
|
||||||
title: '',
|
|
||||||
fileId: 0,
|
|
||||||
path: '',
|
|
||||||
},
|
|
||||||
machineStatsDialog: {
|
|
||||||
visible: false,
|
|
||||||
stats: null,
|
|
||||||
title: '',
|
|
||||||
machineId: 0,
|
|
||||||
},
|
|
||||||
machineRecDialog: {
|
|
||||||
visible: false,
|
|
||||||
machineId: 0,
|
|
||||||
title: '',
|
|
||||||
},
|
|
||||||
activeTermName: '',
|
|
||||||
tabs: new Map<string, any>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
|
|
||||||
|
|
||||||
const tagTreeRef: any = ref(null);
|
|
||||||
|
|
||||||
const autoOpenResourceStore = useAutoOpenResource();
|
|
||||||
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
|
|
||||||
|
|
||||||
let openIds: any = {};
|
|
||||||
|
|
||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
|
|
||||||
// 加载标签树下的机器列表
|
|
||||||
state.params.tagPath = node.key;
|
|
||||||
state.params.pageNum = 1;
|
|
||||||
state.params.pageSize = 1000;
|
|
||||||
const res = await search();
|
|
||||||
// 把list 根据name字段排序
|
|
||||||
res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|
||||||
return res.list.map((x: any) =>
|
|
||||||
new TagTreeNode(x.code, x.name, NodeTypeMachine)
|
|
||||||
.withParams(x)
|
|
||||||
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
|
|
||||||
.withIcon({
|
|
||||||
name: 'Monitor',
|
|
||||||
color: '#409eff',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const NodeTypeMachine = new NodeType(MachineNodeType.Machine)
|
|
||||||
.withLoadNodesFunc((node: TagTreeNode) => {
|
|
||||||
const machine = node.params;
|
|
||||||
// 获取授权凭证列表
|
|
||||||
const authCerts = machine.authCerts;
|
|
||||||
return authCerts.map((x: any) =>
|
|
||||||
new TagTreeNode(x.name, x.username, NodeTypeAuthCert)
|
|
||||||
.withParams({ ...machine, selectAuthCert: x })
|
|
||||||
.withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
|
|
||||||
.withIcon({
|
|
||||||
name: 'Ticket',
|
|
||||||
color: '#409eff',
|
|
||||||
})
|
|
||||||
.withIsLeaf(true)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.withContextMenuItems([
|
|
||||||
new ContextmenuItem('detail', 'common.detail').withIcon('More').withOnClick((node: any) => showInfo(node.params)),
|
|
||||||
|
|
||||||
new ContextmenuItem('status', 'common.status')
|
|
||||||
.withIcon('Compass')
|
|
||||||
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
|
||||||
.withOnClick((node: any) => showMachineStats(node.params)),
|
|
||||||
|
|
||||||
new ContextmenuItem('process', 'machine.process')
|
|
||||||
.withIcon('DataLine')
|
|
||||||
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
|
||||||
.withOnClick((node: any) => showProcess(node.params)),
|
|
||||||
|
|
||||||
new ContextmenuItem('edit', 'machine.terminalPlayback')
|
|
||||||
.withIcon('Compass')
|
|
||||||
.withOnClick((node: any) => showRec(node.params))
|
|
||||||
.withHideFunc((node: any) => actionBtns[perms.updateMachine] && node.params.enableRecorder == 1),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const NodeTypeAuthCert = new NodeType(MachineNodeType.AuthCert)
|
|
||||||
.withNodeDblclickFunc((node: TagTreeNode) => {
|
|
||||||
openTerminal(node.params);
|
|
||||||
})
|
|
||||||
.withContextMenuItems([
|
|
||||||
new ContextmenuItem('term', 'machine.openTerminal').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params)),
|
|
||||||
new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params, true)),
|
|
||||||
new ContextmenuItem('files', 'machine.fileManage').withIcon('FolderOpened').withOnClick((node: any) => showFileManage(node.params)),
|
|
||||||
|
|
||||||
new ContextmenuItem('scripts', 'machine.scriptManage')
|
|
||||||
.withIcon('Files')
|
|
||||||
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
|
||||||
.withOnClick((node: any) => serviceManager(node.params)),
|
|
||||||
]);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => autoOpenResource.value.machineCodePath,
|
|
||||||
(codePath: any) => {
|
|
||||||
autoOpenTerminal(codePath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => state.activeTermName,
|
|
||||||
(newValue, oldValue) => {
|
|
||||||
fitTerminal();
|
|
||||||
|
|
||||||
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
|
|
||||||
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
|
|
||||||
|
|
||||||
const nowTab = state.tabs.get(state.activeTermName);
|
|
||||||
tagTreeRef.value.setCurrentKey(nowTab?.authCert);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
autoOpenTerminal(autoOpenResource.value.machineCodePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
const autoOpenTerminal = (codePath: string) => {
|
|
||||||
if (!codePath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeAndCodes = getTagTypeCodeByPath(codePath);
|
|
||||||
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
|
|
||||||
|
|
||||||
const machineCode = typeAndCodes[TagResourceTypeEnum.Machine.value][0];
|
|
||||||
state.defaultExpendKey = [tagPath, machineCode];
|
|
||||||
|
|
||||||
const authCertName = typeAndCodes[TagResourceTypeEnum.AuthCert.value][0];
|
|
||||||
setTimeout(() => {
|
|
||||||
// 置空
|
|
||||||
autoOpenResourceStore.setMachineCodePath('');
|
|
||||||
tagTreeRef.value.setCurrentKey(authCertName);
|
|
||||||
|
|
||||||
const acNode = tagTreeRef.value.getNode(authCertName);
|
|
||||||
openTerminal(acNode.data.params);
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openTerminal = (machine: any, ex?: boolean) => {
|
|
||||||
// 授权凭证名
|
|
||||||
const ac = machine.selectAuthCert.name;
|
|
||||||
|
|
||||||
// 新窗口打开
|
|
||||||
if (ex) {
|
|
||||||
if (machine.protocol == MachineProtocolEnum.Ssh.value) {
|
|
||||||
const { href } = router.resolve({
|
|
||||||
path: `/machine/terminal`,
|
|
||||||
query: {
|
|
||||||
ac,
|
|
||||||
name: machine.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
window.open(href, '_blank');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (machine.protocol == MachineProtocolEnum.Rdp.value) {
|
|
||||||
const { href } = router.resolve({
|
|
||||||
path: `/machine/terminal-rdp`,
|
|
||||||
query: {
|
|
||||||
machineId: machine.id,
|
|
||||||
ac: ac,
|
|
||||||
name: machine.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
window.open(href, '_blank');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let { name } = machine;
|
|
||||||
const labelName = `${machine.selectAuthCert.username}@${name}`;
|
|
||||||
|
|
||||||
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
|
||||||
openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
|
|
||||||
let sameIndex = openIds[ac];
|
|
||||||
|
|
||||||
let key = `${ac}_${sameIndex}`;
|
|
||||||
// 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
|
|
||||||
const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
|
|
||||||
|
|
||||||
let tab = {
|
|
||||||
key,
|
|
||||||
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
|
||||||
params: machine,
|
|
||||||
authCert: ac,
|
|
||||||
socketUrl: getMachineTerminalSocketUrl(ac),
|
|
||||||
};
|
|
||||||
|
|
||||||
state.tabs.set(key, tab);
|
|
||||||
state.activeTermName = key;
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
handleReconnect(tab);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const serviceManager = (row: any) => {
|
|
||||||
const authCert = row.selectAuthCert;
|
|
||||||
state.serviceDialog.machineId = row.id;
|
|
||||||
state.serviceDialog.visible = true;
|
|
||||||
state.serviceDialog.authCertName = authCert.name;
|
|
||||||
state.serviceDialog.title = `${row.name} => ${authCert.username}@${row.ip}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示机器状态统计信息
|
|
||||||
*/
|
|
||||||
const showMachineStats = async (machine: any) => {
|
|
||||||
state.machineStatsDialog.machineId = machine.id;
|
|
||||||
state.machineStatsDialog.title = `${t('machine.machineState')}: ${machine.name} => ${machine.ip}`;
|
|
||||||
state.machineStatsDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
const res = await machineApi.list.request(state.params);
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showFileManage = (selectionData: any) => {
|
|
||||||
const authCert = selectionData.selectAuthCert;
|
|
||||||
if (selectionData.protocol == 1) {
|
|
||||||
state.fileDialog.visible = true;
|
|
||||||
state.fileDialog.protocol = selectionData.protocol;
|
|
||||||
state.fileDialog.machineId = selectionData.id;
|
|
||||||
state.fileDialog.authCertName = authCert.name;
|
|
||||||
state.fileDialog.title = `${selectionData.name} => ${authCert.username}@${selectionData.ip}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionData.protocol == 2) {
|
|
||||||
state.filesystemDialog.protocol = 2;
|
|
||||||
state.filesystemDialog.machineId = selectionData.id;
|
|
||||||
state.filesystemDialog.authCertName = authCert.name;
|
|
||||||
state.filesystemDialog.fileId = selectionData.id;
|
|
||||||
state.filesystemDialog.path = '/';
|
|
||||||
state.filesystemDialog.title = t('machine.remoteFileDesktopManage');
|
|
||||||
state.filesystemDialog.visible = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showInfo = (info: any) => {
|
|
||||||
state.infoDialog.data = info;
|
|
||||||
state.infoDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showProcess = (row: any) => {
|
|
||||||
state.processDialog.machineId = row.id;
|
|
||||||
state.processDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showRec = (row: any) => {
|
|
||||||
state.machineRecDialog.title = `${row.name}[${row.ip}]-${t('machine.terminalPlayback')}`;
|
|
||||||
state.machineRecDialog.machineId = row.id;
|
|
||||||
state.machineRecDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveTab = (targetName: string) => {
|
|
||||||
let activeTermName = state.activeTermName;
|
|
||||||
const tabNames = [...state.tabs.keys()];
|
|
||||||
for (let i = 0; i < tabNames.length; i++) {
|
|
||||||
const tabName = tabNames[i];
|
|
||||||
if (tabName !== targetName) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.tabs.delete(targetName);
|
|
||||||
let info = state.tabs.get(targetName);
|
|
||||||
if (info) {
|
|
||||||
terminalRefs[info.key]?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTermName != targetName) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
|
||||||
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
|
||||||
if (nextTab) {
|
|
||||||
activeTermName = nextTab;
|
|
||||||
} else {
|
|
||||||
activeTermName = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
state.activeTermName = activeTermName;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const terminalStatusChange = (key: string, status: TerminalStatus) => {
|
|
||||||
state.tabs.get(key).status = status;
|
|
||||||
};
|
|
||||||
|
|
||||||
const terminalRefs: any = {};
|
|
||||||
const setTerminalRef = (el: any, key: any) => {
|
|
||||||
if (key) {
|
|
||||||
terminalRefs[key] = el;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const terminalWrapperRefs: any = {};
|
|
||||||
const setTerminalWrapperRef = (el: any, key: any) => {
|
|
||||||
if (key) {
|
|
||||||
terminalWrapperRefs[key] = el;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResizeTagTree = () => {
|
|
||||||
fitTerminal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const fitTerminal = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
let info = state.tabs.get(state.activeTermName);
|
|
||||||
if (info) {
|
|
||||||
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReconnect = (tab: any, force = false) => {
|
|
||||||
let width = terminalWrapperRefs[tab.key].offsetWidth;
|
|
||||||
let height = terminalWrapperRefs[tab.key].offsetHeight;
|
|
||||||
terminalRefs[tab.key]?.init(width, height, force);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.machine-terminal-tabs {
|
|
||||||
--el-tabs-header-height: 30px;
|
|
||||||
|
|
||||||
.el-tabs {
|
|
||||||
--el-tabs-header-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.machine-terminal-tab-label {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.el-tabs__header {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.el-tabs__item {
|
|
||||||
padding: 0 8px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,76 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true" :before-close="cancel" width="1050px">
|
||||||
<el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true" :before-close="cancel" width="1050px">
|
<el-row :gutter="20">
|
||||||
<el-row :gutter="20">
|
<el-col :lg="12" :md="12">
|
||||||
<el-col :lg="12" :md="12">
|
<el-descriptions size="small" :title="$t('machine.basicInfo')" :column="2" border>
|
||||||
<el-descriptions size="small" :title="$t('machine.basicInfo')" :column="2" border>
|
<template #extra>
|
||||||
<template #extra>
|
<el-link @click="onRefresh" icon="refresh" underline="never" type="success"></el-link>
|
||||||
<el-link @click="onRefresh" icon="refresh" underline="never" type="success"></el-link>
|
</template>
|
||||||
|
<el-descriptions-item :label="$t('machine.hostname')">
|
||||||
|
{{ stats.hostname }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('machine.runTime')">
|
||||||
|
{{ stats.uptime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('machine.totalTask')">
|
||||||
|
{{ stats.totalProcs }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('machine.runningTask')">
|
||||||
|
{{ stats.runningProcs }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('machine.load')"> {{ stats.load1 }} {{ stats.load5 }} {{ stats.load10 }} </el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :lg="6" :md="6">
|
||||||
|
<ECharts height="200" :option="state.memOption" />
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :lg="6" :md="6">
|
||||||
|
<ECharts height="200" :option="state.cpuOption" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :lg="8" :md="8">
|
||||||
|
<span style="font-size: 16px; font-weight: 700">{{ $t('machine.disk') }}</span>
|
||||||
|
<el-table :data="stats.fSInfos" stripe max-height="250" style="width: 100%" border>
|
||||||
|
<el-table-column prop="mountPoint" :label="$t('machine.mountPoint')" min-width="100" show-overflow-tooltip> </el-table-column>
|
||||||
|
<el-table-column :label="$t('machine.available')" min-width="70" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatByteSize(scope.row.free) }}
|
||||||
</template>
|
</template>
|
||||||
<el-descriptions-item :label="$t('machine.hostname')">
|
</el-table-column>
|
||||||
{{ stats.hostname }}
|
<el-table-column prop="Used" :label="$t('machine.used')" min-width="70" show-overflow-tooltip>
|
||||||
</el-descriptions-item>
|
<template #default="scope">
|
||||||
<el-descriptions-item :label="$t('machine.runTime')">
|
{{ formatByteSize(scope.row.used) }}
|
||||||
{{ stats.uptime }}
|
</template>
|
||||||
</el-descriptions-item>
|
</el-table-column>
|
||||||
<el-descriptions-item :label="$t('machine.totalTask')">
|
</el-table>
|
||||||
{{ stats.totalProcs }}
|
</el-col>
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('machine.runningTask')">
|
|
||||||
{{ stats.runningProcs }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('machine.load')"> {{ stats.load1 }} {{ stats.load5 }} {{ stats.load10 }} </el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :lg="6" :md="6">
|
<el-col :lg="16" :md="16">
|
||||||
<ECharts height="200" :option="state.memOption" />
|
<span style="font-size: 16px; font-weight: 700">{{ $t('machine.networkCard') }}</span>
|
||||||
</el-col>
|
<el-table :data="netInter" stripe max-height="250" style="width: 100%" border>
|
||||||
|
<el-table-column prop="name" :label="$t('machine.networkCard')" min-width="120" show-overflow-tooltip></el-table-column>
|
||||||
<el-col :lg="6" :md="6">
|
<el-table-column prop="ipv4" label="IPv4" min-width="130" show-overflow-tooltip> </el-table-column>
|
||||||
<ECharts height="200" :option="state.cpuOption" />
|
<el-table-column prop="ipv6" label="IPv6" min-width="130" show-overflow-tooltip> </el-table-column>
|
||||||
</el-col>
|
<el-table-column prop="rx" :label="`${$t('machine.receive')}(rx)`" min-width="110" show-overflow-tooltip>
|
||||||
</el-row>
|
<template #default="scope">
|
||||||
|
{{ formatByteSize(scope.row.rx) }}
|
||||||
<el-row :gutter="20">
|
</template>
|
||||||
<el-col :lg="8" :md="8">
|
</el-table-column>
|
||||||
<span style="font-size: 16px; font-weight: 700">{{ $t('machine.disk') }}</span>
|
<el-table-column prop="tx" :label="`${$t('machine.send')}(tx)`" min-width="110" show-overflow-tooltip>
|
||||||
<el-table :data="stats.fSInfos" stripe max-height="250" style="width: 100%" border>
|
<template #default="scope">
|
||||||
<el-table-column prop="mountPoint" :label="$t('machine.mountPoint')" min-width="100" show-overflow-tooltip> </el-table-column>
|
{{ formatByteSize(scope.row.tx) }}
|
||||||
<el-table-column :label="$t('machine.available')" min-width="70" show-overflow-tooltip>
|
</template>
|
||||||
<template #default="scope">
|
</el-table-column>
|
||||||
{{ formatByteSize(scope.row.free) }}
|
</el-table>
|
||||||
</template>
|
</el-col>
|
||||||
</el-table-column>
|
</el-row>
|
||||||
<el-table-column prop="Used" :label="$t('machine.used')" min-width="70" show-overflow-tooltip>
|
</el-dialog>
|
||||||
<template #default="scope">
|
|
||||||
{{ formatByteSize(scope.row.used) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :lg="16" :md="16">
|
|
||||||
<span style="font-size: 16px; font-weight: 700">{{ $t('machine.networkCard') }}</span>
|
|
||||||
<el-table :data="netInter" stripe max-height="250" style="width: 100%" border>
|
|
||||||
<el-table-column prop="name" :label="$t('machine.networkCard')" min-width="120" show-overflow-tooltip></el-table-column>
|
|
||||||
<el-table-column prop="ipv4" label="IPv4" min-width="130" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column prop="ipv6" label="IPv6" min-width="130" show-overflow-tooltip> </el-table-column>
|
|
||||||
<el-table-column prop="rx" :label="`${$t('machine.receive')}(rx)`" min-width="110" show-overflow-tooltip>
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatByteSize(scope.row.rx) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="tx" :label="`${$t('machine.send')}(tx)`" min-width="110" show-overflow-tooltip>
|
|
||||||
<template #default="scope">
|
|
||||||
{{ formatByteSize(scope.row.tx) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|||||||
457
frontend/src/views/ops/machine/resource/MachineOp.vue
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full machine-terminal-tabs">
|
||||||
|
<el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" v-model="state.activeTermName" class="!h-full w-full">
|
||||||
|
<el-tab-pane class="!h-full flex flex-col" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||||
|
<template #label>
|
||||||
|
<el-popconfirm @confirm="handleReconnect(dt, true)" :title="$t('machine.reConnTips')">
|
||||||
|
<template #reference>
|
||||||
|
<el-icon
|
||||||
|
class="mr-1"
|
||||||
|
:color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
|
||||||
|
:title="dt.status == TerminalStatusEnum.Connected.value ? '' : $t('machine.clickReConn')"
|
||||||
|
><Connection />
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<div>
|
||||||
|
<span class="machine-terminal-tab-label">{{ dt.label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item :label="$t('common.name')"> {{ dt.params?.name }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
|
||||||
|
<el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div :ref="(el: any) => setTerminalWrapperRef(el, dt.key)" class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
|
||||||
|
<TerminalBody
|
||||||
|
v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
|
||||||
|
:mount-init="false"
|
||||||
|
@status-change="terminalStatusChange(dt.key, $event)"
|
||||||
|
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
||||||
|
:socket-url="dt.socketUrl"
|
||||||
|
/>
|
||||||
|
<machine-rdp
|
||||||
|
v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
|
||||||
|
:machine-id="dt.params.id"
|
||||||
|
:auth-cert="dt.authCert"
|
||||||
|
:ref="(el: any) => setTerminalRef(el, dt.key)"
|
||||||
|
@status-change="terminalStatusChange(dt.key, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
|
||||||
|
<el-descriptions :title="$t('common.detail')" :column="3" border>
|
||||||
|
<el-descriptions-item :span="1.5" label="ID">{{ infoDialog.data.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1.5" :label="$t('common.name')">{{ infoDialog.data.name }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" :label="$t('tag.relateTag')">
|
||||||
|
<ResourceTags :tags="infoDialog.data.tags" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" :label="$t('machine.port')">{{ infoDialog.data.port }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="3" :label="$t('common.remark')">{{ infoDialog.data.remark }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="1.5" :label="$t('machine.sshTunnel')"
|
||||||
|
>{{ infoDialog.data.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')"
|
||||||
|
>{{ infoDialog.data.enableRecorder == 1 ? $t('common.yes') : $t('common.no') }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" :label="$t('common.createTime')">
|
||||||
|
{{ formatDate(infoDialog.data.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" :label="$t('common.creator')">
|
||||||
|
{{ infoDialog.data.creator }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :span="2" :label="$t('common.updateTime')">
|
||||||
|
{{ formatDate(infoDialog.data.updateTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :span="1" :label="$t('common.modifier')">
|
||||||
|
{{ infoDialog.data.modifier }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
|
||||||
|
|
||||||
|
<script-manage
|
||||||
|
:title="serviceDialog.title"
|
||||||
|
v-model:visible="serviceDialog.visible"
|
||||||
|
v-model:machineId="serviceDialog.machineId"
|
||||||
|
:auth-cert-name="serviceDialog.authCertName"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<file-conf-list
|
||||||
|
:title="fileDialog.title"
|
||||||
|
:auth-cert-name="fileDialog.authCertName"
|
||||||
|
v-model:visible="fileDialog.visible"
|
||||||
|
v-model:machineId="fileDialog.machineId"
|
||||||
|
:protocol="fileDialog.protocol"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog destroy-on-close :title="state.filesystemDialog.title" v-model="state.filesystemDialog.visible" :close-on-click-modal="false" width="70%">
|
||||||
|
<machine-file
|
||||||
|
:machine-id="state.filesystemDialog.machineId"
|
||||||
|
:auth-cert-name="state.filesystemDialog.authCertName"
|
||||||
|
:protocol="state.filesystemDialog.protocol"
|
||||||
|
:file-id="state.filesystemDialog.fileId"
|
||||||
|
:path="state.filesystemDialog.path"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
|
||||||
|
|
||||||
|
<machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, getCurrentInstance, inject, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { getMachineTerminalSocketUrl } from '../api';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
|
import TerminalBody from '@/components/terminal/TerminalBody.vue';
|
||||||
|
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
|
||||||
|
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
|
||||||
|
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
|
||||||
|
import ResourceTags from '../../component/ResourceTags.vue';
|
||||||
|
import { MachineProtocolEnum } from '../enums';
|
||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { ResourceOpCtx } from '@/views/ops/component/tag';
|
||||||
|
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||||
|
import { MachineOpComp } from '@/views/ops/machine/resource';
|
||||||
|
|
||||||
|
// 组件
|
||||||
|
const ScriptManage = defineAsyncComponent(() => import('../ScriptManage.vue'));
|
||||||
|
const FileConfList = defineAsyncComponent(() => import('../file/FileConfList.vue'));
|
||||||
|
const MachineStats = defineAsyncComponent(() => import('../MachineStats.vue'));
|
||||||
|
const MachineRec = defineAsyncComponent(() => import('../MachineRec.vue'));
|
||||||
|
const ProcessList = defineAsyncComponent(() => import('../ProcessList.vue'));
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
addMachine: 'machine:add',
|
||||||
|
updateMachine: 'machine:update',
|
||||||
|
delMachine: 'machine:del',
|
||||||
|
terminal: 'machine:terminal',
|
||||||
|
closeCli: 'machine:close-cli',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
|
||||||
|
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
|
||||||
|
|
||||||
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
|
class MachineNodeType {
|
||||||
|
static Machine = 1;
|
||||||
|
static AuthCert = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
defaultExpendKey: [] as any,
|
||||||
|
params: {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 0,
|
||||||
|
ip: null,
|
||||||
|
name: null,
|
||||||
|
tagPath: '',
|
||||||
|
},
|
||||||
|
infoDialog: {
|
||||||
|
visible: false,
|
||||||
|
data: null as any,
|
||||||
|
},
|
||||||
|
serviceDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
authCertName: '',
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
processDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
},
|
||||||
|
fileDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
protocol: 1,
|
||||||
|
title: '',
|
||||||
|
authCertName: '',
|
||||||
|
},
|
||||||
|
filesystemDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
authCertName: '',
|
||||||
|
protocol: 1,
|
||||||
|
title: '',
|
||||||
|
fileId: 0,
|
||||||
|
path: '',
|
||||||
|
},
|
||||||
|
machineStatsDialog: {
|
||||||
|
visible: false,
|
||||||
|
stats: null,
|
||||||
|
title: '',
|
||||||
|
machineId: 0,
|
||||||
|
},
|
||||||
|
machineRecDialog: {
|
||||||
|
visible: false,
|
||||||
|
machineId: 0,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
activeTermName: '',
|
||||||
|
tabs: new Map<string, any>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
|
||||||
|
|
||||||
|
let openIds: any = {};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state.activeTermName,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
fitTerminal();
|
||||||
|
|
||||||
|
oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
|
||||||
|
terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
|
||||||
|
|
||||||
|
const nowTab = state.tabs.get(state.activeTermName);
|
||||||
|
resourceOpCtx?.setCurrentTreeKey(nowTab?.authCert);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
emits('init', { name: MachineOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||||
|
});
|
||||||
|
|
||||||
|
const openTerminal = (machine: any, ex?: boolean) => {
|
||||||
|
// 授权凭证名
|
||||||
|
const ac = machine.selectAuthCert.name;
|
||||||
|
|
||||||
|
// 新窗口打开
|
||||||
|
if (ex) {
|
||||||
|
if (machine.protocol == MachineProtocolEnum.Ssh.value) {
|
||||||
|
const { href } = router.resolve({
|
||||||
|
path: `/machine/terminal`,
|
||||||
|
query: {
|
||||||
|
ac,
|
||||||
|
name: machine.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.open(href, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (machine.protocol == MachineProtocolEnum.Rdp.value) {
|
||||||
|
const { href } = router.resolve({
|
||||||
|
path: `/machine/terminal-rdp`,
|
||||||
|
query: {
|
||||||
|
machineId: machine.id,
|
||||||
|
ac: ac,
|
||||||
|
name: machine.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.open(href, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let { name } = machine;
|
||||||
|
const labelName = `${machine.selectAuthCert.username}@${name}`;
|
||||||
|
|
||||||
|
// 同一个机器的终端打开多次,key后添加下划线和数字区分
|
||||||
|
openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
|
||||||
|
let sameIndex = openIds[ac];
|
||||||
|
|
||||||
|
let key = `${ac}_${sameIndex}`;
|
||||||
|
// 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
|
||||||
|
const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
|
||||||
|
|
||||||
|
let tab = {
|
||||||
|
key,
|
||||||
|
label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
|
||||||
|
params: machine,
|
||||||
|
authCert: ac,
|
||||||
|
socketUrl: getMachineTerminalSocketUrl(ac),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.tabs.set(key, tab);
|
||||||
|
state.activeTermName = key;
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
handleReconnect(tab);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceManager = (row: any) => {
|
||||||
|
const authCert = row.selectAuthCert;
|
||||||
|
state.serviceDialog.machineId = row.id;
|
||||||
|
state.serviceDialog.visible = true;
|
||||||
|
state.serviceDialog.authCertName = authCert.name;
|
||||||
|
state.serviceDialog.title = `${row.name} => ${authCert.username}@${row.ip}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示机器状态统计信息
|
||||||
|
*/
|
||||||
|
const showMachineStats = (machine: any) => {
|
||||||
|
state.machineStatsDialog.machineId = machine.id;
|
||||||
|
state.machineStatsDialog.title = `${t('machine.machineState')}: ${machine.name} => ${machine.ip}`;
|
||||||
|
state.machineStatsDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFileManage = (selectionData: any) => {
|
||||||
|
const authCert = selectionData.selectAuthCert;
|
||||||
|
if (selectionData.protocol == 1) {
|
||||||
|
state.fileDialog.visible = true;
|
||||||
|
state.fileDialog.protocol = selectionData.protocol;
|
||||||
|
state.fileDialog.machineId = selectionData.id;
|
||||||
|
state.fileDialog.authCertName = authCert.name;
|
||||||
|
state.fileDialog.title = `${selectionData.name} => ${authCert.username}@${selectionData.ip}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionData.protocol == 2) {
|
||||||
|
state.filesystemDialog.protocol = 2;
|
||||||
|
state.filesystemDialog.machineId = selectionData.id;
|
||||||
|
state.filesystemDialog.authCertName = authCert.name;
|
||||||
|
state.filesystemDialog.fileId = selectionData.id;
|
||||||
|
state.filesystemDialog.path = '/';
|
||||||
|
state.filesystemDialog.title = t('machine.remoteFileDesktopManage');
|
||||||
|
state.filesystemDialog.visible = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showInfo = (info: any) => {
|
||||||
|
state.infoDialog.data = info;
|
||||||
|
state.infoDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showProcess = (row: any) => {
|
||||||
|
state.processDialog.machineId = row.id;
|
||||||
|
state.processDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showRec = (row: any) => {
|
||||||
|
state.machineRecDialog.title = `${row.name}[${row.ip}]-${t('machine.terminalPlayback')}`;
|
||||||
|
state.machineRecDialog.machineId = row.id;
|
||||||
|
state.machineRecDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTab = (targetName: string) => {
|
||||||
|
let activeTermName = state.activeTermName;
|
||||||
|
const tabNames = [...state.tabs.keys()];
|
||||||
|
for (let i = 0; i < tabNames.length; i++) {
|
||||||
|
const tabName = tabNames[i];
|
||||||
|
if (tabName !== targetName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tabs.delete(targetName);
|
||||||
|
let info = state.tabs.get(targetName);
|
||||||
|
if (info) {
|
||||||
|
terminalRefs[info.key]?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTermName != targetName) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||||
|
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeTermName = nextTab;
|
||||||
|
} else {
|
||||||
|
activeTermName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
state.activeTermName = activeTermName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const terminalStatusChange = (key: string, status: TerminalStatus) => {
|
||||||
|
state.tabs.get(key).status = status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const terminalRefs: any = {};
|
||||||
|
const setTerminalRef = (el: any, key: any) => {
|
||||||
|
if (key) {
|
||||||
|
terminalRefs[key] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const terminalWrapperRefs: any = {};
|
||||||
|
const setTerminalWrapperRef = (el: any, key: any) => {
|
||||||
|
if (key) {
|
||||||
|
terminalWrapperRefs[key] = el;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResizeTagTree = () => {
|
||||||
|
fitTerminal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitTerminal = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let info = state.tabs.get(state.activeTermName);
|
||||||
|
if (info) {
|
||||||
|
terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReconnect = (tab: any, force = false) => {
|
||||||
|
let width = terminalWrapperRefs[tab.key]?.offsetWidth;
|
||||||
|
let height = terminalWrapperRefs[tab.key]?.offsetHeight;
|
||||||
|
terminalRefs[tab.key]?.init(width, height, force);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openTerminal,
|
||||||
|
onResize: fitTerminal,
|
||||||
|
showInfo,
|
||||||
|
showProcess,
|
||||||
|
showRec,
|
||||||
|
showMachineStats,
|
||||||
|
showFileManage,
|
||||||
|
serviceManager,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.machine-terminal-tabs {
|
||||||
|
--el-tabs-header-height: 30px;
|
||||||
|
|
||||||
|
.el-tabs {
|
||||||
|
--el-tabs-header-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-terminal-tab-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.el-tabs__header {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
frontend/src/views/ops/machine/resource/NodeMachineAc.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #suffix="{ data }">
|
||||||
|
<span>{{ ` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}` }}</span>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
119
frontend/src/views/ops/machine/resource/index.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '@/views/ops/component/tag';
|
||||||
|
import { machineApi } from '@/views/ops/machine/api';
|
||||||
|
import { MachineProtocolEnum } from '@/views/ops/machine/enums';
|
||||||
|
|
||||||
|
const MachineList = defineAsyncComponent(() => import('../MachineList.vue'));
|
||||||
|
const MachineOp = defineAsyncComponent(() => import('./MachineOp.vue'));
|
||||||
|
|
||||||
|
const NodeMachineAc = defineAsyncComponent(() => import('./NodeMachineAc.vue'));
|
||||||
|
|
||||||
|
const MachineIcon = {
|
||||||
|
name: ResourceTypeEnum.Machine.extra.icon,
|
||||||
|
color: ResourceTypeEnum.Machine.extra.iconColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MachineOpComp: ResourceComponentConfig = {
|
||||||
|
name: 'tag.machineOp',
|
||||||
|
component: MachineOp,
|
||||||
|
icon: MachineIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodeTypeMachineTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
|
||||||
|
node.ctx?.addResourceComponent(MachineOpComp);
|
||||||
|
// 加载标签树下的机器列表
|
||||||
|
const res = await machineApi.list.request({ tagPath: node.params.tagPath });
|
||||||
|
// 把list 根据name字段排序
|
||||||
|
return res?.list
|
||||||
|
.sort((a: any, b: any) => a.name.localeCompare(b.name))
|
||||||
|
.map((x: any) =>
|
||||||
|
TagTreeNode.new(node, x.code, x.name, NodeTypeMachine)
|
||||||
|
.withParams(x)
|
||||||
|
.withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
|
||||||
|
.withIcon(MachineIcon)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const NodeTypeMachine = new NodeType(11)
|
||||||
|
.withLoadNodesFunc((node: TagTreeNode) => {
|
||||||
|
const machine = node.params;
|
||||||
|
// 获取授权凭证列表
|
||||||
|
const authCerts = machine.authCerts;
|
||||||
|
return authCerts.map((x: any) =>
|
||||||
|
TagTreeNode.new(node, x.name, x.username, NodeTypeAuthCert)
|
||||||
|
.withNodeComponent(NodeMachineAc)
|
||||||
|
.withParams({ ...machine, selectAuthCert: x })
|
||||||
|
.withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
|
||||||
|
.withIcon({
|
||||||
|
name: 'Ticket',
|
||||||
|
color: '#409eff',
|
||||||
|
})
|
||||||
|
.withIsLeaf(true)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('detail', 'common.detail').withIcon('More').withOnClick(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp)).showInfo(node.params);
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ContextmenuItem('status', 'common.status')
|
||||||
|
.withIcon('Compass')
|
||||||
|
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||||
|
.withOnClick(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp)).showMachineStats(node.params);
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ContextmenuItem('process', 'machine.process')
|
||||||
|
.withIcon('DataLine')
|
||||||
|
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||||
|
.withOnClick(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp)).showProcess(node.params);
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ContextmenuItem('edit', 'machine.terminalPlayback')
|
||||||
|
.withIcon('Compass')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp)).showRec(node.params);
|
||||||
|
})
|
||||||
|
.withHideFunc((node: any) => node.params.enableRecorder == 1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const NodeTypeAuthCert = new NodeType(12)
|
||||||
|
.withNodeDblclickFunc(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp)).openTerminal(node.params);
|
||||||
|
})
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('term', 'machine.openTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
|
||||||
|
}),
|
||||||
|
new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params, true);
|
||||||
|
}),
|
||||||
|
new ContextmenuItem('files', 'machine.fileManage').withIcon('FolderOpened').withOnClick(async (node: any) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp)).showFileManage(node.params);
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ContextmenuItem('scripts', 'machine.scriptManage')
|
||||||
|
.withIcon('Files')
|
||||||
|
.withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
|
||||||
|
.withOnClick(async (node: any) => {
|
||||||
|
(await node.ctx?.addResourceComponent(MachineOpComp)).serviceManager(node.params);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: 1,
|
||||||
|
resourceType: ResourceTypeEnum.Machine.value,
|
||||||
|
rootNodeType: NodeTypeMachineTag,
|
||||||
|
manager: {
|
||||||
|
componentConf: {
|
||||||
|
component: MachineList,
|
||||||
|
icon: MachineIcon,
|
||||||
|
name: 'tag.machine',
|
||||||
|
},
|
||||||
|
permCode: 'machine',
|
||||||
|
countKey: 'machine',
|
||||||
|
},
|
||||||
|
} as ResourceConfig;
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
MachineList: () => import('@/views/ops/machine/MachineList.vue'),
|
|
||||||
MachineOp: () => import('@/views/ops/machine/MachineOp.vue'),
|
|
||||||
CronJobList: () => import('@/views/ops/machine/cronjob/CronJobList.vue'),
|
CronJobList: () => import('@/views/ops/machine/cronjob/CronJobList.vue'),
|
||||||
SecurityConfList: () => import('@/views/ops/machine/security/SecurityConfList.vue'),
|
SecurityConfList: () => import('@/views/ops/machine/security/SecurityConfList.vue'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,561 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex-all-center h-full">
|
|
||||||
<ResourceOpPanel>
|
|
||||||
<template #left>
|
|
||||||
<tag-tree
|
|
||||||
ref="tagTreeRef"
|
|
||||||
:default-expanded-keys="state.defaultExpendKey"
|
|
||||||
:resource-type="TagResourceTypeEnum.Mongo.value"
|
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
|
||||||
>
|
|
||||||
<template #prefix="{ data }">
|
|
||||||
<span v-if="data.type.value == MongoNodeType.Mongo">
|
|
||||||
<el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
|
|
||||||
<template #reference>
|
|
||||||
<SvgIcon name="icon mongo/mongo-color" :size="18" />
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<el-descriptions :column="1" size="small">
|
|
||||||
<el-descriptions-item :label="$t('common.name')">
|
|
||||||
{{ data.params.name }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="url">
|
|
||||||
{{ data.params.uri }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SvgIcon v-if="data.type.value == MongoNodeType.Dbs" name="Coin" color="#67c23a" />
|
|
||||||
|
|
||||||
<SvgIcon
|
|
||||||
v-if="data.type.value == MongoNodeType.Coll || data.type.value == MongoNodeType.CollMenu"
|
|
||||||
name="Document"
|
|
||||||
class="color-primary"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #suffix="{ data }">
|
|
||||||
<span v-if="data.type.value == MongoNodeType.Dbs">{{ formatByteSize(data.params.size) }}</span>
|
|
||||||
</template>
|
|
||||||
</tag-tree>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
|
||||||
<div class="mongo-data-tab card h-full !p-1 w-full">
|
|
||||||
<el-row v-if="nowColl">
|
|
||||||
<el-descriptions class="!w-full" :column="10" size="small" border>
|
|
||||||
<!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
|
|
||||||
|
|
||||||
<el-descriptions-item label="ns" label-align="right">
|
|
||||||
{{ nowColl.stats?.ns }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="count" label-align="right">
|
|
||||||
{{ nowColl.stats?.count }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="avgObjSize" label-align="right">
|
|
||||||
{{ formatByteSize(nowColl.stats?.avgObjSize) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="size" label-align="right">
|
|
||||||
{{ formatByteSize(nowColl.stats?.size) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="totalSize" label-align="right">
|
|
||||||
{{ formatByteSize(nowColl.stats?.totalSize) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="storageSize" label-align="right">
|
|
||||||
{{ formatByteSize(nowColl.stats?.storageSize) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="freeStorageSize" label-align="right">
|
|
||||||
{{ formatByteSize(nowColl.stats?.freeStorageSize) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row type="flex">
|
|
||||||
<el-tabs @tab-remove="removeDataTab" class="!w-full ml-1" v-model="state.activeName">
|
|
||||||
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="2">
|
|
||||||
<div class="mt-1">
|
|
||||||
<el-link @click="findCommand(state.activeName)" icon="refresh" underline="never" class=""> </el-link>
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
<el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" underline="never"> </el-link>
|
|
||||||
</div>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="22">
|
|
||||||
<el-input
|
|
||||||
ref="findParamInputRef"
|
|
||||||
v-model="dt.findParamStr"
|
|
||||||
:placeholder="$t('mongo.queryParamPlaceholder')"
|
|
||||||
@focus="showFindDialog(dt.key)"
|
|
||||||
>
|
|
||||||
<template #prepend>{{ $t('mongo.queryParam') }}</template>
|
|
||||||
</el-input>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<el-scrollbar class="mongo-data-tab-data">
|
|
||||||
<el-row>
|
|
||||||
<el-col :span="6" v-for="item in dt.datas" :key="item">
|
|
||||||
<el-card :body-style="{ padding: '0px', position: 'relative' }">
|
|
||||||
<el-input type="textarea" v-model="item.value" :rows="10" />
|
|
||||||
<div style="padding: 3px; float: right" class="mr-1 mongo-doc-btns">
|
|
||||||
<div>
|
|
||||||
<el-link @click="onEditDoc(item)" underline="never" type="success" icon="MagicStick"></el-link>
|
|
||||||
|
|
||||||
<el-divider direction="vertical" border-style="dashed" />
|
|
||||||
|
|
||||||
<el-popconfirm @confirm="onDeleteDoc(item.value)" :title="$t('mongo.deleteDocConfirm')" width="160">
|
|
||||||
<template #reference>
|
|
||||||
<el-link v-auth="perms.delData" underline="never" type="danger" icon="DocumentDelete">
|
|
||||||
</el-link>
|
|
||||||
</template>
|
|
||||||
</el-popconfirm>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</el-scrollbar>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ResourceOpPanel>
|
|
||||||
|
|
||||||
<el-dialog width="600px" title="find params" v-model="findDialog.visible">
|
|
||||||
<el-form label-width="auto">
|
|
||||||
<el-form-item label="filter">
|
|
||||||
<monaco-editor style="width: 100%" height="150px" ref="monacoEditorRef" v-model="findDialog.findParam.filter" language="json" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="sort">
|
|
||||||
<el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable auto-complete="off"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="limit">
|
|
||||||
<el-input v-model.number="findDialog.findParam.limit" type="number" auto-complete="off"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="skip">
|
|
||||||
<el-input v-model.number="findDialog.findParam.skip" type="number" auto-complete="off"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<div>
|
|
||||||
<el-button @click="findDialog.visible = false">{{ $t('common.cancel') }}</el-button>
|
|
||||||
<el-button @click="confirmFindDialog" type="primary">{{ $t('common.confirm') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
width="60%"
|
|
||||||
:title="`${state.docEditDialog.isAdd ? $t('common.add') : $t('common.edit')} '${state.activeName}' $t('mongo.doc')`"
|
|
||||||
v-model="docEditDialog.visible"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
>
|
|
||||||
<monaco-editor v-model="docEditDialog.doc" language="json" />
|
|
||||||
<template #footer>
|
|
||||||
<div>
|
|
||||||
<el-button @click="docEditDialog.visible = false">{{ $t('common.cancel') }}</el-button>
|
|
||||||
<el-button v-auth="perms.saveData" @click="onSaveDoc" type="primary">{{ $t('common.confirm') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 10px"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { mongoApi } from './api';
|
|
||||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
|
|
||||||
import { isTrue, notBlank } from '@/common/assert';
|
|
||||||
import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
|
|
||||||
import TagTree from '../component/TagTree.vue';
|
|
||||||
import { formatByteSize } from '@/common/utils/format';
|
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
|
||||||
import { sleep } from '@/common/utils/loading';
|
|
||||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useI18nDeleteSuccessMsg, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
|
||||||
import ResourceOpPanel from '../component/ResourceOpPanel.vue';
|
|
||||||
|
|
||||||
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const perms = {
|
|
||||||
saveData: 'mongo:data:save',
|
|
||||||
delData: 'mongo:data:del',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 树节点类型
|
|
||||||
*/
|
|
||||||
class MongoNodeType {
|
|
||||||
static Mongo = 1;
|
|
||||||
static Dbs = 2;
|
|
||||||
static CollMenu = 3;
|
|
||||||
static Coll = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tagpath 节点类型
|
|
||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const res = await mongoApi.mongoList.request({ tagPath: parentNode.key });
|
|
||||||
if (!res.total) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const mongoInfos = res.list;
|
|
||||||
await sleep(100);
|
|
||||||
return mongoInfos?.map((x: any) => {
|
|
||||||
x.tagPath = parentNode.key;
|
|
||||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeMongo).withParams(x);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const NodeTypeMongo = new NodeType(MongoNodeType.Mongo).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const inst = parentNode.params;
|
|
||||||
// 点击mongo -> 加载mongo数据库列表
|
|
||||||
const res = await mongoApi.databases.request({ id: inst.id });
|
|
||||||
return res.Databases.map((x: any) => {
|
|
||||||
const database = x.Name;
|
|
||||||
return new TagTreeNode(`${inst.id}.${database}`, database, NodeTypeDbs).withParams({
|
|
||||||
id: inst.id,
|
|
||||||
database,
|
|
||||||
size: x.SizeOnDisk,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const NodeTypeDbs = new NodeType(MongoNodeType.Dbs).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const params = parentNode.params;
|
|
||||||
// 点击数据库列表 -> 加载数据库下拥有的菜单列表
|
|
||||||
return [new TagTreeNode(`${params.id}.${params.database}.mongo-coll`, 'mongo.coll', NodeTypeCollMenu).withParams(params)];
|
|
||||||
});
|
|
||||||
|
|
||||||
const NodeTypeCollMenu = new NodeType(MongoNodeType.CollMenu).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const { id, database } = parentNode.params;
|
|
||||||
// 点击数据库集合节点 -> 加载集合列表
|
|
||||||
const colls = await mongoApi.collections.request({ id, database });
|
|
||||||
return colls.map((x: any) => {
|
|
||||||
return new TagTreeNode(`${id}.${database}.${x}`, x, NodeTypeColl).withIsLeaf(true).withParams({
|
|
||||||
id,
|
|
||||||
database,
|
|
||||||
collection: x,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const NodeTypeColl = new NodeType(MongoNodeType.Coll).withNodeClickFunc((nodeData: TagTreeNode) => {
|
|
||||||
const { id, database, collection } = nodeData.params;
|
|
||||||
changeCollection(id, database, collection);
|
|
||||||
});
|
|
||||||
|
|
||||||
const findParamInputRef: any = ref(null);
|
|
||||||
const tagTreeRef: any = ref(null);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
defaultExpendKey: [] as any,
|
|
||||||
tags: [],
|
|
||||||
mongoList: [] as any,
|
|
||||||
activeName: '', // 当前操作的tab
|
|
||||||
dataTabs: {} as any, // 数据tabs
|
|
||||||
findDialog: {
|
|
||||||
visible: false,
|
|
||||||
findParam: {
|
|
||||||
limit: 0,
|
|
||||||
skip: 0,
|
|
||||||
filter: '',
|
|
||||||
sort: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
docEditDialog: {
|
|
||||||
isAdd: true,
|
|
||||||
visible: false,
|
|
||||||
doc: '',
|
|
||||||
},
|
|
||||||
insertDocDialog: {
|
|
||||||
visible: false,
|
|
||||||
doc: '',
|
|
||||||
},
|
|
||||||
jsonEditorDialog: {
|
|
||||||
visible: false,
|
|
||||||
doc: '',
|
|
||||||
item: {} as any,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { findDialog, docEditDialog } = toRefs(state);
|
|
||||||
|
|
||||||
const autoOpenResourceStore = useAutoOpenResource();
|
|
||||||
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
|
|
||||||
|
|
||||||
const nowColl = computed(() => {
|
|
||||||
return getNowDataTab();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => autoOpenResource.value.mongoCodePath,
|
|
||||||
(codePath: any) => {
|
|
||||||
autoOpenMongo(codePath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
autoOpenMongo(autoOpenResource.value.mongoCodePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
const autoOpenMongo = (codePath: string) => {
|
|
||||||
if (!codePath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeAndCodes = getTagTypeCodeByPath(codePath);
|
|
||||||
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
|
|
||||||
|
|
||||||
const mongoCode = typeAndCodes[TagResourceTypeEnum.Mongo.value][0];
|
|
||||||
state.defaultExpendKey = [tagPath, mongoCode];
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// 置空
|
|
||||||
autoOpenResourceStore.setMongoCodePath('');
|
|
||||||
tagTreeRef.value.setCurrentKey(mongoCode);
|
|
||||||
}, 600);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeCollection = async (id: any, schema: string, collection: string) => {
|
|
||||||
const label = `${id}:\`${schema}\`.${collection}`;
|
|
||||||
let dataTab = state.dataTabs[label];
|
|
||||||
if (!dataTab) {
|
|
||||||
// 默认查询参数
|
|
||||||
const findParam = {
|
|
||||||
filter: '{}',
|
|
||||||
sort: '{"_id": -1}',
|
|
||||||
skip: 0,
|
|
||||||
limit: 12,
|
|
||||||
};
|
|
||||||
state.dataTabs[label] = {
|
|
||||||
key: label,
|
|
||||||
label: label,
|
|
||||||
name: label,
|
|
||||||
mongoId: id,
|
|
||||||
database: schema,
|
|
||||||
collection,
|
|
||||||
datas: [],
|
|
||||||
findParamStr: JSON.stringify(findParam),
|
|
||||||
findParam,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
state.activeName = label;
|
|
||||||
findCommand(label);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showFindDialog = (key: string) => {
|
|
||||||
// 获取当前tab的索引位置,将其输入框失去焦点,防止输入以及重复获取焦点
|
|
||||||
const dataTabNames = Object.keys(state.dataTabs);
|
|
||||||
for (let i = 0; i < dataTabNames.length; i++) {
|
|
||||||
if (key == dataTabNames[i]) {
|
|
||||||
findParamInputRef.value[i].blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.findDialog.findParam = state.dataTabs[key].findParam;
|
|
||||||
state.findDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmFindDialog = () => {
|
|
||||||
state.dataTabs[state.activeName].findParam = state.findDialog.findParam;
|
|
||||||
state.dataTabs[state.activeName].findParamStr = JSON.stringify(state.findDialog.findParam);
|
|
||||||
state.findDialog.visible = false;
|
|
||||||
findCommand(state.activeName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const findCommand = async (key: string) => {
|
|
||||||
const dataTab = getNowDataTab();
|
|
||||||
const findParma = dataTab.findParam;
|
|
||||||
let filter, sort;
|
|
||||||
try {
|
|
||||||
filter = findParma.filter ? JSON.parse(findParma.filter) : {};
|
|
||||||
sort = findParma.sort ? JSON.parse(findParma.sort) : {};
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(t('mongo.findParamErrMsg'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const datas = await mongoApi.findCommand.request({
|
|
||||||
id: dataTab.mongoId,
|
|
||||||
database: dataTab.database,
|
|
||||||
collection: dataTab.collection,
|
|
||||||
filter,
|
|
||||||
sort,
|
|
||||||
limit: findParma.limit || 12,
|
|
||||||
skip: findParma.skip || 0,
|
|
||||||
});
|
|
||||||
state.dataTabs[key].datas = wrapDatas(datas);
|
|
||||||
|
|
||||||
// 获取coll stats
|
|
||||||
state.dataTabs[key].stats = await mongoApi.runCommand.request({
|
|
||||||
id: dataTab.mongoId,
|
|
||||||
database: dataTab.database,
|
|
||||||
command: [
|
|
||||||
{
|
|
||||||
collStats: dataTab.collection,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 包装mongo查询回来的对象,即将其都转为json字符串并用value属性值描述,方便显示
|
|
||||||
*/
|
|
||||||
const wrapDatas = (datas: any) => {
|
|
||||||
const wrapDatas = [] as any;
|
|
||||||
if (!datas) {
|
|
||||||
return wrapDatas;
|
|
||||||
}
|
|
||||||
for (let data of datas) {
|
|
||||||
wrapDatas.push({ value: JSON.stringify(data, null, 4) });
|
|
||||||
}
|
|
||||||
return wrapDatas;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showEditDocDialog = () => {
|
|
||||||
// tab数据中的第一个文档,因为该集合的文档都类似,故使用第一个文档赋值至需要新增的文档输入框,方便直接修改新增
|
|
||||||
const datasFirstDoc = state.dataTabs[state.activeName].datas[0];
|
|
||||||
let doc = '';
|
|
||||||
if (datasFirstDoc) {
|
|
||||||
// 移除_id字段,因为新增无需该字段
|
|
||||||
const docObj = JSON.parse(datasFirstDoc.value);
|
|
||||||
delete docObj['_id'];
|
|
||||||
doc = JSON.stringify(docObj, null, 4);
|
|
||||||
}
|
|
||||||
state.docEditDialog.doc = doc;
|
|
||||||
state.docEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEditDoc = async (item: any) => {
|
|
||||||
// 新增文档
|
|
||||||
if (!item) {
|
|
||||||
state.docEditDialog.isAdd = true;
|
|
||||||
showEditDocDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 编辑修改文档
|
|
||||||
// state.docEditDialog.item = item;
|
|
||||||
state.docEditDialog.isAdd = false;
|
|
||||||
state.docEditDialog.doc = item.value;
|
|
||||||
state.docEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSaveDoc = async () => {
|
|
||||||
if (state.docEditDialog.isAdd) {
|
|
||||||
let docObj;
|
|
||||||
try {
|
|
||||||
docObj = JSON.parse(state.docEditDialog.doc);
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(t('mongo.docErrMsg'));
|
|
||||||
}
|
|
||||||
const dataTab = getNowDataTab();
|
|
||||||
const res = await mongoApi.insertCommand.request({
|
|
||||||
id: dataTab.mongoId,
|
|
||||||
database: dataTab.database,
|
|
||||||
collection: dataTab.collection,
|
|
||||||
doc: docObj,
|
|
||||||
});
|
|
||||||
isTrue(res.InsertedID, 'mongo.insertFail');
|
|
||||||
ElMessage.success(t('mongo.insertSuccess'));
|
|
||||||
} else {
|
|
||||||
const docObj = parseDocJsonString(state.docEditDialog.doc);
|
|
||||||
const id = docObj._id;
|
|
||||||
notBlank(id, t('mongo.idNotExist'));
|
|
||||||
delete docObj['_id'];
|
|
||||||
const dataTab = getNowDataTab();
|
|
||||||
const res = await mongoApi.updateByIdCommand.request({
|
|
||||||
id: dataTab.mongoId,
|
|
||||||
database: dataTab.database,
|
|
||||||
collection: dataTab.collection,
|
|
||||||
docId: id,
|
|
||||||
update: { $set: docObj },
|
|
||||||
});
|
|
||||||
isTrue(res.ModifiedCount == 1, 'common.modifyFail');
|
|
||||||
useI18nSaveSuccessMsg();
|
|
||||||
}
|
|
||||||
findCommand(state.activeName);
|
|
||||||
state.docEditDialog.visible = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDeleteDoc = async (doc: string) => {
|
|
||||||
const docObj = parseDocJsonString(doc);
|
|
||||||
const id = docObj._id;
|
|
||||||
notBlank(id, t('mongo.idNotExist'));
|
|
||||||
const dataTab = getNowDataTab();
|
|
||||||
const res = await mongoApi.deleteByIdCommand.request({
|
|
||||||
id: dataTab.mongoId,
|
|
||||||
database: dataTab.database,
|
|
||||||
collection: dataTab.collection,
|
|
||||||
docId: id,
|
|
||||||
});
|
|
||||||
isTrue(res.DeletedCount == 1, 'common.deleteFail');
|
|
||||||
useI18nDeleteSuccessMsg();
|
|
||||||
findCommand(state.activeName);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将json字符串解析为json对象
|
|
||||||
*/
|
|
||||||
const parseDocJsonString = (doc: string) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(doc);
|
|
||||||
} catch (e) {
|
|
||||||
ElMessage.error(t('mongo.docParse2jsonFail'));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeDataTab = (targetName: string) => {
|
|
||||||
const tabNames = Object.keys(state.dataTabs);
|
|
||||||
let activeName = state.activeName;
|
|
||||||
tabNames.forEach((name, index) => {
|
|
||||||
if (name === targetName) {
|
|
||||||
const nextTab = tabNames[index + 1] || tabNames[index - 1];
|
|
||||||
if (nextTab) {
|
|
||||||
activeName = nextTab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
state.activeName = activeName;
|
|
||||||
delete state.dataTabs[targetName];
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNowDataTab = () => {
|
|
||||||
return state.dataTabs[state.activeName];
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.mongo-doc-btns {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
right: 3px;
|
|
||||||
top: 2px;
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mongo-data-tab {
|
|
||||||
.mongo-data-tab-data {
|
|
||||||
height: calc(100vh - 230px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs__header {
|
|
||||||
margin: 0 0 5px;
|
|
||||||
|
|
||||||
.el-tabs__item {
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
422
frontend/src/views/ops/mongo/resource/MongoDataOp.vue
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mongo-data-tab card h-full !p-1 w-full">
|
||||||
|
<el-row v-if="nowColl">
|
||||||
|
<el-descriptions class="!w-full" :column="10" size="small" border>
|
||||||
|
<!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
|
||||||
|
|
||||||
|
<el-descriptions-item label="ns" label-align="right">
|
||||||
|
{{ nowColl.stats?.ns }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="count" label-align="right">
|
||||||
|
{{ nowColl.stats?.count }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="avgObjSize" label-align="right">
|
||||||
|
{{ formatByteSize(nowColl.stats?.avgObjSize) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="size" label-align="right">
|
||||||
|
{{ formatByteSize(nowColl.stats?.size) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="totalSize" label-align="right">
|
||||||
|
{{ formatByteSize(nowColl.stats?.totalSize) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="storageSize" label-align="right">
|
||||||
|
{{ formatByteSize(nowColl.stats?.storageSize) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="freeStorageSize" label-align="right">
|
||||||
|
{{ formatByteSize(nowColl.stats?.freeStorageSize) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row type="flex">
|
||||||
|
<el-tabs @tab-remove="removeDataTab" class="!w-full ml-1" v-model="state.activeName">
|
||||||
|
<el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="2">
|
||||||
|
<div class="mt-1">
|
||||||
|
<el-link @click="findCommand(state.activeName)" icon="refresh" underline="never" class=""> </el-link>
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" underline="never"> </el-link>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="22">
|
||||||
|
<el-input
|
||||||
|
ref="findParamInputRef"
|
||||||
|
v-model="dt.findParamStr"
|
||||||
|
:placeholder="$t('mongo.queryParamPlaceholder')"
|
||||||
|
@focus="showFindDialog(dt.key)"
|
||||||
|
>
|
||||||
|
<template #prepend>{{ $t('mongo.queryParam') }}</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-scrollbar class="mongo-data-tab-data">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="6" v-for="item in dt.datas" :key="item">
|
||||||
|
<el-card :body-style="{ padding: '0px', position: 'relative' }">
|
||||||
|
<el-input type="textarea" v-model="item.value" :rows="10" />
|
||||||
|
<div style="padding: 3px; float: right" class="mr-1 mongo-doc-btns">
|
||||||
|
<div>
|
||||||
|
<el-link @click="onEditDoc(item)" underline="never" type="success" icon="MagicStick"></el-link>
|
||||||
|
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
|
||||||
|
<el-popconfirm @confirm="onDeleteDoc(item.value)" :title="$t('mongo.deleteDocConfirm')" width="160">
|
||||||
|
<template #reference>
|
||||||
|
<el-link v-auth="perms.delData" underline="never" type="danger" icon="DocumentDelete"> </el-link>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-dialog width="600px" title="find params" v-model="findDialog.visible">
|
||||||
|
<el-form label-width="auto">
|
||||||
|
<el-form-item label="filter">
|
||||||
|
<monaco-editor style="width: 100%" height="150px" ref="monacoEditorRef" v-model="findDialog.findParam.filter" language="json" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="sort">
|
||||||
|
<el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable auto-complete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="limit">
|
||||||
|
<el-input v-model.number="findDialog.findParam.limit" type="number" auto-complete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="skip">
|
||||||
|
<el-input v-model.number="findDialog.findParam.skip" type="number" auto-complete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button @click="findDialog.visible = false">{{ $t('common.cancel') }}</el-button>
|
||||||
|
<el-button @click="confirmFindDialog" type="primary">{{ $t('common.confirm') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
width="60%"
|
||||||
|
:title="`${state.docEditDialog.isAdd ? $t('common.add') : $t('common.edit')} '${state.activeName}' $t('mongo.doc')`"
|
||||||
|
v-model="docEditDialog.visible"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<monaco-editor v-model="docEditDialog.doc" language="json" />
|
||||||
|
<template #footer>
|
||||||
|
<div>
|
||||||
|
<el-button @click="docEditDialog.visible = false">{{ $t('common.cancel') }}</el-button>
|
||||||
|
<el-button v-auth="perms.saveData" @click="onSaveDoc" type="primary">{{ $t('common.confirm') }}</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineAsyncComponent, getCurrentInstance, inject, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { isTrue, notBlank } from '@/common/assert';
|
||||||
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useI18nDeleteSuccessMsg, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { mongoApi } from '@/views/ops/mongo/api';
|
||||||
|
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||||
|
import { MongoOpComp } from '@/views/ops/mongo/resource';
|
||||||
|
import { ResourceOpCtx } from '../../component/tag';
|
||||||
|
|
||||||
|
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const perms = {
|
||||||
|
saveData: 'mongo:data:save',
|
||||||
|
delData: 'mongo:data:del',
|
||||||
|
};
|
||||||
|
|
||||||
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||||
|
|
||||||
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
|
const findParamInputRef: any = ref(null);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
defaultExpendKey: [] as any,
|
||||||
|
tags: [],
|
||||||
|
mongoList: [] as any,
|
||||||
|
activeName: '', // 当前操作的tab
|
||||||
|
dataTabs: {} as any, // 数据tabs
|
||||||
|
findDialog: {
|
||||||
|
visible: false,
|
||||||
|
findParam: {
|
||||||
|
limit: 0,
|
||||||
|
skip: 0,
|
||||||
|
filter: '',
|
||||||
|
sort: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
docEditDialog: {
|
||||||
|
isAdd: true,
|
||||||
|
visible: false,
|
||||||
|
doc: '',
|
||||||
|
},
|
||||||
|
insertDocDialog: {
|
||||||
|
visible: false,
|
||||||
|
doc: '',
|
||||||
|
},
|
||||||
|
jsonEditorDialog: {
|
||||||
|
visible: false,
|
||||||
|
doc: '',
|
||||||
|
item: {} as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { findDialog, docEditDialog } = toRefs(state);
|
||||||
|
|
||||||
|
const nowColl = computed(() => {
|
||||||
|
return getNowDataTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
emits('init', { name: MongoOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||||
|
});
|
||||||
|
|
||||||
|
const changeCollection = async (id: any, schema: string, collection: string) => {
|
||||||
|
const label = `${id}:\`${schema}\`.${collection}`;
|
||||||
|
let dataTab = state.dataTabs[label];
|
||||||
|
if (!dataTab) {
|
||||||
|
// 默认查询参数
|
||||||
|
const findParam = {
|
||||||
|
filter: '{}',
|
||||||
|
sort: '{"_id": -1}',
|
||||||
|
skip: 0,
|
||||||
|
limit: 12,
|
||||||
|
};
|
||||||
|
state.dataTabs[label] = {
|
||||||
|
key: label,
|
||||||
|
label: label,
|
||||||
|
name: label,
|
||||||
|
mongoId: id,
|
||||||
|
database: schema,
|
||||||
|
collection,
|
||||||
|
datas: [],
|
||||||
|
findParamStr: JSON.stringify(findParam),
|
||||||
|
findParam,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
state.activeName = label;
|
||||||
|
findCommand(label);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFindDialog = (key: string) => {
|
||||||
|
// 获取当前tab的索引位置,将其输入框失去焦点,防止输入以及重复获取焦点
|
||||||
|
const dataTabNames = Object.keys(state.dataTabs);
|
||||||
|
for (let i = 0; i < dataTabNames.length; i++) {
|
||||||
|
if (key == dataTabNames[i]) {
|
||||||
|
findParamInputRef.value[i].blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.findDialog.findParam = state.dataTabs[key].findParam;
|
||||||
|
state.findDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmFindDialog = () => {
|
||||||
|
state.dataTabs[state.activeName].findParam = state.findDialog.findParam;
|
||||||
|
state.dataTabs[state.activeName].findParamStr = JSON.stringify(state.findDialog.findParam);
|
||||||
|
state.findDialog.visible = false;
|
||||||
|
findCommand(state.activeName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCommand = async (key: string) => {
|
||||||
|
const dataTab = getNowDataTab();
|
||||||
|
const findParma = dataTab.findParam;
|
||||||
|
let filter, sort;
|
||||||
|
try {
|
||||||
|
filter = findParma.filter ? JSON.parse(findParma.filter) : {};
|
||||||
|
sort = findParma.sort ? JSON.parse(findParma.sort) : {};
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(t('mongo.findParamErrMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const datas = await mongoApi.findCommand.request({
|
||||||
|
id: dataTab.mongoId,
|
||||||
|
database: dataTab.database,
|
||||||
|
collection: dataTab.collection,
|
||||||
|
filter,
|
||||||
|
sort,
|
||||||
|
limit: findParma.limit || 12,
|
||||||
|
skip: findParma.skip || 0,
|
||||||
|
});
|
||||||
|
state.dataTabs[key].datas = wrapDatas(datas);
|
||||||
|
|
||||||
|
// 获取coll stats
|
||||||
|
state.dataTabs[key].stats = await mongoApi.runCommand.request({
|
||||||
|
id: dataTab.mongoId,
|
||||||
|
database: dataTab.database,
|
||||||
|
command: [
|
||||||
|
{
|
||||||
|
collStats: dataTab.collection,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包装mongo查询回来的对象,即将其都转为json字符串并用value属性值描述,方便显示
|
||||||
|
*/
|
||||||
|
const wrapDatas = (datas: any) => {
|
||||||
|
const wrapDatas = [] as any;
|
||||||
|
if (!datas) {
|
||||||
|
return wrapDatas;
|
||||||
|
}
|
||||||
|
for (let data of datas) {
|
||||||
|
wrapDatas.push({ value: JSON.stringify(data, null, 4) });
|
||||||
|
}
|
||||||
|
return wrapDatas;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showEditDocDialog = () => {
|
||||||
|
// tab数据中的第一个文档,因为该集合的文档都类似,故使用第一个文档赋值至需要新增的文档输入框,方便直接修改新增
|
||||||
|
const datasFirstDoc = state.dataTabs[state.activeName].datas[0];
|
||||||
|
let doc = '';
|
||||||
|
if (datasFirstDoc) {
|
||||||
|
// 移除_id字段,因为新增无需该字段
|
||||||
|
const docObj = JSON.parse(datasFirstDoc.value);
|
||||||
|
delete docObj['_id'];
|
||||||
|
doc = JSON.stringify(docObj, null, 4);
|
||||||
|
}
|
||||||
|
state.docEditDialog.doc = doc;
|
||||||
|
state.docEditDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditDoc = async (item: any) => {
|
||||||
|
// 新增文档
|
||||||
|
if (!item) {
|
||||||
|
state.docEditDialog.isAdd = true;
|
||||||
|
showEditDocDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 编辑修改文档
|
||||||
|
// state.docEditDialog.item = item;
|
||||||
|
state.docEditDialog.isAdd = false;
|
||||||
|
state.docEditDialog.doc = item.value;
|
||||||
|
state.docEditDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSaveDoc = async () => {
|
||||||
|
if (state.docEditDialog.isAdd) {
|
||||||
|
let docObj;
|
||||||
|
try {
|
||||||
|
docObj = JSON.parse(state.docEditDialog.doc);
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(t('mongo.docErrMsg'));
|
||||||
|
}
|
||||||
|
const dataTab = getNowDataTab();
|
||||||
|
const res = await mongoApi.insertCommand.request({
|
||||||
|
id: dataTab.mongoId,
|
||||||
|
database: dataTab.database,
|
||||||
|
collection: dataTab.collection,
|
||||||
|
doc: docObj,
|
||||||
|
});
|
||||||
|
isTrue(res.InsertedID, 'mongo.insertFail');
|
||||||
|
ElMessage.success(t('mongo.insertSuccess'));
|
||||||
|
} else {
|
||||||
|
const docObj = parseDocJsonString(state.docEditDialog.doc);
|
||||||
|
const id = docObj._id;
|
||||||
|
notBlank(id, t('mongo.idNotExist'));
|
||||||
|
delete docObj['_id'];
|
||||||
|
const dataTab = getNowDataTab();
|
||||||
|
const res = await mongoApi.updateByIdCommand.request({
|
||||||
|
id: dataTab.mongoId,
|
||||||
|
database: dataTab.database,
|
||||||
|
collection: dataTab.collection,
|
||||||
|
docId: id,
|
||||||
|
update: { $set: docObj },
|
||||||
|
});
|
||||||
|
isTrue(res.ModifiedCount == 1, 'common.modifyFail');
|
||||||
|
useI18nSaveSuccessMsg();
|
||||||
|
}
|
||||||
|
findCommand(state.activeName);
|
||||||
|
state.docEditDialog.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteDoc = async (doc: string) => {
|
||||||
|
const docObj = parseDocJsonString(doc);
|
||||||
|
const id = docObj._id;
|
||||||
|
notBlank(id, t('mongo.idNotExist'));
|
||||||
|
const dataTab = getNowDataTab();
|
||||||
|
const res = await mongoApi.deleteByIdCommand.request({
|
||||||
|
id: dataTab.mongoId,
|
||||||
|
database: dataTab.database,
|
||||||
|
collection: dataTab.collection,
|
||||||
|
docId: id,
|
||||||
|
});
|
||||||
|
isTrue(res.DeletedCount == 1, 'common.deleteFail');
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
findCommand(state.activeName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将json字符串解析为json对象
|
||||||
|
*/
|
||||||
|
const parseDocJsonString = (doc: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(doc);
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(t('mongo.docParse2jsonFail'));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDataTab = (targetName: string) => {
|
||||||
|
const tabNames = Object.keys(state.dataTabs);
|
||||||
|
let activeName = state.activeName;
|
||||||
|
tabNames.forEach((name, index) => {
|
||||||
|
if (name === targetName) {
|
||||||
|
const nextTab = tabNames[index + 1] || tabNames[index - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
state.activeName = activeName;
|
||||||
|
delete state.dataTabs[targetName];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNowDataTab = () => {
|
||||||
|
return state.dataTabs[state.activeName];
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
changeCollection,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.mongo-doc-btns {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
right: 3px;
|
||||||
|
top: 2px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mongo-data-tab {
|
||||||
|
.mongo-data-tab-data {
|
||||||
|
height: calc(100vh - 230px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__header {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
28
frontend/src/views/ops/mongo/resource/NodeMongo.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #prefix="{ data }">
|
||||||
|
<el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon :name="ResourceTypeEnum.Mongo.extra.icon" :color="ResourceTypeEnum.Mongo.extra.iconColor" :size="13" />
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ data.params.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="url">
|
||||||
|
{{ data.params.uri }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
14
frontend/src/views/ops/mongo/resource/NodeMongoDb.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #suffix="{ data }">
|
||||||
|
<span>{{ formatByteSize(data.params.size) }}</span>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
103
frontend/src/views/ops/mongo/resource/index.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '../../component/tag';
|
||||||
|
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
import { mongoApi } from '../api';
|
||||||
|
|
||||||
|
const Icon = {
|
||||||
|
name: ResourceTypeEnum.Mongo.extra.icon,
|
||||||
|
color: ResourceTypeEnum.Mongo.extra.iconColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MongoList = defineAsyncComponent(() => import('../MongoList.vue'));
|
||||||
|
const MongoDataOp = defineAsyncComponent(() => import('./MongoDataOp.vue'));
|
||||||
|
|
||||||
|
const NodeMongo = defineAsyncComponent(() => import('./NodeMongo.vue'));
|
||||||
|
const NodeMongoDb = defineAsyncComponent(() => import('./NodeMongoDb.vue'));
|
||||||
|
|
||||||
|
export const MongoOpComp: ResourceComponentConfig = {
|
||||||
|
name: 'tag.mongoDataOp',
|
||||||
|
component: MongoDataOp,
|
||||||
|
icon: Icon,
|
||||||
|
};
|
||||||
|
|
||||||
|
// tagpath 节点类型
|
||||||
|
const NodeTypeMongoTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
parentNode.ctx?.addResourceComponent(MongoOpComp);
|
||||||
|
|
||||||
|
const res = await mongoApi.mongoList.request({ tagPath: parentNode.params.tagPath });
|
||||||
|
if (!res.total) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mongoInfos = res.list;
|
||||||
|
await sleep(100);
|
||||||
|
return mongoInfos?.map((x: any) => {
|
||||||
|
x.tagPath = parentNode.key;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeMongo).withParams(x).withNodeComponent(NodeMongo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const NodeTypeMongo = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const inst = parentNode.params;
|
||||||
|
// 点击mongo -> 加载mongo数据库列表
|
||||||
|
const res = await mongoApi.databases.request({ id: inst.id });
|
||||||
|
return res.Databases.map((x: any) => {
|
||||||
|
const database = x.Name;
|
||||||
|
return TagTreeNode.new(parentNode, `${inst.id}.${database}`, database, NodeTypeDbs)
|
||||||
|
.withParams({
|
||||||
|
id: inst.id,
|
||||||
|
database,
|
||||||
|
size: x.SizeOnDisk,
|
||||||
|
})
|
||||||
|
.withIcon({ name: 'Coin', color: '#67c23a' })
|
||||||
|
.withNodeComponent(NodeMongoDb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const NodeTypeDbs = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
// 点击数据库列表 -> 加载数据库下拥有的菜单列表
|
||||||
|
return [
|
||||||
|
TagTreeNode.new(parentNode, `${params.id}.${params.database}.mongo-coll`, 'mongo.coll', NodeTypeCollMenu)
|
||||||
|
.withIcon({ name: 'Document' })
|
||||||
|
.withParams(params),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const NodeTypeCollMenu = new NodeType(3).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const { id, database } = parentNode.params;
|
||||||
|
// 点击数据库集合节点 -> 加载集合列表
|
||||||
|
const colls = await mongoApi.collections.request({ id, database });
|
||||||
|
return colls.map((x: any) => {
|
||||||
|
return TagTreeNode.new(parentNode, `${id}.${database}.${x}`, x, NodeTypeColl)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({
|
||||||
|
id,
|
||||||
|
database,
|
||||||
|
collection: x,
|
||||||
|
})
|
||||||
|
.withIcon({ name: 'Document' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const NodeTypeColl = new NodeType(4).withNodeClickFunc(async (nodeData: TagTreeNode) => {
|
||||||
|
const compRef = await nodeData.ctx?.addResourceComponent(MongoOpComp);
|
||||||
|
const { id, database, collection } = nodeData.params;
|
||||||
|
compRef.changeCollection(id, database, collection);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: 4,
|
||||||
|
resourceType: TagResourceTypeEnum.Mongo.value,
|
||||||
|
rootNodeType: NodeTypeMongoTag,
|
||||||
|
manager: {
|
||||||
|
componentConf: {
|
||||||
|
component: MongoList,
|
||||||
|
icon: Icon,
|
||||||
|
name: 'mongo',
|
||||||
|
},
|
||||||
|
countKey: 'mongo',
|
||||||
|
permCode: 'mongo:manage:base',
|
||||||
|
},
|
||||||
|
} as ResourceConfig;
|
||||||
@@ -1,4 +1 @@
|
|||||||
export default {
|
export default {};
|
||||||
MongoList: () => import('@/views/ops/mongo/MongoList.vue'),
|
|
||||||
MongoDataOp: () => import('@/views/ops/mongo/MongoDataOp.vue'),
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,653 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="redis-data-op h-full">
|
|
||||||
<ResourceOpPanel>
|
|
||||||
<template #left>
|
|
||||||
<tag-tree
|
|
||||||
ref="tagTreeRef"
|
|
||||||
:default-expanded-keys="state.defaultExpendKey"
|
|
||||||
:resource-type="TagResourceTypeEnum.Redis.value"
|
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
|
||||||
>
|
|
||||||
<template #prefix="{ data }">
|
|
||||||
<span v-if="data.type.value == RedisNodeType.Redis">
|
|
||||||
<el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
|
|
||||||
<template #reference>
|
|
||||||
<SvgIcon name="icon redis/redis-color" :size="18" />
|
|
||||||
</template>
|
|
||||||
<template #default>
|
|
||||||
<el-descriptions :column="1" size="small">
|
|
||||||
<el-descriptions-item :label="$t('common.name')">
|
|
||||||
{{ data.params.name }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="mode">
|
|
||||||
{{ data.params.mode }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="host">
|
|
||||||
{{ data.params.host }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item :label="$t('common.remark')" label-align="right">
|
|
||||||
{{ data.params.remark }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SvgIcon v-if="data.type.value == RedisNodeType.Db" name="Coin" color="#67c23a" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #suffix="{ data }">
|
|
||||||
<span v-if="data.type.value == RedisNodeType.Db">{{ data.params.keys }}</span>
|
|
||||||
</template>
|
|
||||||
</tag-tree>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
|
||||||
<el-splitter>
|
|
||||||
<el-splitter-panel size="35%" max="50%">
|
|
||||||
<div class="key-list-vtree h-full card !p-1">
|
|
||||||
<el-scrollbar>
|
|
||||||
<el-row :gutter="5">
|
|
||||||
<el-col :span="2">
|
|
||||||
<el-input v-model="state.keySeparator" :placeholder="$t('redis.delimiter')" size="small" />
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="18">
|
|
||||||
<el-input
|
|
||||||
@clear="clear"
|
|
||||||
v-model="scanParam.match"
|
|
||||||
@keyup.enter.native="searchKey()"
|
|
||||||
:placeholder="$t('redis.keyMatchTips')"
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="4">
|
|
||||||
<el-button
|
|
||||||
:disabled="!scanParam.id || !scanParam.db"
|
|
||||||
@click="searchKey()"
|
|
||||||
type="success"
|
|
||||||
icon="search"
|
|
||||||
size="small"
|
|
||||||
plain
|
|
||||||
></el-button>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="5" class="mb-1 mt-1">
|
|
||||||
<el-col :span="19">
|
|
||||||
<el-button :disabled="!scanParam.id || !scanParam.db" @click="scan(true)" type="success" icon="more" size="small" plain>
|
|
||||||
{{ $t('redis.loadMore') }}
|
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<el-button
|
|
||||||
v-auth="'redis:data:save'"
|
|
||||||
:disabled="!scanParam.id || !scanParam.db"
|
|
||||||
@click="showNewKeyDialog"
|
|
||||||
type="primary"
|
|
||||||
icon="plus"
|
|
||||||
size="small"
|
|
||||||
plain
|
|
||||||
class="!ml-0.5"
|
|
||||||
>
|
|
||||||
{{ $t('redis.addKey') }}
|
|
||||||
</el-button>
|
|
||||||
|
|
||||||
<el-button
|
|
||||||
:disabled="!scanParam.id || !scanParam.db"
|
|
||||||
@click="flushDb"
|
|
||||||
type="danger"
|
|
||||||
plain
|
|
||||||
v-auth="'redis:data:del'"
|
|
||||||
size="small"
|
|
||||||
icon="delete"
|
|
||||||
class="!ml-0.5"
|
|
||||||
>
|
|
||||||
flush
|
|
||||||
</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="5">
|
|
||||||
<span class="mt-1" style="display: inline-block">keys:{{ state.dbsize }}</span>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-tree
|
|
||||||
ref="keyTreeRef"
|
|
||||||
:highlight-current="true"
|
|
||||||
:data="keyTreeData"
|
|
||||||
:props="treeProps"
|
|
||||||
:indent="8"
|
|
||||||
node-key="key"
|
|
||||||
:auto-expand-parent="false"
|
|
||||||
:default-expanded-keys="Array.from(state.keyTreeExpanded)"
|
|
||||||
@node-click="handleKeyTreeNodeClick"
|
|
||||||
@node-expand="keyTreeNodeExpand"
|
|
||||||
@node-collapse="keyTreeNodeCollapse"
|
|
||||||
@node-contextmenu="rightClickNode"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<span class="el-dropdown-link key-list-custom-node" :title="node.label">
|
|
||||||
<span v-if="data.type == 1">
|
|
||||||
<SvgIcon :size="15" :name="node.expanded ? 'folder-opened' : 'folder'" />
|
|
||||||
</span>
|
|
||||||
<span :class="'ml-1 ' + (data.type == 1 ? 'folder-label' : 'key-label')">
|
|
||||||
{{ node.label }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="!node.isLeaf" class="ml-1" style="font-weight: bold"> ({{ data.keyCount }}) </span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
</el-scrollbar>
|
|
||||||
|
|
||||||
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
|
||||||
</div>
|
|
||||||
</el-splitter-panel>
|
|
||||||
|
|
||||||
<el-splitter-panel>
|
|
||||||
<div class="h-full card !p-1 key-deatil">
|
|
||||||
<el-tabs class="h-full" @tab-remove="removeDataTab" v-model="state.activeName">
|
|
||||||
<el-tab-pane class="h-full" closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
|
||||||
<key-detail :redis="redisInst" :key-info="dt.keyInfo" @change-key="searchKey()" @del-key="delKey" />
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
</el-splitter-panel>
|
|
||||||
</el-splitter>
|
|
||||||
</template>
|
|
||||||
</ResourceOpPanel>
|
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 10px"></div>
|
|
||||||
|
|
||||||
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
|
||||||
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
|
|
||||||
<el-form-item prop="key" label="Key" required>
|
|
||||||
<el-input v-model.trim="newKeyDialog.keyInfo.key"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="type" :label="$t('common.type')">
|
|
||||||
<el-select v-model="newKeyDialog.keyInfo.type" default-first-option>
|
|
||||||
<el-option key="string" label="string" value="string"></el-option>
|
|
||||||
<el-option key="hash" label="hash" value="hash"></el-option>
|
|
||||||
<el-option key="set" label="set" value="set"></el-option>
|
|
||||||
<el-option key="zset" label="zset" value="zset"></el-option>
|
|
||||||
<el-option key="list" label="list" value="list"></el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="cancelNewKey()">{{ $t('common.cancel') }}</el-button>
|
|
||||||
<el-button v-auth="'redis:data:save'" type="primary" @click="newKey">{{ $t('common.confirm') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { redisApi } from './api';
|
|
||||||
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref, watch, useTemplateRef } from 'vue';
|
|
||||||
import { ElMessageBox } from 'element-plus';
|
|
||||||
import { isTrue, notNull } from '@/common/assert';
|
|
||||||
import { copyToClipboard } from '@/common/utils/string';
|
|
||||||
import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
|
|
||||||
import TagTree from '../component/TagTree.vue';
|
|
||||||
import { keysToTree, sortByTreeNodes, keysToList } from './utils';
|
|
||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
|
||||||
import { sleep } from '@/common/utils/loading';
|
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
|
||||||
import { RedisInst } from './redis';
|
|
||||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
|
||||||
import { Rules } from '@/common/rule';
|
|
||||||
import ResourceOpPanel from '../component/ResourceOpPanel.vue';
|
|
||||||
|
|
||||||
const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue'));
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const keyFormRules = {
|
|
||||||
key: [Rules.requiredInput('Key')],
|
|
||||||
};
|
|
||||||
|
|
||||||
const cmCopyKey = new ContextmenuItem('copyValue', 'Copy')
|
|
||||||
.withIcon('CopyDocument')
|
|
||||||
.withHideFunc((data: any) => !data.isLeaf)
|
|
||||||
.withOnClick(async (data: any) => await copyToClipboard(data.key));
|
|
||||||
|
|
||||||
const cmNewTabOpen = new ContextmenuItem('newTabOpenKey', 'redis.newTabOpen')
|
|
||||||
.withIcon('plus')
|
|
||||||
.withHideFunc((data: any) => !data.isLeaf)
|
|
||||||
.withOnClick((data: any) => showKeyDetail(data.key, true));
|
|
||||||
|
|
||||||
const cmDelKey = new ContextmenuItem('delKey', 'common.delete')
|
|
||||||
.withIcon('delete')
|
|
||||||
.withPermission('redis:data:del')
|
|
||||||
.withHideFunc((data: any) => !data.isLeaf)
|
|
||||||
.withOnClick((data: any) => delKey(data.key));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 树节点类型
|
|
||||||
*/
|
|
||||||
class RedisNodeType {
|
|
||||||
static Redis = 1;
|
|
||||||
static Db = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tagpath 节点类型
|
|
||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const res = await redisApi.redisList.request({ tagPath: parentNode.key });
|
|
||||||
if (!res.total) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const redisInfos = res.list;
|
|
||||||
await sleep(100);
|
|
||||||
return redisInfos.map((x: any) => {
|
|
||||||
x.tagPath = parentNode.key;
|
|
||||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// redis实例节点类型
|
|
||||||
const NodeTypeRedis = new NodeType(RedisNodeType.Redis).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const redisInfo = parentNode.params;
|
|
||||||
|
|
||||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
|
||||||
return new TagTreeNode(x, `db${x}`, NodeTypeDb).withIsLeaf(true).withParams({
|
|
||||||
id: redisInfo.id,
|
|
||||||
db: x,
|
|
||||||
name: `db${x}`,
|
|
||||||
keys: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (redisInfo.mode == 'cluster') {
|
|
||||||
return dbs;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: 'Keyspace' });
|
|
||||||
for (let db in res.Keyspace) {
|
|
||||||
for (let d of dbs) {
|
|
||||||
if (db == d.params.name) {
|
|
||||||
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 替换label
|
|
||||||
dbs.forEach((e: any) => {
|
|
||||||
e.label = `${e.params.name}`;
|
|
||||||
});
|
|
||||||
return dbs;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 库节点类型
|
|
||||||
const NodeTypeDb = new NodeType(RedisNodeType.Db).withNodeClickFunc((nodeData: TagTreeNode) => {
|
|
||||||
resetScanParam();
|
|
||||||
state.scanParam.id = nodeData.params.id;
|
|
||||||
state.scanParam.db = nodeData.params.db;
|
|
||||||
|
|
||||||
redisInst.value.id = nodeData.params.id;
|
|
||||||
redisInst.value.db = Number.parseInt(nodeData.params.db);
|
|
||||||
|
|
||||||
scan();
|
|
||||||
});
|
|
||||||
|
|
||||||
const treeProps = {
|
|
||||||
label: 'name',
|
|
||||||
children: 'children',
|
|
||||||
isLeaf: 'leaf',
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultCount = 250;
|
|
||||||
|
|
||||||
const contextmenuRef = ref();
|
|
||||||
const keyTreeRef: any = ref(null);
|
|
||||||
const tagTreeRef: any = ref(null);
|
|
||||||
const keyFormRef = useTemplateRef('keyForm');
|
|
||||||
|
|
||||||
const redisInst: Ref<RedisInst> = ref(new RedisInst());
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
defaultExpendKey: [] as any,
|
|
||||||
tags: [],
|
|
||||||
redisList: [] as any,
|
|
||||||
dbList: [],
|
|
||||||
keyTreeHeight: '100px',
|
|
||||||
loadingKeyTree: false,
|
|
||||||
keys: [] as any,
|
|
||||||
keySeparator: ':',
|
|
||||||
keyTreeData: [] as any,
|
|
||||||
keyTreeExpanded: new Set(),
|
|
||||||
activeName: '',
|
|
||||||
dataTabs: {} as any,
|
|
||||||
rightClickNode: {} as any,
|
|
||||||
scanParam: {
|
|
||||||
id: null as any,
|
|
||||||
mode: '',
|
|
||||||
db: null as any,
|
|
||||||
match: null,
|
|
||||||
count: defaultCount,
|
|
||||||
cursor: {},
|
|
||||||
},
|
|
||||||
newKeyDialog: {
|
|
||||||
visible: false,
|
|
||||||
keyInfo: {
|
|
||||||
type: 'string',
|
|
||||||
timed: -1,
|
|
||||||
key: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dbsize: 0,
|
|
||||||
contextmenu: {
|
|
||||||
dropdown: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
items: [cmCopyKey, cmNewTabOpen, cmDelKey],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { scanParam, keyTreeData, newKeyDialog } = toRefs(state);
|
|
||||||
|
|
||||||
const autoOpenResourceStore = useAutoOpenResource();
|
|
||||||
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
autoOpenRedis(autoOpenResource.value.redisCodePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => autoOpenResource.value.redisCodePath,
|
|
||||||
(codePath: any) => {
|
|
||||||
autoOpenRedis(codePath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const autoOpenRedis = (codePath: string) => {
|
|
||||||
if (!codePath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const typeAndCodes: any = getTagTypeCodeByPath(codePath);
|
|
||||||
const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
|
|
||||||
|
|
||||||
const redisCode = typeAndCodes[TagResourceTypeEnum.Redis.value][0];
|
|
||||||
state.defaultExpendKey = [tagPath, redisCode];
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// 置空
|
|
||||||
autoOpenResourceStore.setRedisCodePath('');
|
|
||||||
tagTreeRef.value.setCurrentKey(redisCode);
|
|
||||||
}, 600);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scan = async (appendKey = false) => {
|
|
||||||
isTrue(state.scanParam.id != null, 'redis.redisSelectErr');
|
|
||||||
|
|
||||||
const match: string = state.scanParam.match || '';
|
|
||||||
if (!match) {
|
|
||||||
state.scanParam.count = defaultCount;
|
|
||||||
} else if (match.indexOf('*') != -1) {
|
|
||||||
const dbsize = state.dbsize;
|
|
||||||
// 如果为模糊搜索,并且搜索的key模式大于指定字符数,则将count设大点scan
|
|
||||||
if (match.length > 10) {
|
|
||||||
state.scanParam.count = dbsize > 100000 ? Math.floor(dbsize / 10) : 1000;
|
|
||||||
} else {
|
|
||||||
state.scanParam.count = defaultCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scanParam = { ...state.scanParam };
|
|
||||||
// 集群模式count设小点,因为后端会从所有master节点scan一遍然后合并结果,默认假设redis集群有3个master
|
|
||||||
if (scanParam.mode == 'cluster') {
|
|
||||||
scanParam.count = Math.floor(state.scanParam.count / 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
state.loadingKeyTree = true;
|
|
||||||
const res = await redisApi.scan.request(scanParam);
|
|
||||||
// 追加key,则将新key合并至原keys(加载更多)
|
|
||||||
if (appendKey) {
|
|
||||||
state.keys = [...state.keys, ...res.keys];
|
|
||||||
} else {
|
|
||||||
state.keys = res.keys;
|
|
||||||
}
|
|
||||||
setKeyList(state.keys);
|
|
||||||
state.dbsize = res.dbSize;
|
|
||||||
state.scanParam.cursor = res.cursor;
|
|
||||||
} finally {
|
|
||||||
state.loadingKeyTree = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setKeyList = (keys: any) => {
|
|
||||||
state.keyTreeData = state.keySeparator ? keysToTree(keys, state.keySeparator, state.keyTreeExpanded) : keysToList(keys);
|
|
||||||
nextTick(() => {
|
|
||||||
// key长度小于指定数量,则展开所有节点
|
|
||||||
if (keys.length <= 20) {
|
|
||||||
expandAllKeyNode(state.keyTreeData);
|
|
||||||
}
|
|
||||||
|
|
||||||
sortByTreeNodes(keyTreeRef.value.root.childNodes);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 展开所有节点
|
|
||||||
const expandAllKeyNode = (nodes: any) => {
|
|
||||||
for (let node of nodes) {
|
|
||||||
if (!node.children) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
state.keyTreeExpanded.add(node.key);
|
|
||||||
for (let i = 0; i < node.children.length; i++) {
|
|
||||||
expandAllKeyNode(node.children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyTreeNodeClick = async (data: any) => {
|
|
||||||
// 关闭可能存在的右击菜单
|
|
||||||
contextmenuRef.value.closeContextmenu();
|
|
||||||
// 目录则不做处理
|
|
||||||
if (data.type == 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showKeyDetail(data.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showKeyDetail = async (key: any, newTab = false) => {
|
|
||||||
let keyInfo;
|
|
||||||
if (typeof key == 'object') {
|
|
||||||
keyInfo = key;
|
|
||||||
} else {
|
|
||||||
if (state.dataTabs[key]) {
|
|
||||||
state.activeName = key;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await redisApi.keyInfo.request({ id: state.scanParam.id, db: state.scanParam.db, key: key });
|
|
||||||
keyInfo = {
|
|
||||||
key: key,
|
|
||||||
type: res.type,
|
|
||||||
timed: res.ttl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let label = keyInfo.key;
|
|
||||||
if (label.length > 40) {
|
|
||||||
label = label.slice(0, 40) + '...';
|
|
||||||
}
|
|
||||||
const dataTab = {
|
|
||||||
key: keyInfo.key,
|
|
||||||
label,
|
|
||||||
keyInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!newTab) {
|
|
||||||
delete state.dataTabs[state.activeName];
|
|
||||||
}
|
|
||||||
|
|
||||||
state.dataTabs[keyInfo.key] = dataTab;
|
|
||||||
state.activeName = keyInfo.key;
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeDataTab = (targetName: string) => {
|
|
||||||
const tabNames = Object.keys(state.dataTabs);
|
|
||||||
let activeName = state.activeName;
|
|
||||||
tabNames.forEach((name, index) => {
|
|
||||||
if (name === targetName) {
|
|
||||||
const nextTab = tabNames[index + 1] || tabNames[index - 1];
|
|
||||||
if (nextTab) {
|
|
||||||
activeName = nextTab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
state.activeName = activeName;
|
|
||||||
delete state.dataTabs[targetName];
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyTreeNodeExpand = (data: any, node: any) => {
|
|
||||||
state.keyTreeExpanded.add(data.key);
|
|
||||||
// async sort nodes
|
|
||||||
if (!node.customSorted) {
|
|
||||||
node.customSorted = true;
|
|
||||||
sortByTreeNodes(node.childNodes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyTreeNodeCollapse = (data: any) => {
|
|
||||||
state.keyTreeExpanded.delete(data.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rightClickNode = (event: any, data: any, node: any) => {
|
|
||||||
const { clientX, clientY } = event;
|
|
||||||
state.contextmenu.dropdown.x = clientX;
|
|
||||||
state.contextmenu.dropdown.y = clientY;
|
|
||||||
contextmenuRef.value.openContextmenu(node);
|
|
||||||
keyTreeRef.value.setCurrentKey(node.key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchKey = async () => {
|
|
||||||
state.scanParam.cursor = {};
|
|
||||||
await scan(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
resetScanParam();
|
|
||||||
if (state.scanParam.id) {
|
|
||||||
scan();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetScanParam = () => {
|
|
||||||
state.scanParam.match = null;
|
|
||||||
state.scanParam.cursor = {};
|
|
||||||
state.keyTreeExpanded.clear();
|
|
||||||
state.dataTabs = {};
|
|
||||||
state.activeName = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const showNewKeyDialog = () => {
|
|
||||||
notNull(state.scanParam.id, t('redis.redisSelectErr'));
|
|
||||||
resetNewKeyInfo();
|
|
||||||
state.newKeyDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const flushDb = () => {
|
|
||||||
ElMessageBox.confirm(t('redis.flushDbTips', { db: state.scanParam.db }), t('common.hint'), {
|
|
||||||
confirmButtonText: t('common.confirm'),
|
|
||||||
cancelButtonText: t('common.cancel'),
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// FLUSHDB [ASYNC | SYNC]
|
|
||||||
redisInst.value.runCmd(['FLUSHDB']).then(() => {
|
|
||||||
useI18nOperateSuccessMsg();
|
|
||||||
searchKey();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelNewKey = () => {
|
|
||||||
resetNewKeyInfo();
|
|
||||||
state.newKeyDialog.visible = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const newKey = async () => {
|
|
||||||
await useI18nFormValidate(keyFormRef);
|
|
||||||
const keyInfo = state.newKeyDialog.keyInfo;
|
|
||||||
const key = keyInfo.key;
|
|
||||||
|
|
||||||
showKeyDetail(
|
|
||||||
{
|
|
||||||
...keyInfo,
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
state.newKeyDialog.visible = false;
|
|
||||||
|
|
||||||
// 添加新增的key至key tree
|
|
||||||
state.keys.push(key);
|
|
||||||
setKeyList(state.keys);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetNewKeyInfo = () => {
|
|
||||||
state.newKeyDialog.keyInfo.key = '';
|
|
||||||
state.newKeyDialog.keyInfo.type = 'string';
|
|
||||||
state.newKeyDialog.keyInfo.timed = -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const delKey = async (key: string) => {
|
|
||||||
await useI18nDeleteConfirm(key);
|
|
||||||
// DEL key [key ...]
|
|
||||||
await redisInst.value.runCmd(['DEL', key]);
|
|
||||||
useI18nDeleteSuccessMsg();
|
|
||||||
searchKey();
|
|
||||||
|
|
||||||
removeDataTab(key);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.key-deatil {
|
|
||||||
.el-tabs__header {
|
|
||||||
background-color: var(--el-color-white);
|
|
||||||
border-bottom: 1px solid var(--el-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.el-tabs__item) {
|
|
||||||
padding: 0 10px;
|
|
||||||
height: 29px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.el-tabs__nav-next) {
|
|
||||||
line-height: 29px;
|
|
||||||
}
|
|
||||||
::v-deep(.el-tabs__nav-prev) {
|
|
||||||
line-height: 29px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.redis-data-op {
|
|
||||||
.key-list-vtree .folder-label {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-list-vtree .key-label {
|
|
||||||
color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.key-list-vtree .key-list-custom-node {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
/*note the following 2 items should be same value, may not consist with itemSize*/
|
|
||||||
height: 22px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
35
frontend/src/views/ops/redis/resource/NodeRedis.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #prefix="{ data }">
|
||||||
|
<el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon :name="ResourceTypeEnum.Redis.extra.icon" :color="ResourceTypeEnum.Redis.extra.iconColor" :size="13" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ data.params.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="mode">
|
||||||
|
{{ data.params.mode }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="host">
|
||||||
|
{{ data.params.host }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('common.remark')" label-align="right">
|
||||||
|
{{ data.params.remark }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
16
frontend/src/views/ops/redis/resource/NodeRedisDb.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon name="Coin" color="#67c23a" />
|
||||||
|
</template>
|
||||||
|
<template #suffix="{ node, data }">
|
||||||
|
<span>{{ data.params.keys }}</span>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
526
frontend/src/views/ops/redis/resource/RedisDataOp.vue
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
<template>
|
||||||
|
<div class="redis-data-op h-full">
|
||||||
|
<el-splitter>
|
||||||
|
<el-splitter-panel size="35%" max="50%">
|
||||||
|
<div class="key-list-vtree h-full card !p-1">
|
||||||
|
<el-scrollbar>
|
||||||
|
<el-row :gutter="5">
|
||||||
|
<el-col :span="2">
|
||||||
|
<el-input v-model="state.keySeparator" :placeholder="$t('redis.delimiter')" size="small" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="18">
|
||||||
|
<el-input
|
||||||
|
@clear="clear"
|
||||||
|
v-model="scanParam.match"
|
||||||
|
@keyup.enter.native="searchKey()"
|
||||||
|
:placeholder="$t('redis.keyMatchTips')"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="4">
|
||||||
|
<el-button
|
||||||
|
:disabled="!scanParam.id || !scanParam.db"
|
||||||
|
@click="searchKey()"
|
||||||
|
type="success"
|
||||||
|
icon="search"
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
></el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="5" class="mb-1 mt-1">
|
||||||
|
<el-col :span="19">
|
||||||
|
<el-button :disabled="!scanParam.id || !scanParam.db" @click="scan(true)" type="success" icon="more" size="small" plain>
|
||||||
|
{{ $t('redis.loadMore') }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-auth="'redis:data:save'"
|
||||||
|
:disabled="!scanParam.id || !scanParam.db"
|
||||||
|
@click="showNewKeyDialog"
|
||||||
|
type="primary"
|
||||||
|
icon="plus"
|
||||||
|
size="small"
|
||||||
|
plain
|
||||||
|
class="!ml-0.5"
|
||||||
|
>
|
||||||
|
{{ $t('redis.addKey') }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
:disabled="!scanParam.id || !scanParam.db"
|
||||||
|
@click="flushDb"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
v-auth="'redis:data:del'"
|
||||||
|
size="small"
|
||||||
|
icon="delete"
|
||||||
|
class="!ml-0.5"
|
||||||
|
>
|
||||||
|
flush
|
||||||
|
</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="5">
|
||||||
|
<span class="mt-1" style="display: inline-block">keys:{{ state.dbsize }}</span>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-tree
|
||||||
|
ref="keyTreeRef"
|
||||||
|
:highlight-current="true"
|
||||||
|
:data="keyTreeData"
|
||||||
|
:props="treeProps"
|
||||||
|
:indent="8"
|
||||||
|
node-key="key"
|
||||||
|
:auto-expand-parent="false"
|
||||||
|
:default-expanded-keys="Array.from(state.keyTreeExpanded)"
|
||||||
|
@node-click="handleKeyTreeNodeClick"
|
||||||
|
@node-expand="keyTreeNodeExpand"
|
||||||
|
@node-collapse="keyTreeNodeCollapse"
|
||||||
|
@node-contextmenu="rightClickNode"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="el-dropdown-link key-list-custom-node" :title="node.label">
|
||||||
|
<span v-if="data.type == 1">
|
||||||
|
<SvgIcon :size="15" :name="node.expanded ? 'folder-opened' : 'folder'" />
|
||||||
|
</span>
|
||||||
|
<span :class="'ml-1 ' + (data.type == 1 ? 'folder-label' : 'key-label')">
|
||||||
|
{{ node.label }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="!node.isLeaf" class="ml-1" style="font-weight: bold"> ({{ data.keyCount }}) </span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
|
<contextmenu :dropdown="state.contextmenu.dropdown" :items="state.contextmenu.items" ref="contextmenuRef" />
|
||||||
|
</div>
|
||||||
|
</el-splitter-panel>
|
||||||
|
|
||||||
|
<el-splitter-panel>
|
||||||
|
<div class="h-full card !p-1 key-deatil">
|
||||||
|
<el-tabs class="h-full" @tab-remove="removeDataTab" v-model="state.activeName">
|
||||||
|
<el-tab-pane class="h-full" closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
|
||||||
|
<key-detail :redis="redisInst" :key-info="dt.keyInfo" @change-key="searchKey()" @del-key="delKey" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</el-splitter-panel>
|
||||||
|
</el-splitter>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-top: 10px"></div>
|
||||||
|
|
||||||
|
<el-dialog :title="$t('redis.addKey')" v-model="newKeyDialog.visible" width="500px" :destroy-on-close="true" :close-on-click-modal="false">
|
||||||
|
<el-form ref="keyForm" label-width="auto" :rules="keyFormRules" :model="newKeyDialog.keyInfo">
|
||||||
|
<el-form-item prop="key" label="Key" required>
|
||||||
|
<el-input v-model.trim="newKeyDialog.keyInfo.key"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="type" :label="$t('common.type')">
|
||||||
|
<el-select v-model="newKeyDialog.keyInfo.type" default-first-option>
|
||||||
|
<el-option key="string" label="string" value="string"></el-option>
|
||||||
|
<el-option key="hash" label="hash" value="hash"></el-option>
|
||||||
|
<el-option key="set" label="set" value="set"></el-option>
|
||||||
|
<el-option key="zset" label="zset" value="zset"></el-option>
|
||||||
|
<el-option key="list" label="list" value="list"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="cancelNewKey()">{{ $t('common.cancel') }}</el-button>
|
||||||
|
<el-button v-auth="'redis:data:save'" type="primary" @click="newKey">{{ $t('common.confirm') }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { redisApi } from '../api';
|
||||||
|
import { ref, defineAsyncComponent, toRefs, reactive, onMounted, nextTick, Ref, watch, useTemplateRef, getCurrentInstance } from 'vue';
|
||||||
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import { isTrue, notNull } from '@/common/assert';
|
||||||
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
|
import { keysToTree, sortByTreeNodes, keysToList } from '../utils';
|
||||||
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
import { RedisInst } from '../redis';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { Rules } from '@/common/rule';
|
||||||
|
import { RedisOpComp } from '@/views/ops/redis/resource';
|
||||||
|
|
||||||
|
const KeyDetail = defineAsyncComponent(() => import('../KeyDetail.vue'));
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
|
const keyFormRules = {
|
||||||
|
key: [Rules.requiredInput('Key')],
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmCopyKey = new ContextmenuItem('copyValue', 'Copy')
|
||||||
|
.withIcon('CopyDocument')
|
||||||
|
.withHideFunc((data: any) => !data.isLeaf)
|
||||||
|
.withOnClick(async (data: any) => await copyToClipboard(data.key));
|
||||||
|
|
||||||
|
const cmNewTabOpen = new ContextmenuItem('newTabOpenKey', 'redis.newTabOpen')
|
||||||
|
.withIcon('plus')
|
||||||
|
.withHideFunc((data: any) => !data.isLeaf)
|
||||||
|
.withOnClick((data: any) => showKeyDetail(data.key, true));
|
||||||
|
|
||||||
|
const cmDelKey = new ContextmenuItem('delKey', 'common.delete')
|
||||||
|
.withIcon('delete')
|
||||||
|
.withPermission('redis:data:del')
|
||||||
|
.withHideFunc((data: any) => !data.isLeaf)
|
||||||
|
.withOnClick((data: any) => delKey(data.key));
|
||||||
|
|
||||||
|
const treeProps = {
|
||||||
|
label: 'name',
|
||||||
|
children: 'children',
|
||||||
|
isLeaf: 'leaf',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultCount = 250;
|
||||||
|
|
||||||
|
const contextmenuRef = ref();
|
||||||
|
const keyTreeRef: any = ref(null);
|
||||||
|
const keyFormRef = useTemplateRef('keyForm');
|
||||||
|
|
||||||
|
const redisInst: Ref<RedisInst> = ref(new RedisInst());
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
defaultExpendKey: [] as any,
|
||||||
|
tags: [],
|
||||||
|
redisList: [] as any,
|
||||||
|
dbList: [],
|
||||||
|
keyTreeHeight: '100px',
|
||||||
|
loadingKeyTree: false,
|
||||||
|
keys: [] as any,
|
||||||
|
keySeparator: ':',
|
||||||
|
keyTreeData: [] as any,
|
||||||
|
keyTreeExpanded: new Set(),
|
||||||
|
activeName: '',
|
||||||
|
dataTabs: {} as any,
|
||||||
|
rightClickNode: {} as any,
|
||||||
|
scanParam: {
|
||||||
|
id: null as any,
|
||||||
|
mode: '',
|
||||||
|
db: null as any,
|
||||||
|
match: null,
|
||||||
|
count: defaultCount,
|
||||||
|
cursor: {},
|
||||||
|
},
|
||||||
|
newKeyDialog: {
|
||||||
|
visible: false,
|
||||||
|
keyInfo: {
|
||||||
|
type: 'string',
|
||||||
|
timed: -1,
|
||||||
|
key: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dbsize: 0,
|
||||||
|
contextmenu: {
|
||||||
|
dropdown: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
items: [cmCopyKey, cmNewTabOpen, cmDelKey],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { scanParam, keyTreeData, newKeyDialog } = toRefs(state);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
emits('init', { name: RedisOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||||
|
});
|
||||||
|
|
||||||
|
const scan = async (appendKey = false) => {
|
||||||
|
isTrue(state.scanParam.id != null, 'redis.redisSelectErr');
|
||||||
|
|
||||||
|
const match: string = state.scanParam.match || '';
|
||||||
|
if (!match) {
|
||||||
|
state.scanParam.count = defaultCount;
|
||||||
|
} else if (match.indexOf('*') != -1) {
|
||||||
|
const dbsize = state.dbsize;
|
||||||
|
// 如果为模糊搜索,并且搜索的key模式大于指定字符数,则将count设大点scan
|
||||||
|
if (match.length > 10) {
|
||||||
|
state.scanParam.count = dbsize > 100000 ? Math.floor(dbsize / 10) : 1000;
|
||||||
|
} else {
|
||||||
|
state.scanParam.count = defaultCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanParam = { ...state.scanParam };
|
||||||
|
// 集群模式count设小点,因为后端会从所有master节点scan一遍然后合并结果,默认假设redis集群有3个master
|
||||||
|
if (scanParam.mode == 'cluster') {
|
||||||
|
scanParam.count = Math.floor(state.scanParam.count / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.loadingKeyTree = true;
|
||||||
|
const res = await redisApi.scan.request(scanParam);
|
||||||
|
// 追加key,则将新key合并至原keys(加载更多)
|
||||||
|
if (appendKey) {
|
||||||
|
state.keys = [...state.keys, ...res.keys];
|
||||||
|
} else {
|
||||||
|
state.keys = res.keys;
|
||||||
|
}
|
||||||
|
setKeyList(state.keys);
|
||||||
|
state.dbsize = res.dbSize;
|
||||||
|
state.scanParam.cursor = res.cursor;
|
||||||
|
} finally {
|
||||||
|
state.loadingKeyTree = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setKeyList = (keys: any) => {
|
||||||
|
state.keyTreeData = state.keySeparator ? keysToTree(keys, state.keySeparator, state.keyTreeExpanded) : keysToList(keys);
|
||||||
|
nextTick(() => {
|
||||||
|
// key长度小于指定数量,则展开所有节点
|
||||||
|
if (keys.length <= 20) {
|
||||||
|
expandAllKeyNode(state.keyTreeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByTreeNodes(keyTreeRef.value.root.childNodes);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 展开所有节点
|
||||||
|
const expandAllKeyNode = (nodes: any) => {
|
||||||
|
for (let node of nodes) {
|
||||||
|
if (!node.children) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
state.keyTreeExpanded.add(node.key);
|
||||||
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
|
expandAllKeyNode(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyTreeNodeClick = async (data: any) => {
|
||||||
|
// 关闭可能存在的右击菜单
|
||||||
|
contextmenuRef.value.closeContextmenu();
|
||||||
|
// 目录则不做处理
|
||||||
|
if (data.type == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showKeyDetail(data.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showKeyDetail = async (key: any, newTab = false) => {
|
||||||
|
let keyInfo;
|
||||||
|
if (typeof key == 'object') {
|
||||||
|
keyInfo = key;
|
||||||
|
} else {
|
||||||
|
if (state.dataTabs[key]) {
|
||||||
|
state.activeName = key;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await redisApi.keyInfo.request({ id: state.scanParam.id, db: state.scanParam.db, key: key });
|
||||||
|
keyInfo = {
|
||||||
|
key: key,
|
||||||
|
type: res.type,
|
||||||
|
timed: res.ttl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = keyInfo.key;
|
||||||
|
if (label.length > 40) {
|
||||||
|
label = label.slice(0, 40) + '...';
|
||||||
|
}
|
||||||
|
const dataTab = {
|
||||||
|
key: keyInfo.key,
|
||||||
|
label,
|
||||||
|
keyInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!newTab) {
|
||||||
|
delete state.dataTabs[state.activeName];
|
||||||
|
}
|
||||||
|
|
||||||
|
state.dataTabs[keyInfo.key] = dataTab;
|
||||||
|
state.activeName = keyInfo.key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDataTab = (targetName: string) => {
|
||||||
|
const tabNames = Object.keys(state.dataTabs);
|
||||||
|
let activeName = state.activeName;
|
||||||
|
tabNames.forEach((name, index) => {
|
||||||
|
if (name === targetName) {
|
||||||
|
const nextTab = tabNames[index + 1] || tabNames[index - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
state.activeName = activeName;
|
||||||
|
delete state.dataTabs[targetName];
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyTreeNodeExpand = (data: any, node: any) => {
|
||||||
|
state.keyTreeExpanded.add(data.key);
|
||||||
|
// async sort nodes
|
||||||
|
if (!node.customSorted) {
|
||||||
|
node.customSorted = true;
|
||||||
|
sortByTreeNodes(node.childNodes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyTreeNodeCollapse = (data: any) => {
|
||||||
|
state.keyTreeExpanded.delete(data.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rightClickNode = (event: any, data: any, node: any) => {
|
||||||
|
const { clientX, clientY } = event;
|
||||||
|
state.contextmenu.dropdown.x = clientX;
|
||||||
|
state.contextmenu.dropdown.y = clientY;
|
||||||
|
contextmenuRef.value.openContextmenu(node);
|
||||||
|
keyTreeRef.value.setCurrentKey(node.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchKey = async () => {
|
||||||
|
state.scanParam.cursor = {};
|
||||||
|
await scan(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
resetScanParam();
|
||||||
|
if (state.scanParam.id) {
|
||||||
|
scan();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetScanParam = () => {
|
||||||
|
state.scanParam.match = null;
|
||||||
|
state.scanParam.cursor = {};
|
||||||
|
state.keyTreeExpanded.clear();
|
||||||
|
state.dataTabs = {};
|
||||||
|
state.activeName = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const showNewKeyDialog = () => {
|
||||||
|
notNull(state.scanParam.id, t('redis.redisSelectErr'));
|
||||||
|
resetNewKeyInfo();
|
||||||
|
state.newKeyDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushDb = () => {
|
||||||
|
ElMessageBox.confirm(t('redis.flushDbTips', { db: state.scanParam.db }), t('common.hint'), {
|
||||||
|
confirmButtonText: t('common.confirm'),
|
||||||
|
cancelButtonText: t('common.cancel'),
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// FLUSHDB [ASYNC | SYNC]
|
||||||
|
redisInst.value.runCmd(['FLUSHDB']).then(() => {
|
||||||
|
useI18nOperateSuccessMsg();
|
||||||
|
searchKey();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelNewKey = () => {
|
||||||
|
resetNewKeyInfo();
|
||||||
|
state.newKeyDialog.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newKey = async () => {
|
||||||
|
await useI18nFormValidate(keyFormRef);
|
||||||
|
const keyInfo = state.newKeyDialog.keyInfo;
|
||||||
|
const key = keyInfo.key;
|
||||||
|
|
||||||
|
showKeyDetail(
|
||||||
|
{
|
||||||
|
...keyInfo,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
state.newKeyDialog.visible = false;
|
||||||
|
|
||||||
|
// 添加新增的key至key tree
|
||||||
|
state.keys.push(key);
|
||||||
|
setKeyList(state.keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetNewKeyInfo = () => {
|
||||||
|
state.newKeyDialog.keyInfo.key = '';
|
||||||
|
state.newKeyDialog.keyInfo.type = 'string';
|
||||||
|
state.newKeyDialog.keyInfo.timed = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const delKey = async (key: string) => {
|
||||||
|
await useI18nDeleteConfirm(key);
|
||||||
|
// DEL key [key ...]
|
||||||
|
await redisInst.value.runCmd(['DEL', key]);
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
searchKey();
|
||||||
|
|
||||||
|
removeDataTab(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDbClick = async (dbInfo: any) => {
|
||||||
|
if (state.scanParam.db == dbInfo.db) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetScanParam();
|
||||||
|
|
||||||
|
state.scanParam.id = dbInfo.id;
|
||||||
|
state.scanParam.mode = dbInfo.mode;
|
||||||
|
state.scanParam.db = dbInfo.db;
|
||||||
|
|
||||||
|
redisInst.value.id = dbInfo.id;
|
||||||
|
redisInst.value.db = Number.parseInt(dbInfo.db);
|
||||||
|
|
||||||
|
scan();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onDbClick,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.key-deatil {
|
||||||
|
.el-tabs__header {
|
||||||
|
background-color: var(--el-color-white);
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-tabs__item) {
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 29px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-tabs__nav-next) {
|
||||||
|
line-height: 29px;
|
||||||
|
}
|
||||||
|
::v-deep(.el-tabs__nav-prev) {
|
||||||
|
line-height: 29px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.redis-data-op {
|
||||||
|
.key-list-vtree .folder-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-list-vtree .key-label {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-list-vtree .key-list-custom-node {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
/*note the following 2 items should be same value, may not consist with itemSize*/
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
frontend/src/views/ops/redis/resource/index.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '../../component/tag';
|
||||||
|
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { redisApi } from '../api';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
|
||||||
|
const RedisIcon = {
|
||||||
|
name: ResourceTypeEnum.Redis.extra.icon,
|
||||||
|
color: ResourceTypeEnum.Redis.extra.iconColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const RedisList = defineAsyncComponent(() => import('../RedisList.vue'));
|
||||||
|
const RedisDataOp = defineAsyncComponent(() => import('./RedisDataOp.vue'));
|
||||||
|
|
||||||
|
const NodeRedis = defineAsyncComponent(() => import('./NodeRedis.vue'));
|
||||||
|
const NodeRedisDb = defineAsyncComponent(() => import('./NodeRedisDb.vue'));
|
||||||
|
|
||||||
|
export const RedisOpComp: ResourceComponentConfig = {
|
||||||
|
name: 'tag.redisDataOp',
|
||||||
|
component: RedisDataOp,
|
||||||
|
icon: RedisIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
// tagpath 节点类型
|
||||||
|
const NodeTypeRedisTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
parentNode.ctx?.addResourceComponent(RedisOpComp);
|
||||||
|
|
||||||
|
const res = await redisApi.redisList.request({ tagPath: parentNode.params.tagPath });
|
||||||
|
if (!res.total) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const redisInfos = res.list;
|
||||||
|
await sleep(100);
|
||||||
|
return redisInfos.map((x: any) => {
|
||||||
|
x.tagPath = parentNode.key;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeRedis).withParams(x).withNodeComponent(NodeRedis);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// redis实例节点类型
|
||||||
|
const NodeTypeRedis = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const redisInfo = parentNode.params;
|
||||||
|
|
||||||
|
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||||
|
return TagTreeNode.new(parentNode, x, `db${x}`, NodeTypeDb)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({
|
||||||
|
id: redisInfo.id,
|
||||||
|
db: x,
|
||||||
|
name: `db${x}`,
|
||||||
|
keys: 0,
|
||||||
|
})
|
||||||
|
.withNodeComponent(NodeRedisDb);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (redisInfo.mode == 'cluster') {
|
||||||
|
return dbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await redisApi.redisInfo.request({ id: redisInfo.id, host: redisInfo.host, section: 'Keyspace' });
|
||||||
|
for (let db in res.Keyspace) {
|
||||||
|
for (let d of dbs) {
|
||||||
|
if (db == d.params.name) {
|
||||||
|
d.params.keys = res.Keyspace[db]?.split(',')[0]?.split('=')[1] || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 替换label
|
||||||
|
dbs.forEach((e: any) => {
|
||||||
|
e.label = `${e.params.name}`;
|
||||||
|
});
|
||||||
|
return dbs;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 库节点类型
|
||||||
|
const NodeTypeDb = new NodeType(21).withNodeClickFunc(async (node: TagTreeNode) => {
|
||||||
|
(await node.ctx?.addResourceComponent(RedisOpComp)).onDbClick(node.params);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: 3,
|
||||||
|
resourceType: TagResourceTypeEnum.Redis.value,
|
||||||
|
rootNodeType: NodeTypeRedisTag,
|
||||||
|
manager: {
|
||||||
|
componentConf: {
|
||||||
|
component: RedisList,
|
||||||
|
icon: RedisIcon,
|
||||||
|
name: 'redis',
|
||||||
|
},
|
||||||
|
countKey: 'redis',
|
||||||
|
permCode: 'redis:manage',
|
||||||
|
},
|
||||||
|
} as ResourceConfig;
|
||||||
@@ -1,4 +1 @@
|
|||||||
export default {
|
export default {};
|
||||||
RedisList: () => import('@/views/ops/redis/RedisList.vue'),
|
|
||||||
DataOperation: () => import('@/views/ops/redis/DataOperation.vue'),
|
|
||||||
};
|
|
||||||
|
|||||||
93
frontend/src/views/ops/resource/BaseTreeNode.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:id="props.node.key"
|
||||||
|
class="w-full node-container flex items-center cursor-pointer select-none"
|
||||||
|
:class="props.data.type.nodeDblclickFunc ? 'select-none' : ''"
|
||||||
|
@mouseenter="showActions = true"
|
||||||
|
@mouseleave="showActions = false"
|
||||||
|
>
|
||||||
|
<!-- prefix -->
|
||||||
|
<SvgIcon :size="13" v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
||||||
|
<slot :node="node" :data="data" name="prefix"></slot>
|
||||||
|
|
||||||
|
<!-- label -->
|
||||||
|
<span class="ml-0.5" :title="data.labelRemark">
|
||||||
|
<slot name="label" :data="data" v-if="!data.disabled"> {{ $t(data.label) }}</slot>
|
||||||
|
|
||||||
|
<!-- 禁用状态 -->
|
||||||
|
<slot name="disabledLabel" :data="data" v-else>
|
||||||
|
<el-link type="danger" disabled underline="never">
|
||||||
|
{{ `${$t(data.label)}` }}
|
||||||
|
</el-link>
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- 操作按钮 or suffix 区域 -->
|
||||||
|
<span v-if="(showActions || dropdownVisible) && !data.disabled && contextMenuItems.length > 0" class="ml-auto pr-2.5 flex items-center">
|
||||||
|
<el-dropdown size="small" trigger="click" @command="handleCommand" @visibleChange="(visible: boolean) => (dropdownVisible = visible)">
|
||||||
|
<el-button text bg size="small" circle @click.stop type="primary">
|
||||||
|
<SvgIcon name="MoreFilled" />
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<template v-for="item in contextMenuItems" :key="item.clickId">
|
||||||
|
<el-dropdown-item v-if="!item.isHide(props.data)" :command="item">
|
||||||
|
<SvgIcon v-if="item.icon" :name="item.icon" class="mr-1" />{{ $t(item.txt) }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else class="ml-auto pr-1.5 text-[10px] text-gray-400">
|
||||||
|
<slot :node="node" :data="data" name="suffix"></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, inject } from 'vue';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
|
||||||
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
|
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||||
|
|
||||||
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
node: {
|
||||||
|
type: [Object],
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: [TagTreeNode],
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['contextmenu']);
|
||||||
|
|
||||||
|
const showActions = ref(false);
|
||||||
|
const dropdownVisible = ref(false);
|
||||||
|
|
||||||
|
// 获取上下文菜单项
|
||||||
|
const contextMenuItems = computed(() => {
|
||||||
|
let items = props.data.type.contextMenuItems;
|
||||||
|
if (!items || items.length == 0) {
|
||||||
|
// 如果 BaseTreeNode 组件无法直接访问父组件的 loadContextmenuItems 方法
|
||||||
|
// 可以通过事件通知父组件处理
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理命令点击
|
||||||
|
const handleCommand = (contextMenuItem: ContextmenuItem) => {
|
||||||
|
contextMenuItem.onClickFunc({ ...props.data, ctx: resourceOpCtx });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
410
frontend/src/views/ops/resource/ResourceOp.vue
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<ResourceOpPanel @resize="onResizeOpPanel">
|
||||||
|
<template #left>
|
||||||
|
<el-card class="h-full flex tag-tree-card" body-class="!p-0 flex flex-col w-full">
|
||||||
|
<div class="tag-tree-header flex flex-row justify-between items-center">
|
||||||
|
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="tag-tree-search w-full">
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon class="tag-tree-search-icon" name="search" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<div class="ml-1" v-if="Object.keys(resourceComponents).length > 1">
|
||||||
|
<el-dropdown placement="bottom-start" @command="changeResourceOp">
|
||||||
|
<el-button type="primary" link plain><SvgIcon name="Switch" /> </el-button>
|
||||||
|
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item
|
||||||
|
:command="{ name }"
|
||||||
|
v-for="(compConf, name) in resourceComponents"
|
||||||
|
:disabled="name == activeResourceComp"
|
||||||
|
>
|
||||||
|
<SvgIcon v-if="compConf.icon" :name="compConf.icon.name" :color="compConf.icon.color" />
|
||||||
|
<div class="ml-1">{{ $t(name) }}</div>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-scrollbar>
|
||||||
|
<el-tree
|
||||||
|
class="min-w-full inline-block"
|
||||||
|
ref="treeRef"
|
||||||
|
:highlight-current="true"
|
||||||
|
:indent="10"
|
||||||
|
:load="loadNode"
|
||||||
|
:props="treeProps"
|
||||||
|
lazy
|
||||||
|
node-key="key"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
@node-click="treeNodeClick"
|
||||||
|
@node-expand="treeNodeClick"
|
||||||
|
:default-expanded-keys="state.defaultExpandedKeys"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<component v-if="data.nodeComponent" :is="data.nodeComponent" :node="node" :data="data" />
|
||||||
|
<BaseTreeNode v-else :node="node" :data="data" />
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<el-card class="h-full" body-class=" h-full !p-1 flex flex-col flex-1">
|
||||||
|
<transition name="slide-x" mode="out-in">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="resourceComponents[activeResourceComp]?.component" :key="activeResourceComp" @init="initResourceComp" />
|
||||||
|
</keep-alive>
|
||||||
|
</transition>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
</ResourceOpPanel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { markRaw, nextTick, provide, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { isPrefixSubsequence } from '@/common/utils/string';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import EnumValue from '@/common/Enum';
|
||||||
|
import { getResourceNodeType, getResourceTypes, ResourceOpCtxKey } from './resource';
|
||||||
|
import BaseTreeNode from './BaseTreeNode.vue';
|
||||||
|
import { tagApi } from '@/views/ops/tag/api';
|
||||||
|
import ResourceOpPanel from '@/views/ops/component/ResourceOpPanel.vue';
|
||||||
|
import { TagTreeNode, ResourceComponentConfig, ResourceOpCtx } from '@/views/ops/component/tag';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useAutoOpenResource } from '@/store/autoOpenResource';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
load: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
loadContextmenuItems: {
|
||||||
|
type: Function,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const treeProps = {
|
||||||
|
label: 'name',
|
||||||
|
children: 'zones',
|
||||||
|
isLeaf: 'isLeaf',
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoOpenResourceStore = useAutoOpenResource();
|
||||||
|
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
||||||
|
|
||||||
|
const treeRef: any = useTemplateRef('treeRef');
|
||||||
|
|
||||||
|
// 存储所有注册的资源组件引用
|
||||||
|
const resourceComponents = ref<Record<string, ResourceComponentConfig>>({});
|
||||||
|
// 当前激活的资源组件
|
||||||
|
const activeResourceComp = ref<string>('');
|
||||||
|
|
||||||
|
const resourceComponentRefs = ref<Record<string, any>>({});
|
||||||
|
|
||||||
|
// :ref="(el: any) => setResourceComponentRefs(activeResourceComp, el)"
|
||||||
|
const setResourceComponentRefs = async (name: string, ref: any) => {
|
||||||
|
if (!name || !ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resourceComponentRefs.value[name]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resourceComponentRefs.value[name] = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
defaultExpandedKeys: [] as string[],
|
||||||
|
filterText: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { filterText } = toRefs(state);
|
||||||
|
|
||||||
|
watch(filterText, (val) => {
|
||||||
|
treeRef.value?.filter(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => autoOpenResource.value.codePath,
|
||||||
|
(autoOpenCodePath: any) => {
|
||||||
|
if (!autoOpenCodePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandedKeys: string[] = [];
|
||||||
|
let currentTagPath = '';
|
||||||
|
const parts = autoOpenCodePath.split('/'); // 切分字符串并保留数字和对应的值部分
|
||||||
|
let addResouceType = false;
|
||||||
|
for (let part of parts) {
|
||||||
|
if (!part) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let [key, value] = part.split('|'); // 分割数字和值部分
|
||||||
|
// 如果不存在第二个参数,则说明为标签类型
|
||||||
|
if (!value) {
|
||||||
|
const tagPath = key + '/';
|
||||||
|
currentTagPath = currentTagPath + tagPath;
|
||||||
|
expandedKeys.push(currentTagPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!addResouceType) {
|
||||||
|
expandedKeys.push(currentTagPath + '-' + key);
|
||||||
|
expandedKeys.push(value);
|
||||||
|
addResouceType = true;
|
||||||
|
} else {
|
||||||
|
expandedKeys.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.defaultExpandedKeys = expandedKeys;
|
||||||
|
autoOpenResourceStore.setCodePath('');
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentKey(expandedKeys[expandedKeys.length - 1]);
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterNode = (value: string, data: any) => {
|
||||||
|
return !value || isPrefixSubsequence(value, data.label);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载树节点
|
||||||
|
* @param { Object } node
|
||||||
|
* @param { Object } resolve
|
||||||
|
*/
|
||||||
|
const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
|
||||||
|
if (typeof resolve !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let nodes = [];
|
||||||
|
try {
|
||||||
|
if (node.level == 0) {
|
||||||
|
nodes = await loadTags();
|
||||||
|
} else if (props.load) {
|
||||||
|
nodes = await props.load(node);
|
||||||
|
} else {
|
||||||
|
nodes = await node.data.loadChildren();
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
// 调用 reject 以保持节点状态,并允许远程加载继续。
|
||||||
|
return reject();
|
||||||
|
}
|
||||||
|
return resolve(nodes);
|
||||||
|
};
|
||||||
|
|
||||||
|
let lastNodeClickTime = 0;
|
||||||
|
|
||||||
|
const treeNodeClick = async (data: any, node: any) => {
|
||||||
|
const currentClickNodeTime = Date.now();
|
||||||
|
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
||||||
|
treeNodeDblclick(data, node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastNodeClickTime = currentClickNodeTime;
|
||||||
|
|
||||||
|
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
||||||
|
emit('nodeClick', data);
|
||||||
|
await data.type.nodeClickFunc(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 树节点双击事件
|
||||||
|
const treeNodeDblclick = (data: any, node: any) => {
|
||||||
|
if (node.expanded) {
|
||||||
|
node.collapse();
|
||||||
|
} else {
|
||||||
|
node.expand();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.disabled && data.type.nodeDblclickFunc) {
|
||||||
|
data.type.nodeDblclickFunc(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化资源组件ref
|
||||||
|
const initResourceComp = (val: any) => {
|
||||||
|
if (!val.ref || resourceComponentRefs.value[val.name]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resourceComponentRefs.value[val.name] = val.ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addResourceComponent = async (componentConf: ResourceComponentConfig) => {
|
||||||
|
console.log(componentConf);
|
||||||
|
const compName = componentConf.name;
|
||||||
|
|
||||||
|
if (!resourceComponents.value[compName]) {
|
||||||
|
// 使用 markRaw 标记组件,防止其被变成响应式对象
|
||||||
|
resourceComponents.value[compName] = {
|
||||||
|
...componentConf,
|
||||||
|
component: markRaw(componentConf.component),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
activeResourceComp.value = compName;
|
||||||
|
|
||||||
|
// 使用一个 Promise 来确保组件引用已经被设置
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkRef = () => {
|
||||||
|
if (resourceComponentRefs.value[compName]) {
|
||||||
|
resolve(resourceComponentRefs.value[compName]);
|
||||||
|
} else {
|
||||||
|
// 如果引用还没有设置,稍后再检查
|
||||||
|
setTimeout(checkRef, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 先等待 nextTick 确保 DOM 更新
|
||||||
|
nextTick().then(() => {
|
||||||
|
checkRef();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeResourceOp = (data: any) => {
|
||||||
|
activeResourceComp.value = data.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadNode = (nodeKey: any) => {
|
||||||
|
let node = getNode(nodeKey);
|
||||||
|
node.loaded = false;
|
||||||
|
node.expand();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNode = (nodeKey: any) => {
|
||||||
|
let node = treeRef.value.getNode(nodeKey);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error('未找到节点: ' + nodeKey);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentKey = (nodeKey: any) => {
|
||||||
|
treeRef.value.setCurrentKey(nodeKey);
|
||||||
|
|
||||||
|
// 通过Id获取到对应的dom元素
|
||||||
|
const node = document.getElementById(nodeKey);
|
||||||
|
if (node) {
|
||||||
|
setTimeout(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
|
||||||
|
node.scrollIntoView({ block: 'center' });
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResizeOpPanel = () => {
|
||||||
|
for (let name in resourceComponentRefs.value) {
|
||||||
|
resourceComponentRefs.value[name]?.onResize?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载相关标签树节点
|
||||||
|
*/
|
||||||
|
const loadTags = async () => {
|
||||||
|
const tags = await tagApi.getTagTrees.request({
|
||||||
|
type: getResourceTypes().join(','),
|
||||||
|
});
|
||||||
|
const tagNodes = [];
|
||||||
|
for (let tag of tags) {
|
||||||
|
const tagNode = processTagNode(tag);
|
||||||
|
tagNodes.push(tagNode);
|
||||||
|
}
|
||||||
|
return tagNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processTagNode = (tag: any): TagTreeNode => {
|
||||||
|
const tagNode = new TagTreeNode(tag.codePath, tag.name, tag.type);
|
||||||
|
|
||||||
|
if (!tag.children || !Array.isArray(tag.children) || tag.children.length == 0) {
|
||||||
|
return tagNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子节点还是tag类型,则直接默认加载children即可
|
||||||
|
if (tag.children[0].type == TagResourceTypeEnum.Tag.value) {
|
||||||
|
tagNode.loadChildren = async () => {
|
||||||
|
const childNodes = [];
|
||||||
|
for (let child of tag.children) {
|
||||||
|
const childNode = processTagNode(child);
|
||||||
|
childNodes.push(childNode);
|
||||||
|
}
|
||||||
|
return childNodes;
|
||||||
|
};
|
||||||
|
return tagNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建中间节点, 按类型分组
|
||||||
|
const type2Tags = new Map<number, any>();
|
||||||
|
tag.children.forEach((child: any) => {
|
||||||
|
if (!type2Tags.has(child.type)) {
|
||||||
|
type2Tags.set(child.type, [child]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
type2Tags.get(child.type).push(child);
|
||||||
|
});
|
||||||
|
|
||||||
|
tagNode.loadChildren = async () => {
|
||||||
|
const childNodes = [];
|
||||||
|
|
||||||
|
for (let [type, children] of type2Tags) {
|
||||||
|
// 创建中间节点
|
||||||
|
const typeEnum = EnumValue.getEnumByValue(TagResourceTypeEnum, type);
|
||||||
|
const intermediateNode = new TagTreeNode(`${tag.codePath}-${type}`, t(typeEnum?.label || '未知'), getResourceNodeType(type))
|
||||||
|
.withIcon({
|
||||||
|
name: typeEnum?.extra.icon,
|
||||||
|
color: typeEnum?.extra.iconColor,
|
||||||
|
})
|
||||||
|
.withIsLeaf(false)
|
||||||
|
.withParams({ resourceCodes: children.map((c: any) => c.code), tagPath: tag.codePath })
|
||||||
|
.withContext(ctx);
|
||||||
|
|
||||||
|
childNodes.push(intermediateNode);
|
||||||
|
}
|
||||||
|
return childNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
return tagNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx: ResourceOpCtx = {
|
||||||
|
addResourceComponent,
|
||||||
|
setCurrentTreeKey: setCurrentKey,
|
||||||
|
getTreeNode: getNode,
|
||||||
|
reloadTreeNode: reloadNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
provide(ResourceOpCtxKey, ctx);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tag-tree-header {
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-tree-search {
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
border-radius: 14px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
57
frontend/src/views/ops/resource/resource.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { NodeType, ResourceConfig } from '@/views/ops/component/tag';
|
||||||
|
|
||||||
|
export const ResourceOpCtxKey = 'ResourceOpCtx';
|
||||||
|
|
||||||
|
// 加载目录下所有资源操作组件信息
|
||||||
|
const allResources: Record<string, any> = import.meta.glob('../**/resource/index.ts', { eager: true });
|
||||||
|
|
||||||
|
const resources = new Map<number, ResourceConfig>();
|
||||||
|
|
||||||
|
export function registerResource(type: number, rc: ResourceConfig) {
|
||||||
|
resources.set(type, rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResourceNodeType(type: number): NodeType | undefined {
|
||||||
|
init();
|
||||||
|
return resources.get(type)?.rootNodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResourceTypes() {
|
||||||
|
init();
|
||||||
|
return Array.from(resources.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResourceConfigs(): ResourceConfig[] {
|
||||||
|
init();
|
||||||
|
return sortByOrder(Array.from(resources.values()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResourceConfig(type: number): ResourceConfig | undefined {
|
||||||
|
init();
|
||||||
|
return resources.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (resources.size == 0) {
|
||||||
|
for (const path in allResources) {
|
||||||
|
// path => ../xxx/resource/index.ts
|
||||||
|
// 获取默认导出的资源组件配置信息
|
||||||
|
const resourceConf: ResourceConfig = allResources[path].default;
|
||||||
|
registerResource(resourceConf.resourceType, resourceConf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByOrder(items: any[]) {
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
if (a.order !== undefined && b.order !== undefined) {
|
||||||
|
return a.order - b.order; // 按order字段排序
|
||||||
|
} else if (a.order !== undefined) {
|
||||||
|
return -1; // a有order字段,排在前面
|
||||||
|
} else if (b.order !== undefined) {
|
||||||
|
return 1; // b有order字段,排在前面
|
||||||
|
} else {
|
||||||
|
return 0; // 两个都没有order字段,保持原顺序
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
3
frontend/src/views/ops/resource/route.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
ResourceOp: () => import('@/views/ops/resource/ResourceOp.vue'),
|
||||||
|
};
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tag-tree-list card !p-2 h-full flex">
|
<div class="tag-tree-list card !p-2 h-full flex">
|
||||||
<el-splitter>
|
<el-splitter>
|
||||||
<el-splitter-panel size="30%" min="25%" max="35%" class="flex flex-col flex-1">
|
<el-splitter-panel size="25%" max="35%" class="flex flex-col flex-1">
|
||||||
<div class="card !p-1 !mb-1 !mr-1 flex justify-between">
|
<div class="card !p-1 !mr-1 flex flex-row items-center justify-between overflow-hidden">
|
||||||
<div class="mb-1">
|
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2" />
|
||||||
<el-input v-model="filterTag" clearable :placeholder="$t('tag.nameFilterPlaceholder')" class="mr-2 !w-[200px]" />
|
<el-button
|
||||||
<el-button
|
v-if="useUserInfo().userInfo.username == 'admin'"
|
||||||
v-if="useUserInfo().userInfo.username == 'admin'"
|
v-auth="'tag:save'"
|
||||||
v-auth="'tag:save'"
|
type="primary"
|
||||||
type="primary"
|
icon="plus"
|
||||||
icon="plus"
|
@click="onShowSaveTagDialog(null)"
|
||||||
@click="onShowSaveTagDialog(null)"
|
></el-button>
|
||||||
></el-button>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<el-tooltip placement="top">
|
<el-tooltip placement="top">
|
||||||
<template #content>
|
<template #content>
|
||||||
@@ -21,13 +19,14 @@
|
|||||||
{{ $t('tag.tagTips2') }} <br />
|
{{ $t('tag.tagTips2') }} <br />
|
||||||
{{ $t('tag.tagTips3') }}
|
{{ $t('tag.tagTips3') }}
|
||||||
</template>
|
</template>
|
||||||
<SvgIcon name="question-filled" />
|
<SvgIcon class="ml-1" name="question-filled" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="tag-tree-data">
|
<el-scrollbar class="tag-tree-data">
|
||||||
<el-tree
|
<el-tree
|
||||||
class="min-w-full inline-block"
|
class="min-w-full inline-block"
|
||||||
|
:indent="10"
|
||||||
ref="tagTreeRef"
|
ref="tagTreeRef"
|
||||||
node-key="id"
|
node-key="id"
|
||||||
highlight-current
|
highlight-current
|
||||||
@@ -54,10 +53,12 @@
|
|||||||
|
|
||||||
<span class="ml-1">
|
<span class="ml-1">
|
||||||
{{ data.name }}
|
{{ data.name }}
|
||||||
<span style="color: #3c8dbc">【</span>
|
<template v-if="data.code">
|
||||||
{{ data.code }}
|
<span style="color: #3c8dbc">【</span>
|
||||||
<span style="color: #3c8dbc">】</span>
|
{{ data.code }}
|
||||||
<el-tag v-if="data.children !== null" size="small">{{ data.children.length }}</el-tag>
|
<span style="color: #3c8dbc">】</span>
|
||||||
|
</template>
|
||||||
|
<el-tag v-if="data.children !== null && data.id != allNode.id" size="small">{{ data.children.length }}</el-tag>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
<el-splitter-panel>
|
<el-splitter-panel>
|
||||||
<div class="ml-2 h-full">
|
<div class="ml-2 h-full">
|
||||||
<el-tabs class="h-full" @tab-change="onTabChange" v-model="state.activeTabName" v-if="currentTag">
|
<el-tabs class="h-full" @tab-change="onTabChange" v-model="state.activeTabName" v-if="currentTag">
|
||||||
<el-tab-pane :label="$t('common.detail')" :name="TagDetail">
|
<el-tab-pane v-if="currentTag.id != allNode.id" :label="$t('common.detail')" :name="TagDetail">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item :label="$t('common.type')">
|
<el-descriptions-item :label="$t('common.type')">
|
||||||
<EnumTag :enums="TagResourceTypeEnum" :value="currentTag.type" />
|
<EnumTag :enums="TagResourceTypeEnum" :value="currentTag.type" />
|
||||||
@@ -92,46 +93,20 @@
|
|||||||
<el-tab-pane
|
<el-tab-pane
|
||||||
class="h-full"
|
class="h-full"
|
||||||
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
||||||
:label="`${$t('tag.machine')} (${resourceCount.machine || 0})`"
|
:label="`${$t(resource?.componentConf.name || '')} (${resourceCount[resource?.countKey || ''] || 0})`"
|
||||||
:name="MachineTag"
|
:name="index"
|
||||||
|
v-for="(resource, index) in resources"
|
||||||
>
|
>
|
||||||
<MachineList lazy ref="machineListRef" />
|
<template #label>
|
||||||
</el-tab-pane>
|
<SvgIcon :name="resource?.componentConf.icon?.name" :color="resource?.componentConf.icon?.color" />
|
||||||
|
<span class="ml-1">
|
||||||
|
{{ `${$t(resource?.componentConf.name || '')} (${resourceCount[resource?.countKey || ''] || 0})` }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-tab-pane
|
<div class="h-full" v-if="Number.isInteger(state.activeTabName) && Number.parseInt(state.activeTabName) === index">
|
||||||
class="h-full"
|
<component lazy :ref="(el: any) => setComponentRef(el, index)" :is="resource?.componentConf.component"></component>
|
||||||
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
</div>
|
||||||
:label="`${$t('tag.db')} (${resourceCount.db || 0})`"
|
|
||||||
:name="DbTag"
|
|
||||||
>
|
|
||||||
<InstanceList lazy ref="dbInstanceListRef" />
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane
|
|
||||||
class="h-full"
|
|
||||||
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
|
||||||
:label="`${$t('tag.es')} (${resourceCount.es || 0})`"
|
|
||||||
:name="EsTag"
|
|
||||||
>
|
|
||||||
<EsInstanceList lazy ref="esInstanceListRef" />
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane
|
|
||||||
class="h-full"
|
|
||||||
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
|
||||||
:label="`Redis (${resourceCount.redis || 0})`"
|
|
||||||
:name="RedisTag"
|
|
||||||
>
|
|
||||||
<RedisList lazy ref="redisListRef" />
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane
|
|
||||||
class="h-full"
|
|
||||||
:disabled="currentTag.type != TagResourceTypeEnum.Tag.value"
|
|
||||||
:label="`Mongo (${resourceCount.mongo || 0})`"
|
|
||||||
:name="MongoTag"
|
|
||||||
>
|
|
||||||
<MongoList lazy ref="mongoListRef" />
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,10 +126,8 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<el-button @click="onCancelSaveTag()">{{ $t('common.cancel') }}</el-button>
|
||||||
<el-button @click="onCancelSaveTag()">{{ $t('common.cancel') }}</el-button>
|
<el-button @click="onSaveTag" type="primary">{{ $t('common.confirm') }}</el-button>
|
||||||
<el-button @click="onSaveTag" type="primary">{{ $t('common.confirm') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -163,7 +136,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, ref, watch, reactive, onMounted, Ref, defineAsyncComponent } from 'vue';
|
import { toRefs, ref, watch, reactive, onMounted, computed, nextTick, useTemplateRef } from 'vue';
|
||||||
import { tagApi } from './api';
|
import { tagApi } from './api';
|
||||||
import { formatDate } from '@/common/utils/format';
|
import { formatDate } from '@/common/utils/format';
|
||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu/index';
|
||||||
@@ -183,12 +156,13 @@ import {
|
|||||||
useI18nSaveSuccessMsg,
|
useI18nSaveSuccessMsg,
|
||||||
} from '@/hooks/useI18n';
|
} from '@/hooks/useI18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { getResourceConfigs } from '@/views/ops/resource/resource';
|
||||||
|
import { hasPerm } from '@/components/auth/auth';
|
||||||
|
|
||||||
const MachineList = defineAsyncComponent(() => import('../machine/MachineList.vue'));
|
const compRefs = ref<Array<any>>([]);
|
||||||
const InstanceList = defineAsyncComponent(() => import('../db/InstanceList.vue'));
|
const setComponentRef = (el: any, index: number) => {
|
||||||
const EsInstanceList = defineAsyncComponent(() => import('../es/EsInstanceList.vue'));
|
compRefs.value[index] = el;
|
||||||
const RedisList = defineAsyncComponent(() => import('../redis/RedisList.vue'));
|
};
|
||||||
const MongoList = defineAsyncComponent(() => import('../mongo/MongoList.vue'));
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -200,21 +174,32 @@ interface Tree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tagForm: any = ref(null);
|
const tagForm: any = ref(null);
|
||||||
const tagTreeRef: any = ref(null);
|
const tagTreeRef: any = useTemplateRef('tagTreeRef');
|
||||||
const filterTag = ref('');
|
const filterTag = ref('');
|
||||||
const contextmenuRef = ref();
|
const contextmenuRef = ref();
|
||||||
const machineListRef: Ref<any> = ref(null);
|
|
||||||
const dbInstanceListRef: Ref<any> = ref(null);
|
|
||||||
const esInstanceListRef: Ref<any> = ref(null);
|
|
||||||
const redisListRef: Ref<any> = ref(null);
|
|
||||||
const mongoListRef: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const TagDetail = 'tagDetail';
|
const TagDetail = 'tagDetail';
|
||||||
const MachineTag = 'machineTag';
|
|
||||||
const DbTag = 'dbTag';
|
const allNode = {
|
||||||
const EsTag = 'EsTag';
|
id: -1,
|
||||||
const RedisTag = 'redisTag';
|
name: t('tag.allResource'),
|
||||||
const MongoTag = 'mongoTag';
|
type: TagResourceTypeEnum.Tag.value,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const resources = computed(() => {
|
||||||
|
return getResourceConfigs()
|
||||||
|
.filter((x) => {
|
||||||
|
if (!x.manager?.componentConf) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!x.manager.permCode) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasPerm(x.manager.permCode);
|
||||||
|
})
|
||||||
|
.map((x) => x.manager);
|
||||||
|
});
|
||||||
|
|
||||||
const contextmenuAdd = new ContextmenuItem('addTag', 'tag.createSubTag')
|
const contextmenuAdd = new ContextmenuItem('addTag', 'tag.createSubTag')
|
||||||
.withIcon('circle-plus')
|
.withIcon('circle-plus')
|
||||||
@@ -283,6 +268,8 @@ const rules = {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
search();
|
search();
|
||||||
|
tagTreeRef.value.setCurrentKey(allNode.id);
|
||||||
|
onTreeNodeClick(allNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(filterTag, (val) => {
|
watch(filterTag, (val) => {
|
||||||
@@ -382,35 +369,37 @@ const onTabChange = () => {
|
|||||||
setNowTabData();
|
setNowTabData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNowTabData = () => {
|
const setNowTabData = async () => {
|
||||||
const tagPath = state.currentTag.codePath;
|
if (Number.isInteger(state.activeTabName)) {
|
||||||
switch (state.activeTabName) {
|
(await getResouceCompRef(Number.parseInt(state.activeTabName))).search(state.currentTag.codePath);
|
||||||
case MachineTag:
|
|
||||||
machineListRef.value.search(tagPath);
|
|
||||||
break;
|
|
||||||
case DbTag:
|
|
||||||
dbInstanceListRef.value.search(tagPath);
|
|
||||||
break;
|
|
||||||
case EsTag:
|
|
||||||
esInstanceListRef.value.search(tagPath);
|
|
||||||
break;
|
|
||||||
case RedisTag:
|
|
||||||
redisListRef.value.search(tagPath);
|
|
||||||
break;
|
|
||||||
case MongoTag:
|
|
||||||
mongoListRef.value.search(tagPath);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getResouceCompRef = (index: number): Promise<any> => {
|
||||||
|
// 使用一个 Promise 来确保组件引用已经被设置
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkRef = () => {
|
||||||
|
if (compRefs.value[index]) {
|
||||||
|
resolve(compRefs.value[index]);
|
||||||
|
} else {
|
||||||
|
// 如果引用还没有设置,稍后再检查
|
||||||
|
setTimeout(checkRef, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 先等待 nextTick 确保 DOM 更新
|
||||||
|
nextTick().then(() => {
|
||||||
|
checkRef();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const filterNode = (value: string, data: Tree) => {
|
const filterNode = (value: string, data: Tree) => {
|
||||||
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
|
return !value || isPrefixSubsequence(value, data.codePath) || isPrefixSubsequence(value, data.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
let res = await tagApi.getTagTrees.request(null);
|
let res = await tagApi.getTagTrees.request(null);
|
||||||
|
res.unshift(allNode);
|
||||||
state.data = res;
|
state.data = res;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -428,10 +417,18 @@ const onNodeContextmenu = (event: any, data: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onTreeNodeClick = async (data: any) => {
|
const onTreeNodeClick = async (data: any) => {
|
||||||
state.currentTag = await getDetail(data.id);
|
|
||||||
state.activeTabName = TagDetail;
|
|
||||||
// 关闭可能存在的右击菜单
|
// 关闭可能存在的右击菜单
|
||||||
contextmenuRef.value.closeContextmenu();
|
contextmenuRef.value.closeContextmenu();
|
||||||
|
|
||||||
|
if (data.id == allNode.id) {
|
||||||
|
state.currentTag = data;
|
||||||
|
state.activeTabName = 0 as any;
|
||||||
|
onTabChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.currentTag = await getDetail(data.id);
|
||||||
|
state.activeTabName = TagDetail;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onShowSaveTagDialog = (data: any) => {
|
const onShowSaveTagDialog = (data: any) => {
|
||||||
@@ -508,10 +505,10 @@ const removeDeafultExpandId = (id: any) => {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.tag-tree-list {
|
.tag-tree-list {
|
||||||
.tag-tree-data {
|
.tag-tree-data {
|
||||||
.el-tree-node__content {
|
// .el-tree-node__content {
|
||||||
height: 40px;
|
// height: 40px;
|
||||||
line-height: 40px;
|
// line-height: 40px;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
<div class="card !p-2 system-resource-list h-full flex">
|
<div class="card !p-2 system-resource-list h-full flex">
|
||||||
<el-splitter>
|
<el-splitter>
|
||||||
<el-splitter-panel size="30%" max="35%" min="25%" class="flex flex-col flex-1">
|
<el-splitter-panel size="30%" max="35%" min="25%" class="flex flex-col flex-1">
|
||||||
<div class="card !p-1 mr-1 flex justify-between">
|
<div class="card !p-1 mr-1 flex flex-row items-center justify-between overflow-hidden">
|
||||||
<div class="mb-1">
|
<el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2" />
|
||||||
<el-input v-model="filterResource" clearable :placeholder="$t('system.menu.filterPlaceholder')" class="mr-2 !w-[200px]" />
|
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="onAddResource(false)"></el-button>
|
||||||
<el-button v-auth="perms.addResource" type="primary" icon="plus" @click="onAddResource(false)"></el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="ml-1">
|
||||||
<el-tooltip placement="top">
|
<el-tooltip placement="top">
|
||||||
<template #content> {{ $t('system.menu.opTips') }} </template>
|
<template #content> {{ $t('system.menu.opTips') }} </template>
|
||||||
<SvgIcon name="question-filled" />
|
<SvgIcon name="question-filled" />
|
||||||
@@ -436,8 +434,8 @@ const removeDeafultExpandId = (id: any) => {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.system-resource-list {
|
.system-resource-list {
|
||||||
.el-tree-node__content {
|
.el-tree-node__content {
|
||||||
height: 40px;
|
height: 35px;
|
||||||
line-height: 40px;
|
line-height: 35px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ require (
|
|||||||
gitee.com/chunanyong/dm v1.8.20
|
gitee.com/chunanyong/dm v1.8.20
|
||||||
gitee.com/liuzongyang/libpq v1.10.11
|
gitee.com/liuzongyang/libpq v1.10.11
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
|
github.com/docker/docker v28.3.3+incompatible
|
||||||
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-gormigrate/gormigrate/v2 v2.1.4
|
github.com/go-gormigrate/gormigrate/v2 v2.1.4
|
||||||
@@ -20,6 +22,7 @@ require (
|
|||||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250630080345-f9402614f6ba
|
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250630080345-f9402614f6ba
|
||||||
github.com/microsoft/go-mssqldb v1.9.2
|
github.com/microsoft/go-mssqldb v1.9.2
|
||||||
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
||||||
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/sftp v1.13.9
|
github.com/pkg/sftp v1.13.9
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
@@ -38,25 +41,35 @@ require (
|
|||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
// gorm
|
// gorm
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/gorm v1.30.1
|
gorm.io/gorm v1.30.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
@@ -70,14 +83,18 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
@@ -87,6 +104,13 @@ require (
|
|||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||||
golang.org/x/arch v0.19.0 // indirect
|
golang.org/x/arch v0.19.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
||||||
golang.org/x/image v0.29.0 // indirect
|
golang.org/x/image v0.29.0 // indirect
|
||||||
@@ -99,3 +123,5 @@ require (
|
|||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.38.1 // indirect
|
modernc.org/sqlite v1.38.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250603155806-513f23925822
|
||||||
|
|||||||
9
server/internal/docker/api/api.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "mayfly-go/pkg/ioc"
|
||||||
|
|
||||||
|
func InitIoc() {
|
||||||
|
ioc.Register(new(Docker))
|
||||||
|
ioc.Register(new(Container))
|
||||||
|
ioc.Register(new(Image))
|
||||||
|
}
|
||||||
678
server/internal/docker/api/container.go
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mayfly-go/internal/docker/api/form"
|
||||||
|
"mayfly-go/internal/docker/api/vo"
|
||||||
|
"mayfly-go/internal/docker/dkm"
|
||||||
|
"mayfly-go/internal/docker/imsg"
|
||||||
|
"mayfly-go/pkg/biz"
|
||||||
|
"mayfly-go/pkg/errorx"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
"mayfly-go/pkg/req"
|
||||||
|
"mayfly-go/pkg/utils/anyx"
|
||||||
|
"mayfly-go/pkg/utils/collx"
|
||||||
|
"mayfly-go/pkg/ws"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ReqConfs() *req.Confs {
|
||||||
|
reqs := [...]*req.Conf{
|
||||||
|
req.NewGet("", d.GetContainers),
|
||||||
|
req.NewGet("/stats", d.GetContainersStats),
|
||||||
|
req.NewPost("/stop", d.ContainerStop).Log(req.NewLogSaveI(imsg.LogDockerContainerStop)),
|
||||||
|
req.NewPost("/remove", d.ContainerRemove).Log(req.NewLogSaveI(imsg.LogDockerContainerRemove)),
|
||||||
|
req.NewPost("/restart", d.ContainerRestart).Log(req.NewLogSaveI(imsg.LogDockerContainerStop)),
|
||||||
|
req.NewPost("/create", d.ContainerCreate).Log(req.NewLogSaveI(imsg.LogDockerContainerCreate)),
|
||||||
|
|
||||||
|
req.NewGet("/exec", d.ContainerExecAttach).NoRes(),
|
||||||
|
req.NewGet("/logs", d.ContainerLogs).NoRes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.NewConfs("docker/containers", reqs[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) GetContainers(rc *req.Ctx) {
|
||||||
|
cli, err := dkm.GetCli(rc.Query("host"))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
cs, err := cli.ContainerList()
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
rc.ResData = collx.ArrayMap(cs, func(val container.Summary) vo.Container {
|
||||||
|
c := vo.Container{
|
||||||
|
ContainerId: val.ID,
|
||||||
|
Name: val.Names[0][1:],
|
||||||
|
ImageId: strings.Split(val.ImageID, ":")[1],
|
||||||
|
ImageName: val.Image,
|
||||||
|
State: val.State,
|
||||||
|
Status: val.Status,
|
||||||
|
CreateTime: time.Unix(val.Created, 0),
|
||||||
|
Ports: transPortToStr(val.Ports),
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.NetworkSettings != nil && len(val.NetworkSettings.Networks) > 0 {
|
||||||
|
if ns := val.NetworkSettings.Networks; len(ns) > 0 {
|
||||||
|
networks := make([]string, 0, len(ns))
|
||||||
|
for key := range ns {
|
||||||
|
networks = append(networks, ns[key].IPAddress)
|
||||||
|
}
|
||||||
|
sort.Strings(networks)
|
||||||
|
c.Networks = networks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) GetContainersStats(rc *req.Ctx) {
|
||||||
|
cli, err := dkm.GetCli(rc.Query("host"))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
cs, err := cli.ContainerList()
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(cs))
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
allStats := make([]vo.ContainerStats, 0)
|
||||||
|
for _, c := range cs {
|
||||||
|
go func(item container.Summary) {
|
||||||
|
defer wg.Done()
|
||||||
|
if item.State != "running" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := cli.ContainerStats(c.ID)
|
||||||
|
if err != nil {
|
||||||
|
logx.Error("get docker container stats err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var cs vo.ContainerStats
|
||||||
|
cs.ContainerId = c.ID
|
||||||
|
|
||||||
|
cs.CPUTotalUsage = stats.CPUStats.CPUUsage.TotalUsage - stats.PreCPUStats.CPUUsage.TotalUsage
|
||||||
|
cs.SystemUsage = stats.CPUStats.SystemUsage - stats.PreCPUStats.SystemUsage
|
||||||
|
cs.CPUPercent = calculateCPUPercentUnix(stats)
|
||||||
|
cs.PercpuUsage = len(stats.CPUStats.CPUUsage.PercpuUsage)
|
||||||
|
|
||||||
|
cs.MemoryCache = stats.MemoryStats.Stats["cache"]
|
||||||
|
cs.MemoryUsage = stats.MemoryStats.Usage
|
||||||
|
cs.MemoryLimit = stats.MemoryStats.Limit
|
||||||
|
|
||||||
|
cs.MemoryPercent = calculateMemPercentUnix(stats.MemoryStats)
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
allStats = append(allStats, cs)
|
||||||
|
mu.Unlock()
|
||||||
|
}(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
rc.ResData = allStats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ContainerCreate(rc *req.Ctx) {
|
||||||
|
containerCreate := &form.ContainerCreate{}
|
||||||
|
biz.ErrIsNil(rc.BindJSON(containerCreate))
|
||||||
|
|
||||||
|
rc.ReqParam = containerCreate
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(containerCreate.Host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
config, hostConfig, networkConfig, err := loadConfigInfo(true, containerCreate, nil)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
ctx := rc.MetaCtx
|
||||||
|
con, err := cli.DockerClient.ContainerCreate(ctx, config, hostConfig, networkConfig, &v1.Platform{}, containerCreate.Name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
|
panic(errorx.NewBiz("create container failed, err: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
logx.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", containerCreate.Name)
|
||||||
|
|
||||||
|
if err := cli.DockerClient.ContainerStart(ctx, con.ID, container.StartOptions{}); err != nil {
|
||||||
|
_ = cli.DockerClient.ContainerRemove(ctx, containerCreate.Name, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||||
|
panic(errorx.NewBiz("create successful but start failed, err: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ContainerStop(rc *req.Ctx) {
|
||||||
|
containerOp := &form.ContainerOp{}
|
||||||
|
biz.ErrIsNil(rc.BindJSON(containerOp))
|
||||||
|
|
||||||
|
rc.ReqParam = collx.Kvs("host", containerOp.Host, "containerId", containerOp.ContainerId)
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(containerOp.Host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
err = cli.ContainerStop(containerOp.ContainerId)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ContainerRemove(rc *req.Ctx) {
|
||||||
|
containerOp := &form.ContainerOp{}
|
||||||
|
biz.ErrIsNil(rc.BindJSON(containerOp))
|
||||||
|
|
||||||
|
rc.ReqParam = collx.Kvs("host", containerOp.Host, "containerId", containerOp.ContainerId)
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(containerOp.Host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
err = cli.ContainerRemove(containerOp.ContainerId)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ContainerRestart(rc *req.Ctx) {
|
||||||
|
containerOp := &form.ContainerOp{}
|
||||||
|
biz.ErrIsNil(rc.BindJSON(containerOp))
|
||||||
|
|
||||||
|
rc.ReqParam = collx.Kvs("host", containerOp.Host, "containerId", containerOp.ContainerId)
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(containerOp.Host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
err = cli.ContainerRestart(containerOp.ContainerId)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ContainerLogs(rc *req.Ctx) {
|
||||||
|
wsConn, err := ws.Upgrader.Upgrade(rc.GetWriter(), rc.GetRequest(), nil)
|
||||||
|
defer func() {
|
||||||
|
if wsConn != nil {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte(anyx.ToString(err)))
|
||||||
|
}
|
||||||
|
wsConn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
biz.ErrIsNilAppendErr(err, "Upgrade websocket fail: %s")
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(rc.Query("host"))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(rc.MetaCtx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 设置日志选项
|
||||||
|
logOptions := container.LogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
ShowStderr: true,
|
||||||
|
Follow: rc.Query("follow") == "1",
|
||||||
|
Timestamps: false,
|
||||||
|
Since: rc.Query("since"),
|
||||||
|
}
|
||||||
|
tail := rc.QueryInt("tail")
|
||||||
|
if tail > 0 {
|
||||||
|
logOptions.Tail = cast.ToString(tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := cli.DockerClient.ContainerLogs(ctx, rc.Query("containerId"), logOptions)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
defer logs.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, _, err := wsConn.ReadMessage()
|
||||||
|
// 读取ws关闭错误,取消日志输出
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
n, err := logs.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF && err != context.Canceled {
|
||||||
|
logx.ErrorTrace("Read container log error", err)
|
||||||
|
}
|
||||||
|
wsConn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !utf8.Valid(buf[:n]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := wsConn.WriteMessage(websocket.TextMessage, buf[:n]); err != nil {
|
||||||
|
logx.ErrorTrace("Write container log error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ContainerExecAttach(rc *req.Ctx) {
|
||||||
|
wsConn, err := ws.Upgrader.Upgrade(rc.GetWriter(), rc.GetRequest(), nil)
|
||||||
|
defer func() {
|
||||||
|
if wsConn != nil {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte(anyx.ToString(err)))
|
||||||
|
}
|
||||||
|
wsConn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
biz.ErrIsNilAppendErr(err, "Upgrade websocket fail: %s")
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte("Connecting to container..."))
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(rc.Query("host"))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
cols := rc.QueryIntDefault("cols", 80)
|
||||||
|
rows := rc.QueryIntDefault("rows", 32)
|
||||||
|
|
||||||
|
err = cli.ContainerAttach(rc.Query("containerId"), wsConn, rows, cols)
|
||||||
|
if err != nil {
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error attaching to container: %s", err.Error())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Container) ContainerProxy(rc *req.Ctx) {
|
||||||
|
// 获取 containerId 和剩余路径
|
||||||
|
pathParts := strings.Split(rc.GetRequest().URL.Path, "/")
|
||||||
|
if len(pathParts) < 4 {
|
||||||
|
http.Error(rc.GetWriter(), "Invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
containerID := pathParts[2]
|
||||||
|
remainingPath := strings.Join(pathParts[3:], "/")
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(rc.Query("host"))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
ctx := rc.MetaCtx
|
||||||
|
containerJSON, err := cli.DockerClient.ContainerInspect(ctx, containerID)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
// 获取容器的网络信息
|
||||||
|
networkSettings := containerJSON.NetworkSettings
|
||||||
|
if networkSettings == nil || len(networkSettings.Networks) == 0 {
|
||||||
|
panic(errorx.NewBiz("container network settings not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 假设我们使用第一个网络的IP地址
|
||||||
|
var containerIP string
|
||||||
|
for _, network := range networkSettings.Networks {
|
||||||
|
containerIP = network.IPAddress
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取容器的端口映射
|
||||||
|
var containerPort string
|
||||||
|
portBindings := containerJSON.HostConfig.PortBindings
|
||||||
|
if len(portBindings) > 0 {
|
||||||
|
for _, bindings := range portBindings {
|
||||||
|
if len(bindings) > 0 {
|
||||||
|
containerPort = bindings[0].HostPort
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerIP == "" || containerPort == "" {
|
||||||
|
panic(errorx.NewBiz("container IP or port not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建目标URL
|
||||||
|
targetURL, err := url.Parse(fmt.Sprintf("http://%s:%s", containerIP, containerPort))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
// 创建反向代理
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(targetURL)
|
||||||
|
|
||||||
|
// 修改请求头中的主机地址和路径
|
||||||
|
proxy.Director = func(req *http.Request) {
|
||||||
|
req.Header.Set("X-Real-IP", req.RemoteAddr)
|
||||||
|
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
req.Host = targetURL.Host
|
||||||
|
|
||||||
|
// 重写请求路径
|
||||||
|
req.URL.Path = "/" + remainingPath
|
||||||
|
req.URL.RawPath = "/" + remainingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理请求
|
||||||
|
proxy.ServeHTTP(rc.GetWriter(), rc.GetRequest())
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCPUPercentUnix(stats container.StatsResponse) float64 {
|
||||||
|
cpuPercent := 0.0
|
||||||
|
cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage)
|
||||||
|
systemDelta := float64(stats.CPUStats.SystemUsage) - float64(stats.PreCPUStats.SystemUsage)
|
||||||
|
|
||||||
|
if systemDelta > 0.0 && cpuDelta > 0.0 {
|
||||||
|
cpuPercent = (cpuDelta / systemDelta) * 100.0
|
||||||
|
if len(stats.CPUStats.CPUUsage.PercpuUsage) != 0 {
|
||||||
|
cpuPercent = cpuPercent * float64(len(stats.CPUStats.CPUUsage.PercpuUsage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cpuPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateMemPercentUnix(memStats container.MemoryStats) float64 {
|
||||||
|
memPercent := 0.0
|
||||||
|
memUsage := float64(memStats.Usage)
|
||||||
|
memLimit := float64(memStats.Limit)
|
||||||
|
if memUsage > 0.0 && memLimit > 0.0 {
|
||||||
|
memPercent = (memUsage / memLimit) * 100.0
|
||||||
|
}
|
||||||
|
return memPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateBlockIO(blkio container.BlkioStats) (blkRead float64, blkWrite float64) {
|
||||||
|
for _, bioEntry := range blkio.IoServiceBytesRecursive {
|
||||||
|
switch strings.ToLower(bioEntry.Op) {
|
||||||
|
case "read":
|
||||||
|
blkRead = (blkRead + float64(bioEntry.Value)) / 1024 / 1024
|
||||||
|
case "write":
|
||||||
|
blkWrite = (blkWrite + float64(bioEntry.Value)) / 1024 / 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateNetwork(network map[string]container.NetworkStats) (float64, float64) {
|
||||||
|
var rx, tx float64
|
||||||
|
|
||||||
|
for _, v := range network {
|
||||||
|
rx += float64(v.RxBytes) / 1024
|
||||||
|
tx += float64(v.TxBytes) / 1024
|
||||||
|
}
|
||||||
|
return rx, tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func transPortToStr(ports []container.Port) []string {
|
||||||
|
var (
|
||||||
|
ipv4Ports []container.Port
|
||||||
|
ipv6Ports []container.Port
|
||||||
|
)
|
||||||
|
for _, port := range ports {
|
||||||
|
if strings.Contains(port.IP, ":") {
|
||||||
|
ipv6Ports = append(ipv6Ports, port)
|
||||||
|
} else {
|
||||||
|
ipv4Ports = append(ipv4Ports, port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list1 := simplifyPort(ipv4Ports)
|
||||||
|
list2 := simplifyPort(ipv6Ports)
|
||||||
|
return append(list1, list2...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func simplifyPort(ports []container.Port) []string {
|
||||||
|
var datas []string
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return datas
|
||||||
|
}
|
||||||
|
if len(ports) == 1 {
|
||||||
|
ip := ""
|
||||||
|
if len(ports[0].IP) != 0 {
|
||||||
|
ip = ports[0].IP + ":"
|
||||||
|
}
|
||||||
|
itemPortStr := fmt.Sprintf("%s%v/%s", ip, ports[0].PrivatePort, ports[0].Type)
|
||||||
|
if ports[0].PublicPort != 0 {
|
||||||
|
itemPortStr = fmt.Sprintf("%s%v->%v/%s", ip, ports[0].PublicPort, ports[0].PrivatePort, ports[0].Type)
|
||||||
|
}
|
||||||
|
datas = append(datas, itemPortStr)
|
||||||
|
return datas
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(ports, func(i, j int) bool {
|
||||||
|
return ports[i].PrivatePort < ports[j].PrivatePort
|
||||||
|
})
|
||||||
|
start := ports[0]
|
||||||
|
|
||||||
|
for i := 1; i < len(ports); i++ {
|
||||||
|
if ports[i].PrivatePort != ports[i-1].PrivatePort+1 || ports[i].IP != ports[i-1].IP || ports[i].PublicPort != ports[i-1].PublicPort+1 || ports[i].Type != ports[i-1].Type {
|
||||||
|
if ports[i-1].PrivatePort == start.PrivatePort {
|
||||||
|
itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type)
|
||||||
|
if start.PublicPort != 0 {
|
||||||
|
itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type)
|
||||||
|
}
|
||||||
|
if len(start.IP) == 0 {
|
||||||
|
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||||
|
}
|
||||||
|
datas = append(datas, itemPortStr)
|
||||||
|
} else {
|
||||||
|
itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i-1].PrivatePort, start.Type)
|
||||||
|
if start.PublicPort != 0 {
|
||||||
|
itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i-1].PublicPort, start.PrivatePort, ports[i-1].PrivatePort, start.Type)
|
||||||
|
}
|
||||||
|
if len(start.IP) == 0 {
|
||||||
|
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||||
|
}
|
||||||
|
datas = append(datas, itemPortStr)
|
||||||
|
}
|
||||||
|
start = ports[i]
|
||||||
|
}
|
||||||
|
if i == len(ports)-1 {
|
||||||
|
if ports[i].PrivatePort == start.PrivatePort {
|
||||||
|
itemPortStr := fmt.Sprintf("%s:%v/%s", start.IP, start.PrivatePort, start.Type)
|
||||||
|
if start.PublicPort != 0 {
|
||||||
|
itemPortStr = fmt.Sprintf("%s:%v->%v/%s", start.IP, start.PublicPort, start.PrivatePort, start.Type)
|
||||||
|
}
|
||||||
|
if len(start.IP) == 0 {
|
||||||
|
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||||
|
}
|
||||||
|
datas = append(datas, itemPortStr)
|
||||||
|
} else {
|
||||||
|
itemPortStr := fmt.Sprintf("%s:%v-%v/%s", start.IP, start.PrivatePort, ports[i].PrivatePort, start.Type)
|
||||||
|
if start.PublicPort != 0 {
|
||||||
|
itemPortStr = fmt.Sprintf("%s:%v-%v->%v-%v/%s", start.IP, start.PublicPort, ports[i].PublicPort, start.PrivatePort, ports[i].PrivatePort, start.Type)
|
||||||
|
}
|
||||||
|
if len(start.IP) == 0 {
|
||||||
|
itemPortStr = strings.TrimPrefix(itemPortStr, ":")
|
||||||
|
}
|
||||||
|
datas = append(datas, itemPortStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return datas
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPortStats(ports []form.ExposedPort) (nat.PortMap, error) {
|
||||||
|
portMap := make(nat.PortMap)
|
||||||
|
if len(ports) == 0 {
|
||||||
|
return portMap, nil
|
||||||
|
}
|
||||||
|
for _, port := range ports {
|
||||||
|
if strings.Contains(port.ContainerPort, "-") {
|
||||||
|
if !strings.Contains(port.HostPort, "-") {
|
||||||
|
return portMap, errorx.NewBiz("exposed port error")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostStart := cast.ToInt(strings.Split(port.HostPort, "-")[0])
|
||||||
|
hostEnd := cast.ToInt(strings.Split(port.HostPort, "-")[1])
|
||||||
|
containerStart := cast.ToInt(strings.Split(port.ContainerPort, "-")[0])
|
||||||
|
containerEnd := cast.ToInt(strings.Split(port.ContainerPort, "-")[1])
|
||||||
|
if (hostEnd-hostStart) <= 0 || (containerEnd-containerStart) <= 0 {
|
||||||
|
return portMap, errorx.NewBiz("exposed port error")
|
||||||
|
}
|
||||||
|
if (containerEnd - containerStart) != (hostEnd - hostStart) {
|
||||||
|
return portMap, errorx.NewBiz("exposed port error")
|
||||||
|
}
|
||||||
|
for i := 0; i <= hostEnd-hostStart; i++ {
|
||||||
|
bindItem := nat.PortBinding{HostPort: strconv.Itoa(hostStart + i), HostIP: port.HostIP}
|
||||||
|
portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
portItem := 0
|
||||||
|
if strings.Contains(port.HostPort, "-") {
|
||||||
|
portItem = cast.ToInt(strings.Split(port.HostPort, "-")[0])
|
||||||
|
} else {
|
||||||
|
portItem = cast.ToInt(port.HostPort)
|
||||||
|
}
|
||||||
|
bindItem := nat.PortBinding{HostPort: cast.ToString(portItem), HostIP: port.HostIP}
|
||||||
|
portMap[nat.Port(fmt.Sprintf("%s/%s", port.ContainerPort, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return portMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfigInfo(isCreate bool, req *form.ContainerCreate, oldContainer *types.ContainerJSON) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
|
||||||
|
var config container.Config
|
||||||
|
var hostConf container.HostConfig
|
||||||
|
if !isCreate {
|
||||||
|
config = *oldContainer.Config
|
||||||
|
hostConf = *oldContainer.HostConfig
|
||||||
|
}
|
||||||
|
var networkConf network.NetworkingConfig
|
||||||
|
|
||||||
|
portMap, err := checkPortStats(req.ExposedPorts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
exposed := make(nat.PortSet)
|
||||||
|
for port := range portMap {
|
||||||
|
exposed[port] = struct{}{}
|
||||||
|
}
|
||||||
|
config.Image = req.Image
|
||||||
|
config.Cmd = req.Cmd
|
||||||
|
config.Entrypoint = req.Entrypoint
|
||||||
|
config.Env = req.Envs
|
||||||
|
config.Labels = stringsToMap(req.Labels)
|
||||||
|
config.ExposedPorts = exposed
|
||||||
|
config.OpenStdin = req.OpenStdin
|
||||||
|
config.Tty = req.Tty
|
||||||
|
|
||||||
|
hostConf.Privileged = req.Privileged
|
||||||
|
hostConf.AutoRemove = req.AutoRemove
|
||||||
|
hostConf.CPUShares = req.CPUShares
|
||||||
|
hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)}
|
||||||
|
if req.RestartPolicy == "on-failure" {
|
||||||
|
hostConf.RestartPolicy.MaximumRetryCount = 5
|
||||||
|
}
|
||||||
|
hostConf.NanoCPUs = int64(req.NanoCPUs * 1000000000)
|
||||||
|
hostConf.Memory = int64(req.Memory * 1024 * 1024 * 1024)
|
||||||
|
hostConf.MemorySwap = 0
|
||||||
|
hostConf.PortBindings = portMap
|
||||||
|
hostConf.Binds = []string{}
|
||||||
|
hostConf.Mounts = []mount.Mount{}
|
||||||
|
hostConf.ShmSize = int64(req.ShmSize * 1024 * 1024 * 1024)
|
||||||
|
hostConf.CapAdd = req.CapAdd
|
||||||
|
hostConf.NetworkMode = container.NetworkMode(req.NetworkMode)
|
||||||
|
|
||||||
|
if len(req.Devices) > 0 {
|
||||||
|
hostConf.DeviceRequests = collx.ArrayMap(req.Devices, func(val form.DeviceRequest) container.DeviceRequest {
|
||||||
|
return container.DeviceRequest{
|
||||||
|
Driver: val.Driver,
|
||||||
|
Count: val.Count,
|
||||||
|
DeviceIDs: val.DeviceIDs,
|
||||||
|
Capabilities: [][]string{val.Capabilities},
|
||||||
|
Options: val.Options,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// hostConf.DeviceRequests = []container.DeviceRequest{
|
||||||
|
// {
|
||||||
|
// Driver: "nvidia",
|
||||||
|
// Count: 2, // 限制使用 2 个 GPU
|
||||||
|
// Capabilities: [][]string{
|
||||||
|
// {"gpu"},
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// hostConf.Runtime = "nvidia"
|
||||||
|
|
||||||
|
config.Volumes = make(map[string]struct{})
|
||||||
|
for _, volume := range req.Volumes {
|
||||||
|
if volume.Type == "volume" {
|
||||||
|
hostConf.Mounts = append(hostConf.Mounts, mount.Mount{
|
||||||
|
Type: mount.Type(volume.Type),
|
||||||
|
Source: volume.HostDir,
|
||||||
|
Target: volume.ContainerDir,
|
||||||
|
})
|
||||||
|
config.Volumes[volume.ContainerDir] = struct{}{}
|
||||||
|
} else {
|
||||||
|
hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.HostDir, volume.ContainerDir, volume.Mode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &config, &hostConf, &networkConf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringsToMap(list []string) map[string]string {
|
||||||
|
var labelMap = make(map[string]string)
|
||||||
|
for _, label := range list {
|
||||||
|
if strings.Contains(label, "=") {
|
||||||
|
sps := strings.SplitN(label, "=", 2)
|
||||||
|
labelMap[sps[0]] = sps[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labelMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *types.NetworkSettings) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var oldNetworkConf network.NetworkingConfig
|
||||||
|
if networkConf != nil {
|
||||||
|
for networkKey := range networkConf.Networks {
|
||||||
|
oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("recreate after container update failed, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil {
|
||||||
|
logx.Errorf("restart after container update failed, err: %v", err)
|
||||||
|
}
|
||||||
|
logx.Info("recreate after container update successful")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadVolumeBinds(binds []types.MountPoint) []form.Volume {
|
||||||
|
var datas []form.Volume
|
||||||
|
for _, bind := range binds {
|
||||||
|
var volumeItem form.Volume
|
||||||
|
volumeItem.Type = string(bind.Type)
|
||||||
|
if bind.Type == "volume" {
|
||||||
|
volumeItem.HostDir = bind.Name
|
||||||
|
} else {
|
||||||
|
volumeItem.HostDir = bind.Source
|
||||||
|
}
|
||||||
|
volumeItem.ContainerDir = bind.Destination
|
||||||
|
volumeItem.Mode = "ro"
|
||||||
|
if bind.RW {
|
||||||
|
volumeItem.Mode = "rw"
|
||||||
|
}
|
||||||
|
datas = append(datas, volumeItem)
|
||||||
|
}
|
||||||
|
return datas
|
||||||
|
}
|
||||||
27
server/internal/docker/api/docker.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/internal/docker/dkm"
|
||||||
|
"mayfly-go/pkg/biz"
|
||||||
|
"mayfly-go/pkg/req"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Docker struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) ReqConfs() *req.Confs {
|
||||||
|
reqs := [...]*req.Conf{
|
||||||
|
req.NewGet("/info", d.GetDockerInfo),
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.NewConfs("docker", reqs[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Docker) GetDockerInfo(rc *req.Ctx) {
|
||||||
|
host := rc.Query("host")
|
||||||
|
cli, err := dkm.GetCli(host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
info, err := cli.DockerClient.Info(rc.MetaCtx)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
rc.ResData = info
|
||||||
|
}
|
||||||
55
server/internal/docker/api/form/container.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package form
|
||||||
|
|
||||||
|
type ContainerOp struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
ContainerId string `json:"containerId" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerCreate struct {
|
||||||
|
Host string `json:"host" binding:"required"`
|
||||||
|
ContainerID string `json:"containerId"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Image string `json:"image" validate:"required"`
|
||||||
|
ForcePull bool `json:"forcePull"`
|
||||||
|
ExposedPorts []ExposedPort `json:"exposedPorts"`
|
||||||
|
Tty bool `json:"tty"`
|
||||||
|
OpenStdin bool `json:"openStdin"`
|
||||||
|
Cmd []string `json:"cmd"`
|
||||||
|
Entrypoint []string `json:"entrypoint"`
|
||||||
|
CPUShares int64 `json:"cpuShares"`
|
||||||
|
NanoCPUs float64 `json:"nanoCpus"`
|
||||||
|
Memory float64 `json:"memory"`
|
||||||
|
CapAdd []string `json:"capAdd"`
|
||||||
|
ShmSize float64 `json:"shmSize"`
|
||||||
|
NetworkMode string `json:"networkMode"`
|
||||||
|
Privileged bool `json:"privileged"`
|
||||||
|
AutoRemove bool `json:"autoRemove"`
|
||||||
|
RestartPolicy string `json:"restartPolicy"`
|
||||||
|
Volumes []Volume `json:"volumes"`
|
||||||
|
Devices []DeviceRequest `json:"devices"`
|
||||||
|
Runtime string `json:"runtime"`
|
||||||
|
Labels []string `json:"labels"`
|
||||||
|
Envs []string `json:"envs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExposedPort struct {
|
||||||
|
HostIP string `json:"hostIP"`
|
||||||
|
HostPort string `json:"hostPort"`
|
||||||
|
ContainerPort string `json:"containerPort"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Volume struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
HostDir string `json:"hostDir"`
|
||||||
|
ContainerDir string `json:"containerDir"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceRequest struct {
|
||||||
|
Driver string `json:"driver"` // Name of device driver
|
||||||
|
Count int `json:"count"` // Number of devices to request (-1 = All)
|
||||||
|
DeviceIDs []string `json:"deviceIds"` // List of device IDs as recognizable by the device driver
|
||||||
|
Capabilities []string `json:"capabilities"` // An OR list of AND lists of device capabilities (e.g. "gpu")
|
||||||
|
Options map[string]string `json:"Options"` // Options to pass onto the device driver
|
||||||
|
}
|
||||||
6
server/internal/docker/api/form/image.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package form
|
||||||
|
|
||||||
|
type ImageOp struct {
|
||||||
|
Host string `json:"host" binding:"required"`
|
||||||
|
ImageId string `json:"imageId" binding:"required"`
|
||||||
|
}
|
||||||
111
server/internal/docker/api/image.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mayfly-go/internal/docker/api/form"
|
||||||
|
"mayfly-go/internal/docker/api/vo"
|
||||||
|
"mayfly-go/internal/docker/dkm"
|
||||||
|
"mayfly-go/internal/docker/imsg"
|
||||||
|
"mayfly-go/pkg/biz"
|
||||||
|
"mayfly-go/pkg/req"
|
||||||
|
"mayfly-go/pkg/utils/collx"
|
||||||
|
"mayfly-go/pkg/utils/jsonx"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Image) ReqConfs() *req.Confs {
|
||||||
|
reqs := [...]*req.Conf{
|
||||||
|
req.NewGet("", d.GetImages),
|
||||||
|
|
||||||
|
req.NewPost("/remove", d.ImageRemove).Log(req.NewLogSaveI(imsg.LogDockerImageRemove)),
|
||||||
|
|
||||||
|
req.NewGet("/save", d.ImageExport).NoRes(),
|
||||||
|
|
||||||
|
req.NewPost("/load", d.ImageLoad).Log(req.NewLogSaveI(imsg.LogDockerImageLoad)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return req.NewConfs("docker/images", reqs[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Image) GetImages(rc *req.Ctx) {
|
||||||
|
cli, err := dkm.GetCli(rc.Query("host"))
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
is, err := cli.ImageList()
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
containers, _ := cli.ContainerList()
|
||||||
|
imageId2Container := collx.ArrayToMap(containers, func(item container.Summary) string {
|
||||||
|
return item.ImageID
|
||||||
|
})
|
||||||
|
rc.ResData = collx.ArrayMap[image.Summary, vo.Image](is, func(val image.Summary) vo.Image {
|
||||||
|
c := vo.Image{
|
||||||
|
Id: val.ID,
|
||||||
|
Size: val.Size,
|
||||||
|
CreateTime: time.Unix(val.Created, 0),
|
||||||
|
Tags: val.RepoTags,
|
||||||
|
IsUse: imageId2Container[val.ID].ID != "",
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Image) ImageRemove(rc *req.Ctx) {
|
||||||
|
imageOp := &form.ImageOp{}
|
||||||
|
biz.ErrIsNil(rc.BindJSON(imageOp))
|
||||||
|
|
||||||
|
rc.ReqParam = collx.Kvs("host", imageOp.Host, "imageId", imageOp.ImageId)
|
||||||
|
cli, err := dkm.GetCli(imageOp.Host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
err = cli.ImageRemove(imageOp.ImageId)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Image) ImageLoad(rc *req.Ctx) {
|
||||||
|
host := rc.PostForm("host")
|
||||||
|
biz.NotEmpty(host, "host cannot be empty")
|
||||||
|
rc.ReqParam = host
|
||||||
|
|
||||||
|
fileheader, err := rc.FormFile("file")
|
||||||
|
biz.ErrIsNilAppendErr(err, "read form file error: %s")
|
||||||
|
|
||||||
|
file, err := fileheader.Open()
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
resp, err := cli.DockerClient.ImageLoad(rc.MetaCtx, file)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
content, err := io.ReadAll(resp.Body)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
errMsg, _ := jsonx.GetStringByBytes(content, "errorDetail.message")
|
||||||
|
biz.IsTrue(errMsg == "", "%s", errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Image) ImageExport(rc *req.Ctx) {
|
||||||
|
host := rc.Query("host")
|
||||||
|
biz.NotEmpty(host, "host cannot be empty")
|
||||||
|
tag := rc.Query("tag")
|
||||||
|
biz.NotEmpty(tag, "tag cannot be empty")
|
||||||
|
|
||||||
|
cli, err := dkm.GetCli(host)
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
|
||||||
|
reader, err := cli.DockerClient.ImageSave(rc.MetaCtx, []string{tag}, client.ImageSaveWithPlatforms())
|
||||||
|
biz.ErrIsNil(err)
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
filename := rc.QueryDefault("filename", tag)
|
||||||
|
rc.Download(reader, fmt.Sprintf("%s.tar", filename))
|
||||||
|
}
|
||||||
38
server/internal/docker/api/vo/docker.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
ContainerId string `json:"containerId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ImageId string `json:"imageId"`
|
||||||
|
ImageName string `json:"imageName"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreateTime time.Time `json:"createTime"`
|
||||||
|
Networks []string `json:"networks"`
|
||||||
|
Ports []string `json:"ports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerStats struct {
|
||||||
|
ContainerId string `json:"containerId"`
|
||||||
|
|
||||||
|
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||||
|
SystemUsage uint64 `json:"systemUsage"`
|
||||||
|
CPUPercent float64 `json:"cpuPercent"`
|
||||||
|
PercpuUsage int `json:"percpuUsage"`
|
||||||
|
|
||||||
|
MemoryCache uint64 `json:"memoryCache"`
|
||||||
|
MemoryUsage uint64 `json:"memoryUsage"`
|
||||||
|
MemoryLimit uint64 `json:"memoryLimit"`
|
||||||
|
MemoryPercent float64 `json:"memoryPercent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
CreateTime time.Time `json:"createTime"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
IsUse bool `json:"isUse"`
|
||||||
|
}
|
||||||
286
server/internal/docker/dkm/client.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
package dkm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"mayfly-go/internal/machine/mcm"
|
||||||
|
"mayfly-go/pkg/logx"
|
||||||
|
"mayfly-go/pkg/pool"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
poolGroup = pool.NewPoolGroup[*Client]()
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultServer = "unix:///var/run/docker.sock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerServer struct {
|
||||||
|
Host string
|
||||||
|
|
||||||
|
Client *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
DockerClient *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.DockerClient.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Ping() error {
|
||||||
|
_, err := c.DockerClient.Ping(context.Background())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCli get docker cli
|
||||||
|
func GetCli(host string) (*Client, error) {
|
||||||
|
if host == "" {
|
||||||
|
host = DefaultServer
|
||||||
|
}
|
||||||
|
pool, err := poolGroup.GetCachePool(host, func() (*Client, error) {
|
||||||
|
return NewClient(&DockerServer{Host: host})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pool.Get(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient new docker client
|
||||||
|
func NewClient(server *DockerServer) (*Client, error) {
|
||||||
|
if server.Host == "" {
|
||||||
|
server.Host = DefaultServer
|
||||||
|
}
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithHost(server.Host), client.WithAPIVersionNegotiation())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
DockerClient: cli,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ContainerList() ([]container.Summary, error) {
|
||||||
|
var (
|
||||||
|
options container.ListOptions
|
||||||
|
)
|
||||||
|
options.All = true
|
||||||
|
containers, err := c.DockerClient.ContainerList(context.Background(), options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return containers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ContainerStats(containerID string) (container.StatsResponse, error) {
|
||||||
|
var stats container.StatsResponse
|
||||||
|
res, err := c.DockerClient.ContainerStats(context.Background(), containerID, false)
|
||||||
|
if err != nil {
|
||||||
|
return stats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return stats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &stats); err != nil {
|
||||||
|
return stats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ContainerRestart(containerID string) error {
|
||||||
|
return c.DockerClient.ContainerRestart(context.Background(), containerID, container.StopOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ContainerStop(containerID string) error {
|
||||||
|
return c.DockerClient.ContainerStop(context.Background(), containerID, container.StopOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ContainerAttach(containerID string, wsConn *websocket.Conn, rows, cols int) error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 创建exec配置(启用TTY)
|
||||||
|
execID, err := c.DockerClient.ContainerExecCreate(ctx, containerID, container.ExecOptions{
|
||||||
|
AttachStdin: true,
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
Tty: true,
|
||||||
|
Cmd: []string{"/bin/bash"}, // 或指定其他shell
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hijackedResp, err := c.DockerClient.ContainerExecAttach(ctx, execID.ID, container.ExecAttachOptions{
|
||||||
|
Tty: true,
|
||||||
|
ConsoleSize: &[2]uint{cast.ToUint(rows), cast.ToUint(cols)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer hijackedResp.Close()
|
||||||
|
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte("\033[2J\033[3J\033[1;1H")) // 清屏
|
||||||
|
|
||||||
|
// 转发容器输出到前端
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
n, err := hijackedResp.Reader.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
logx.ErrorTrace("Read container output error:", err)
|
||||||
|
}
|
||||||
|
// 容器退出时主动关闭WebSocket
|
||||||
|
wsConn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, buf[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
_, input, err := wsConn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
if websocket.IsUnexpectedCloseError(err) {
|
||||||
|
logx.Debug("WebSocket closed:", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析消息
|
||||||
|
msgObj, err := mcm.ParseMsg(input)
|
||||||
|
if err != nil {
|
||||||
|
wsConn.WriteMessage(websocket.TextMessage, []byte("failed to parse the message content..."))
|
||||||
|
logx.Error("terminal message parsing failed: ", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msgObj.Type {
|
||||||
|
case mcm.MsgTypeResize:
|
||||||
|
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||||
|
c.DockerClient.ContainerExecResize(ctx, execID.ID, container.ResizeOptions{
|
||||||
|
Height: cast.ToUint(msgObj.Rows),
|
||||||
|
Width: cast.ToUint(msgObj.Cols),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case mcm.MsgTypeData:
|
||||||
|
data := []byte(msgObj.Msg)
|
||||||
|
hijackedResp.Conn.Write(data)
|
||||||
|
case mcm.MsgTypePing:
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ContainerRemove(containerID string) error {
|
||||||
|
return c.DockerClient.ContainerRemove(context.Background(), containerID, container.RemoveOptions{Force: true, RemoveVolumes: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ContainerInspect(containerID string) (types.ContainerJSON, error) {
|
||||||
|
return c.DockerClient.ContainerInspect(context.Background(), containerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ImageRemove(imageID string) error {
|
||||||
|
if _, err := c.DockerClient.ImageRemove(context.Background(), imageID, image.RemoveOptions{Force: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) ImageList() ([]image.Summary, error) {
|
||||||
|
return c.DockerClient.ImageList(context.Background(), image.ListOptions{
|
||||||
|
All: false,
|
||||||
|
ContainerCount: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) PullImage(imageName string, force bool) error {
|
||||||
|
if !force {
|
||||||
|
exist, err := c.CheckImageExist(imageName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exist {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := c.DockerClient.ImagePull(context.Background(), imageName, image.PullOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) GetImageIDByName(imageName string) (string, error) {
|
||||||
|
filter := filters.NewArgs()
|
||||||
|
filter.Add("reference", imageName)
|
||||||
|
list, err := c.DockerClient.ImageList(context.Background(), image.ListOptions{
|
||||||
|
Filters: filter,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(list) > 0 {
|
||||||
|
return list[0].ID, nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) CheckImageExist(imageName string) (bool, error) {
|
||||||
|
filter := filters.NewArgs()
|
||||||
|
filter.Add("reference", imageName)
|
||||||
|
list, err := c.DockerClient.ImageList(context.Background(), image.ListOptions{
|
||||||
|
Filters: filter,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(list) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) CreateNetwork(name string) error {
|
||||||
|
_, err := c.DockerClient.NetworkCreate(context.Background(), name, network.CreateOptions{
|
||||||
|
Driver: "bridge",
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) NetworkExist(name string) bool {
|
||||||
|
var options network.ListOptions
|
||||||
|
options.Filters = filters.NewArgs(filters.Arg("name", name))
|
||||||
|
networks, err := c.DockerClient.NetworkList(context.Background(), options)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(networks) > 0
|
||||||
|
}
|
||||||
8
server/internal/docker/imsg/en.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package imsg
|
||||||
|
|
||||||
|
import "mayfly-go/pkg/i18n"
|
||||||
|
|
||||||
|
var En = map[i18n.MsgId]string{
|
||||||
|
LogDockerContainerStop: "Container - Stop",
|
||||||
|
LogDockerContainerRestart: "Container - Restart",
|
||||||
|
}
|
||||||
21
server/internal/docker/imsg/imsg.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package imsg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mayfly-go/internal/pkg/consts"
|
||||||
|
"mayfly-go/pkg/i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
i18n.AppendLangMsg(i18n.Zh_CN, Zh_CN)
|
||||||
|
i18n.AppendLangMsg(i18n.En, En)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogDockerContainerStop = iota + consts.ImsgNumDocker
|
||||||
|
LogDockerContainerRemove
|
||||||
|
LogDockerContainerRestart
|
||||||
|
LogDockerContainerCreate
|
||||||
|
|
||||||
|
LogDockerImageRemove
|
||||||
|
LogDockerImageLoad
|
||||||
|
)
|
||||||
13
server/internal/docker/imsg/zh_cn.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package imsg
|
||||||
|
|
||||||
|
import "mayfly-go/pkg/i18n"
|
||||||
|
|
||||||
|
var Zh_CN = map[i18n.MsgId]string{
|
||||||
|
LogDockerContainerStop: "容器-停止",
|
||||||
|
LogDockerContainerRemove: "容器-删除",
|
||||||
|
LogDockerContainerRestart: "容器-重启",
|
||||||
|
LogDockerContainerCreate: "容器-创建",
|
||||||
|
|
||||||
|
LogDockerImageRemove: "镜像-删除",
|
||||||
|
LogDockerImageLoad: "镜像-导入",
|
||||||
|
}
|
||||||
7
server/internal/docker/init/init.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import "mayfly-go/internal/docker/api"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
api.InitIoc()
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Resize = 1
|
MsgTypeResize = 1
|
||||||
Data = 2
|
MsgTypeData = 2
|
||||||
Ping = 3
|
MsgTypePing = 3
|
||||||
|
|
||||||
MsgSplit = "|"
|
MsgSplit = "|"
|
||||||
)
|
)
|
||||||
@@ -202,7 +202,7 @@ func (ts *TerminalSession) receiveWsMsg() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 解析消息
|
// 解析消息
|
||||||
msgObj, err := parseMsg(wsData)
|
msgObj, err := ParseMsg(wsData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ts.WriteToWs(GetErrorContentRn("failed to parse the message content..."))
|
ts.WriteToWs(GetErrorContentRn("failed to parse the message content..."))
|
||||||
logx.Error("machine ssh terminal message parsing failed: ", err)
|
logx.Error("machine ssh terminal message parsing failed: ", err)
|
||||||
@@ -210,13 +210,13 @@ func (ts *TerminalSession) receiveWsMsg() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch msgObj.Type {
|
switch msgObj.Type {
|
||||||
case Resize:
|
case MsgTypeResize:
|
||||||
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||||
if err := ts.terminal.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
|
if err := ts.terminal.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
|
||||||
logx.Error("ssh pty change windows size failed")
|
logx.Error("ssh pty change windows size failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case Data:
|
case MsgTypeData:
|
||||||
data := []byte(msgObj.Msg)
|
data := []byte(msgObj.Msg)
|
||||||
if ts.handler != nil {
|
if ts.handler != nil {
|
||||||
if err := ts.handler.PreWriteHandle(data); err != nil {
|
if err := ts.handler.PreWriteHandle(data); err != nil {
|
||||||
@@ -232,7 +232,7 @@ func (ts *TerminalSession) receiveWsMsg() {
|
|||||||
logx.Errorf("failed to write data to the ssh terminal: %s", err)
|
logx.Errorf("failed to write data to the ssh terminal: %s", err)
|
||||||
ts.WriteToWs(GetErrorContentRn(fmt.Sprintf("failed to write data to the ssh terminal: %s", err.Error())))
|
ts.WriteToWs(GetErrorContentRn(fmt.Sprintf("failed to write data to the ssh terminal: %s", err.Error())))
|
||||||
}
|
}
|
||||||
case Ping:
|
case MsgTypePing:
|
||||||
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
_, err := ts.terminal.SshSession.SendRequest("ping", true, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ts.WriteToWs(GetErrorContentRn("the terminal connection has been disconnected..."))
|
ts.WriteToWs(GetErrorContentRn("the terminal connection has been disconnected..."))
|
||||||
@@ -249,7 +249,7 @@ func (ts *TerminalSession) WriteToWs(msg string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析消息
|
// 解析消息
|
||||||
func parseMsg(msg []byte) (*WsMsg, error) {
|
func ParseMsg(msg []byte) (*WsMsg, error) {
|
||||||
// 消息格式为 msgType|msgContent, 如果msgType为resize则为msgType|rows|cols
|
// 消息格式为 msgType|msgContent, 如果msgType为resize则为msgType|rows|cols
|
||||||
msgStr := string(msg)
|
msgStr := string(msg)
|
||||||
// 查找第一个 "|" 的位置
|
// 查找第一个 "|" 的位置
|
||||||
@@ -259,12 +259,12 @@ func parseMsg(msg []byte) (*WsMsg, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取消息类型, 提取第一个 "|" 之前的内容
|
// 获取消息类型, 提取第一个 "|" 之前的内容
|
||||||
msgType := cmp.Or(cast.ToInt(msgStr[:index]), Ping)
|
msgType := cmp.Or(cast.ToInt(msgStr[:index]), MsgTypePing)
|
||||||
// 其余内容则为消息内容
|
// 其余内容则为消息内容
|
||||||
msgContent := msgStr[index+1:]
|
msgContent := msgStr[index+1:]
|
||||||
|
|
||||||
wsMsg := &WsMsg{Type: msgType, Msg: msgContent}
|
wsMsg := &WsMsg{Type: msgType, Msg: msgContent}
|
||||||
if msgType == Resize {
|
if msgType == MsgTypeResize {
|
||||||
rowsAndCols := strings.Split(msgContent, MsgSplit)
|
rowsAndCols := strings.Split(msgContent, MsgSplit)
|
||||||
wsMsg.Rows = cmp.Or(cast.ToInt(rowsAndCols[0]), 80)
|
wsMsg.Rows = cmp.Or(cast.ToInt(rowsAndCols[0]), 80)
|
||||||
wsMsg.Cols = cmp.Or(cast.ToInt(rowsAndCols[1]), 80)
|
wsMsg.Cols = cmp.Or(cast.ToInt(rowsAndCols[1]), 80)
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ const (
|
|||||||
ImsgNumMongo = 80000
|
ImsgNumMongo = 80000
|
||||||
ImsgNumMsg = 90000
|
ImsgNumMsg = 90000
|
||||||
ImsgNumEs = 100000
|
ImsgNumEs = 100000
|
||||||
|
ImsgNumDocker = 110000
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ func (p *TagTree) CountTagResource(rc *req.Ctx) {
|
|||||||
CodePathLikes: collx.AsArray(tagPath),
|
CodePathLikes: collx.AsArray(tagPath),
|
||||||
}).GetCodePaths()...)
|
}).GetCodePaths()...)
|
||||||
|
|
||||||
dbCodes := entity.GetCodesByCodePaths(entity.TagTypeDb, p.tagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{
|
dbCodes := entity.GetCodesByCodePaths(entity.TagTypeDbInstance, p.tagTreeApp.GetAccountTags(accountId, &entity.TagTreeQuery{
|
||||||
Types: collx.AsArray(entity.TagTypeDb),
|
TypePaths: collx.AsArray(entity.NewTypePaths(entity.TagTypeDbInstance, entity.TagTypeAuthCert)),
|
||||||
CodePathLikes: collx.AsArray(tagPath),
|
CodePathLikes: collx.AsArray(tagPath),
|
||||||
}).GetCodePaths()...)
|
}).GetCodePaths()...)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
_ "mayfly-go/internal/auth/init"
|
_ "mayfly-go/internal/auth/init"
|
||||||
_ "mayfly-go/internal/common/init"
|
_ "mayfly-go/internal/common/init"
|
||||||
_ "mayfly-go/internal/db/init"
|
_ "mayfly-go/internal/db/init"
|
||||||
|
_ "mayfly-go/internal/docker/init"
|
||||||
_ "mayfly-go/internal/es/init"
|
_ "mayfly-go/internal/es/init"
|
||||||
_ "mayfly-go/internal/file/init"
|
_ "mayfly-go/internal/file/init"
|
||||||
_ "mayfly-go/internal/flow/init"
|
_ "mayfly-go/internal/flow/init"
|
||||||
|
|||||||