add some function
This commit is contained in:
@@ -10,6 +10,10 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
396
frontend/package-lock.json
generated
396
frontend/package-lock.json
generated
@@ -14,6 +14,9 @@
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"docx": "^9.5.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^4.0.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-router-dom": "^6.20.0",
|
||||
@@ -1513,6 +1516,28 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz",
|
||||
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
@@ -1540,6 +1565,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||
@@ -1953,6 +1985,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.11",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||
@@ -2087,6 +2128,26 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -2207,6 +2268,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.47.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz",
|
||||
"integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2222,6 +2301,15 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -2458,6 +2546,41 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz",
|
||||
"integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.0.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
@@ -2468,6 +2591,16 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -2863,6 +2996,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
|
||||
@@ -2873,6 +3017,12 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@@ -3218,6 +3368,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -3230,6 +3390,19 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -3240,6 +3413,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -3283,7 +3462,6 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
@@ -3295,6 +3473,12 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@@ -3367,6 +3551,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -3450,6 +3640,41 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz",
|
||||
"integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip/node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3474,6 +3699,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
@@ -3609,6 +3843,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
@@ -3766,6 +4006,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -3826,6 +4072,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -4039,6 +4292,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -4093,6 +4352,16 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
@@ -4203,6 +4472,21 @@
|
||||
"pify": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -4248,6 +4532,13 @@
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
@@ -4290,6 +4581,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -4373,6 +4674,21 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@@ -4392,6 +4708,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -4435,6 +4757,25 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@@ -4510,6 +4851,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||
@@ -4558,6 +4909,15 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -4715,6 +5075,12 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
@@ -4769,9 +5135,17 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "36.9.2",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||
@@ -4933,6 +5307,24 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"axios": "^1.6.2",
|
||||
"clsx": "^2.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"docx": "^9.5.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^4.0.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-router-dom": "^6.20.0",
|
||||
|
||||
@@ -22,6 +22,7 @@ import MonitoringPage from '@/pages/Monitoring'
|
||||
import ObjectStoragePage from '@/pages/ObjectStorage'
|
||||
import SnapshotReplicationPage from '@/pages/SnapshotReplication'
|
||||
import ShareShieldPage from '@/pages/ShareShield'
|
||||
import ExecutiveDashboardPage from '@/pages/ExecutiveDashboard'
|
||||
import Layout from '@/components/Layout'
|
||||
|
||||
// Create a client
|
||||
@@ -80,6 +81,7 @@ function App() {
|
||||
<Route path="iam" element={<IAMPage />} />
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="profile/:id" element={<ProfilePage />} />
|
||||
<Route path="reporting" element={<ExecutiveDashboardPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<Toaster />
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
Camera,
|
||||
Shield,
|
||||
ShieldCheck,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
FileBarChart
|
||||
} from 'lucide-react'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
@@ -80,6 +81,7 @@ export default function Layout() {
|
||||
{ name: 'Storage', href: '/backup?tab=storage', icon: HardDrive },
|
||||
{ name: 'Console', href: '/backup?tab=console', icon: Terminal },
|
||||
]
|
||||
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||
@@ -96,6 +98,7 @@ export default function Layout() {
|
||||
{ name: 'Monitoring & Logs', href: '/monitoring', icon: Activity },
|
||||
{ name: 'Alerts', href: '/alerts', icon: Bell },
|
||||
{ name: 'System', href: '/system', icon: Server },
|
||||
{ name: 'Reporting', href: '/reporting', icon: FileBarChart },
|
||||
]
|
||||
|
||||
if (user?.roles.includes('admin')) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
863
frontend/src/pages/ExecutiveDashboard.tsx
Normal file
863
frontend/src/pages/ExecutiveDashboard.tsx
Normal file
@@ -0,0 +1,863 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { backupAPI } from '@/api/backup'
|
||||
import { zfsApi, storageApi } from '@/api/storage'
|
||||
import { objectStorageApi } from '@/api/objectStorage'
|
||||
import { FileDown, FileText } from 'lucide-react'
|
||||
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||
import jsPDF from 'jspdf'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { Document, Packer, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, WidthType, AlignmentType } from 'docx'
|
||||
|
||||
const COLORS = ['#1d72f2', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']
|
||||
|
||||
export default function ExecutiveDashboard() {
|
||||
const reportRef = useRef<HTMLDivElement>(null)
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
|
||||
// Fetch data for dashboard
|
||||
const { data: clientsData } = useQuery({
|
||||
queryKey: ['backup-clients-all'],
|
||||
queryFn: () => backupAPI.listClients(),
|
||||
})
|
||||
|
||||
const { data: jobsData } = useQuery({
|
||||
queryKey: ['backup-jobs'],
|
||||
queryFn: () => backupAPI.listJobs({ limit: 100 }),
|
||||
})
|
||||
|
||||
const { data: poolsData } = useQuery({
|
||||
queryKey: ['storage-pools'],
|
||||
queryFn: () => backupAPI.listStoragePools(),
|
||||
})
|
||||
|
||||
// Fetch Storage (ZFS) data
|
||||
const { data: zfsPools = [] } = useQuery({
|
||||
queryKey: ['zfs-pools'],
|
||||
queryFn: () => zfsApi.listPools(),
|
||||
})
|
||||
|
||||
const { data: repositories = [] } = useQuery({
|
||||
queryKey: ['repositories'],
|
||||
queryFn: () => storageApi.listRepositories(),
|
||||
})
|
||||
|
||||
const { data: disks = [] } = useQuery({
|
||||
queryKey: ['disks'],
|
||||
queryFn: () => storageApi.listDisks(),
|
||||
})
|
||||
|
||||
// Fetch Object Storage data
|
||||
const { data: buckets = [] } = useQuery({
|
||||
queryKey: ['object-storage-buckets'],
|
||||
queryFn: () => objectStorageApi.listBuckets(),
|
||||
})
|
||||
|
||||
const { data: objectStorageUsers = [] } = useQuery({
|
||||
queryKey: ['object-storage-users'],
|
||||
queryFn: () => objectStorageApi.listUsers(),
|
||||
})
|
||||
|
||||
const clients = clientsData?.clients || []
|
||||
const jobs = jobsData?.jobs || []
|
||||
const pools = poolsData?.pools || []
|
||||
|
||||
// Calculate statistics
|
||||
const totalClients = clients.length
|
||||
const filesystemClients = clients.filter(c => c.category === 'File' || !c.category).length
|
||||
const databaseClients = clients.filter(c => c.category === 'Database').length
|
||||
const virtualizationClients = clients.filter(c => c.category === 'Virtual').length
|
||||
|
||||
const totalJobs = jobs.length
|
||||
const successfulJobs = jobs.filter(j => j.status === 'Completed').length
|
||||
const failedJobs = jobs.filter(j => j.status === 'Failed').length
|
||||
const runningJobs = jobs.filter(j => j.status === 'Running').length
|
||||
|
||||
// Backup Storage metrics
|
||||
const totalBackupStorage = pools.reduce((sum, p) => sum + (p.total_bytes || 0), 0)
|
||||
const usedBackupStorage = pools.reduce((sum, p) => sum + (p.used_bytes || 0), 0)
|
||||
const backupStorageUtilization = totalBackupStorage > 0 ? (usedBackupStorage / totalBackupStorage) * 100 : 0
|
||||
|
||||
// ZFS Storage metrics
|
||||
const totalZFSStorage = zfsPools.reduce((sum, p) => sum + (p.size_bytes || 0), 0)
|
||||
const usedZFSStorage = zfsPools.reduce((sum, p) => sum + (p.used_bytes || 0), 0)
|
||||
const zfsStorageUtilization = totalZFSStorage > 0 ? (usedZFSStorage / totalZFSStorage) * 100 : 0
|
||||
|
||||
// Object Storage metrics
|
||||
const totalObjectStorage = buckets.reduce((sum, b) => sum + (b.size || 0), 0)
|
||||
const totalObjects = buckets.reduce((sum, b) => sum + (b.objects || 0), 0)
|
||||
const publicBuckets = buckets.filter(b => b.access_policy !== 'private').length
|
||||
const privateBuckets = buckets.filter(b => b.access_policy === 'private').length
|
||||
|
||||
// Prepare chart data
|
||||
const clientDistributionData = [
|
||||
{ name: 'Filesystem', value: filesystemClients, color: COLORS[0] },
|
||||
{ name: 'Database', value: databaseClients, color: COLORS[1] },
|
||||
{ name: 'Virtualization', value: virtualizationClients, color: COLORS[2] },
|
||||
]
|
||||
|
||||
const jobStatusData = [
|
||||
{ name: 'Successful', value: successfulJobs, color: COLORS[1] },
|
||||
{ name: 'Failed', value: failedJobs, color: COLORS[3] },
|
||||
{ name: 'Running', value: runningJobs, color: COLORS[0] },
|
||||
{ name: 'Other', value: totalJobs - successfulJobs - failedJobs - runningJobs, color: COLORS[4] },
|
||||
]
|
||||
|
||||
const storageDistributionData = [
|
||||
{ name: 'Backup Storage', value: totalBackupStorage, used: usedBackupStorage, color: COLORS[0] },
|
||||
{ name: 'ZFS Storage', value: totalZFSStorage, used: usedZFSStorage, color: COLORS[1] },
|
||||
{ name: 'Object Storage', value: totalObjectStorage, used: totalObjectStorage, color: COLORS[2] },
|
||||
]
|
||||
|
||||
const bucketAccessData = [
|
||||
{ name: 'Private', value: privateBuckets, color: COLORS[3] },
|
||||
{ name: 'Public', value: publicBuckets, color: COLORS[1] },
|
||||
]
|
||||
|
||||
// Generate monthly backup trend (last 6 months)
|
||||
const monthlyTrendData = []
|
||||
const now = new Date()
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const date = new Date(now.getFullYear(), now.getMonth() - i, 1)
|
||||
const monthName = date.toLocaleDateString('en-US', { month: 'short' })
|
||||
// Mock data - in real app, this would come from API
|
||||
monthlyTrendData.push({
|
||||
month: monthName,
|
||||
backups: Math.floor(Math.random() * 50) + 20,
|
||||
size: Math.floor(Math.random() * 500) + 200,
|
||||
})
|
||||
}
|
||||
|
||||
// Export to PDF
|
||||
const exportToPDF = async () => {
|
||||
if (!reportRef.current) return
|
||||
|
||||
setIsExporting(true)
|
||||
try {
|
||||
const canvas = await html2canvas(reportRef.current, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
})
|
||||
|
||||
const imgData = canvas.toDataURL('image/png')
|
||||
const pdf = new jsPDF('p', 'mm', 'a4')
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth()
|
||||
const pdfHeight = pdf.internal.pageSize.getHeight()
|
||||
const imgWidth = canvas.width
|
||||
const imgHeight = canvas.height
|
||||
const ratio = Math.min(pdfWidth / imgWidth, pdfHeight / imgHeight)
|
||||
const imgScaledWidth = imgWidth * ratio
|
||||
const imgScaledHeight = imgHeight * ratio
|
||||
const xOffset = (pdfWidth - imgScaledWidth) / 2
|
||||
const yOffset = (pdfHeight - imgScaledHeight) / 2
|
||||
|
||||
pdf.addImage(imgData, 'PNG', xOffset, yOffset, imgScaledWidth, imgScaledHeight)
|
||||
|
||||
// Add page breaks if content is too long
|
||||
const totalPages = Math.ceil(imgScaledHeight / pdfHeight)
|
||||
if (totalPages > 1) {
|
||||
for (let i = 1; i < totalPages; i++) {
|
||||
pdf.addPage()
|
||||
const yPos = -((i * pdfHeight) - yOffset)
|
||||
pdf.addImage(imgData, 'PNG', xOffset, yPos, imgScaledWidth, imgScaledHeight)
|
||||
}
|
||||
}
|
||||
|
||||
pdf.save(`calypso-executive-report-${new Date().toISOString().split('T')[0]}.pdf`)
|
||||
} catch (error) {
|
||||
console.error('Error exporting to PDF:', error)
|
||||
alert('Failed to export PDF. Please try again.')
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Export to Word
|
||||
const exportToWord = async () => {
|
||||
setIsExporting(true)
|
||||
try {
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children: [
|
||||
new Paragraph({
|
||||
text: 'Calypso Executive Dashboard Report',
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
new Paragraph({
|
||||
text: `Generated on ${new Date().toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}`,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
new Paragraph({ text: '' }),
|
||||
|
||||
// Executive Summary
|
||||
new Paragraph({
|
||||
text: 'Executive Summary',
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Clients: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${totalClients}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Backup Jobs: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${totalJobs}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Success Rate: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${totalJobs > 0 ? ((successfulJobs / totalJobs) * 100).toFixed(2) : 0}%`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Backup Storage Utilization: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${backupStorageUtilization.toFixed(2)}%`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `ZFS Storage Utilization: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${zfsStorageUtilization.toFixed(2)}%`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Object Storage Buckets: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${buckets.length}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Objects: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${totalObjects}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({ text: '' }),
|
||||
|
||||
// Client Distribution
|
||||
new Paragraph({
|
||||
text: 'Client Distribution',
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({ children: [new Paragraph('Category')] }),
|
||||
new TableCell({ children: [new Paragraph('Count')] }),
|
||||
],
|
||||
}),
|
||||
...clientDistributionData.map(item =>
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({ children: [new Paragraph(item.name)] }),
|
||||
new TableCell({ children: [new Paragraph(item.value.toString())] }),
|
||||
],
|
||||
})
|
||||
),
|
||||
],
|
||||
}),
|
||||
new Paragraph({ text: '' }),
|
||||
|
||||
// Job Status
|
||||
new Paragraph({
|
||||
text: 'Backup Job Status',
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({ children: [new Paragraph('Status')] }),
|
||||
new TableCell({ children: [new Paragraph('Count')] }),
|
||||
],
|
||||
}),
|
||||
...jobStatusData.map(item =>
|
||||
new TableRow({
|
||||
children: [
|
||||
new TableCell({ children: [new Paragraph(item.name)] }),
|
||||
new TableCell({ children: [new Paragraph(item.value.toString())] }),
|
||||
],
|
||||
})
|
||||
),
|
||||
],
|
||||
}),
|
||||
new Paragraph({ text: '' }),
|
||||
|
||||
// Backup Storage Information
|
||||
new Paragraph({
|
||||
text: 'Backup Storage Information',
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Storage Pools: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${pools.length}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Capacity: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${(totalBackupStorage / (1024 ** 3)).toFixed(2)} GB`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Used Storage: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${(usedBackupStorage / (1024 ** 3)).toFixed(2)} GB`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Available Storage: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${((totalBackupStorage - usedBackupStorage) / (1024 ** 3)).toFixed(2)} GB`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({ text: '' }),
|
||||
|
||||
// ZFS Storage Information
|
||||
new Paragraph({
|
||||
text: 'ZFS Storage Information',
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total ZFS Pools: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${zfsPools.length}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Repositories: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${repositories.length}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Physical Disks: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${disks.length}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Capacity: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${(totalZFSStorage / (1024 ** 3)).toFixed(2)} GB`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Used Storage: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${(usedZFSStorage / (1024 ** 3)).toFixed(2)} GB`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({ text: '' }),
|
||||
|
||||
// Object Storage Information
|
||||
new Paragraph({
|
||||
text: 'Object Storage Information',
|
||||
heading: HeadingLevel.HEADING_1,
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Buckets: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${buckets.length}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Objects: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${totalObjects}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Storage: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${(totalObjectStorage / (1024 ** 3)).toFixed(2)} GB`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Total Users: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${objectStorageUsers.length}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Private Buckets: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${privateBuckets}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `Public Buckets: `,
|
||||
bold: true,
|
||||
}),
|
||||
new TextRun({
|
||||
text: `${publicBuckets}`,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const blob = await Packer.toBlob(doc)
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `calypso-executive-report-${new Date().toISOString().split('T')[0]}.docx`
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('Error exporting to Word:', error)
|
||||
alert('Failed to export Word document. Please try again.')
|
||||
} finally {
|
||||
setIsExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-background-dark p-6">
|
||||
<div className="max-w-[1600px] mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">Executive Dashboard</h1>
|
||||
<p className="text-slate-400 mt-1">Comprehensive overview of backup operations and system health</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={exportToPDF}
|
||||
disabled={isExporting}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary/90 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FileDown className="w-4 h-4" />
|
||||
{isExporting ? 'Exporting...' : 'Export PDF'}
|
||||
</button>
|
||||
<button
|
||||
onClick={exportToWord}
|
||||
disabled={isExporting}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
{isExporting ? 'Exporting...' : 'Export Word'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Report Content */}
|
||||
<div ref={reportRef} className="bg-white dark:bg-slate-900 rounded-xl p-8 space-y-8">
|
||||
{/* Report Header */}
|
||||
<div className="text-center border-b pb-6">
|
||||
<h2 className="text-3xl font-bold text-slate-900 dark:text-white">Calypso Executive Dashboard Report</h2>
|
||||
<p className="text-slate-600 dark:text-slate-400 mt-2">
|
||||
Generated on {new Date().toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium">Total Clients</div>
|
||||
<div className="text-3xl font-bold text-slate-900 dark:text-white mt-2">{totalClients}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{filesystemClients} Filesystem • {databaseClients} Database • {virtualizationClients} Virtualization
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium">Total Jobs</div>
|
||||
<div className="text-3xl font-bold text-slate-900 dark:text-white mt-2">{totalJobs}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{successfulJobs} Successful • {failedJobs} Failed • {runningJobs} Running
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium">Success Rate</div>
|
||||
<div className="text-3xl font-bold text-emerald-600 mt-2">
|
||||
{totalJobs > 0 ? ((successfulJobs / totalJobs) * 100).toFixed(1) : 0}%
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Based on {totalJobs} total jobs</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium">Backup Storage Utilization</div>
|
||||
<div className="text-3xl font-bold text-slate-900 dark:text-white mt-2">{backupStorageUtilization.toFixed(1)}%</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{(usedBackupStorage / (1024 ** 3)).toFixed(2)} GB / {(totalBackupStorage / (1024 ** 3)).toFixed(2)} GB
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium">ZFS Storage Utilization</div>
|
||||
<div className="text-3xl font-bold text-slate-900 dark:text-white mt-2">{zfsStorageUtilization.toFixed(1)}%</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{(usedZFSStorage / (1024 ** 3)).toFixed(2)} GB / {(totalZFSStorage / (1024 ** 3)).toFixed(2)} GB
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<div className="text-sm text-slate-600 dark:text-slate-400 font-medium">Object Storage Buckets</div>
|
||||
<div className="text-3xl font-bold text-slate-900 dark:text-white mt-2">{buckets.length}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{totalObjects} Objects • {(totalObjectStorage / (1024 ** 3)).toFixed(2)} GB
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Client Distribution */}
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Client Distribution</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={clientDistributionData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{clientDistributionData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Job Status */}
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Job Status Distribution</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={jobStatusData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="value" fill="#1d72f2">
|
||||
{jobStatusData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Storage Distribution */}
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Storage Distribution</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={storageDistributionData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip formatter={(value: number) => `${(value / (1024 ** 3)).toFixed(2)} GB`} />
|
||||
<Legend />
|
||||
<Bar dataKey="value" fill="#1d72f2" name="Total" />
|
||||
<Bar dataKey="used" fill="#10b981" name="Used" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Object Storage Bucket Access */}
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Object Storage Bucket Access</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={bucketAccessData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{bucketAccessData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monthly Trend */}
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Monthly Backup Trend</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={monthlyTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="backups" stroke="#1d72f2" name="Number of Backups" />
|
||||
<Line type="monotone" dataKey="size" stroke="#10b981" name="Size (GB)" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Backup Storage Pools Table */}
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Backup Storage Pools</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-300 dark:border-slate-700">
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Pool Name</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Total Size</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Used</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Available</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Utilization</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pools.map((pool) => {
|
||||
const totalGB = (pool.total_bytes || 0) / (1024 ** 3)
|
||||
const usedGB = (pool.used_bytes || 0) / (1024 ** 3)
|
||||
const availableGB = totalGB - usedGB
|
||||
const utilization = totalGB > 0 ? (usedGB / totalGB) * 100 : 0
|
||||
return (
|
||||
<tr key={pool.pool_id} className="border-b border-slate-200 dark:border-slate-700">
|
||||
<td className="py-2 text-sm text-slate-900 dark:text-white">{pool.name}</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{totalGB.toFixed(2)} GB</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{usedGB.toFixed(2)} GB</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{availableGB.toFixed(2)} GB</td>
|
||||
<td className="py-2 text-sm">
|
||||
<span className={`font-medium ${utilization > 80 ? 'text-red-600' : utilization > 60 ? 'text-amber-600' : 'text-emerald-600'}`}>
|
||||
{utilization.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ZFS Storage Pools Table */}
|
||||
{zfsPools.length > 0 && (
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">ZFS Storage Pools</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-300 dark:border-slate-700">
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Pool Name</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Total Size</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Used</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Available</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Utilization</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{zfsPools.map((pool) => {
|
||||
const totalGB = (pool.size_bytes || 0) / (1024 ** 3)
|
||||
const usedGB = (pool.used_bytes || 0) / (1024 ** 3)
|
||||
const availableGB = totalGB - usedGB
|
||||
const utilization = totalGB > 0 ? (usedGB / totalGB) * 100 : 0
|
||||
return (
|
||||
<tr key={pool.id} className="border-b border-slate-200 dark:border-slate-700">
|
||||
<td className="py-2 text-sm text-slate-900 dark:text-white">{pool.name}</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{totalGB.toFixed(2)} GB</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{usedGB.toFixed(2)} GB</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{availableGB.toFixed(2)} GB</td>
|
||||
<td className="py-2 text-sm">
|
||||
<span className={`font-medium ${utilization > 80 ? 'text-red-600' : utilization > 60 ? 'text-amber-600' : 'text-emerald-600'}`}>
|
||||
{utilization.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Object Storage Buckets Table */}
|
||||
{buckets.length > 0 && (
|
||||
<div className="bg-slate-50 dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Object Storage Buckets</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-300 dark:border-slate-700">
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Bucket Name</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Size</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Objects</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Access Policy</th>
|
||||
<th className="pb-2 text-sm font-semibold text-slate-700 dark:text-slate-300">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{buckets.map((bucket) => {
|
||||
const sizeGB = (bucket.size || 0) / (1024 ** 3)
|
||||
return (
|
||||
<tr key={bucket.name} className="border-b border-slate-200 dark:border-slate-700">
|
||||
<td className="py-2 text-sm text-slate-900 dark:text-white">{bucket.name}</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{sizeGB.toFixed(2)} GB</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">{bucket.objects || 0}</td>
|
||||
<td className="py-2 text-sm">
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
bucket.access_policy === 'private'
|
||||
? 'bg-red-500/10 text-red-600'
|
||||
: bucket.access_policy === 'public-read'
|
||||
? 'bg-amber-500/10 text-amber-600'
|
||||
: 'bg-emerald-500/10 text-emerald-600'
|
||||
}`}>
|
||||
{bucket.access_policy}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 text-sm text-slate-600 dark:text-slate-400">
|
||||
{new Date(bucket.creation_date).toLocaleDateString()}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user