Compare commits

...

40 Commits

Author SHA1 Message Date
DmitriyA e47161acd1 fixed a bug with collapsing the menu, improved the MUI skeleton, fixed several visual bugs 2025-04-22 08:59:35 -04:00
YurijO fd53b187d5 Merge pull request 'authorization-token' (#38) from authorization-token into rc
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/38
Reviewed-by: Vladislav Drozdov <ya2@ya.ru>
Reviewed-by: YurijO <ya@ya.ru>
2025-04-21 16:10:30 +03:00
DmitriyA b4d653f3a6 corrections after the code review
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-21 09:01:36 -04:00
DmitriyA 54cf5504a4 removed unnecessary components #2
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-21 03:32:30 -04:00
DmitriyA f38c8825fe removed unnecessary components 2025-04-21 03:28:14 -04:00
DmitriyA c7ebbcaf5c fixed a bug with multiple web socket connection 2025-04-21 02:55:11 -04:00
DmitriyA d83f05e2b5 set up an authorization session using tokens and cookies 2025-04-14 04:40:02 -04:00
YurijO bcdbd0f0fc Merge pull request 'redesign' (#36) from redesign into rc
test-org/trust-module-frontend/pipeline/pr-main Build succeeded
Reviewed-on: http://192.168.2.61/deployer3000/trust-module-frontend/pulls/36
Reviewed-by: Vladislav Drozdov <ya2@ya.ru>
Reviewed-by: YurijO <ya@ya.ru>
2025-04-10 14:57:52 +03:00
DmitriyA 08eaa274b2 Merge pull request 'Converted time to constants' (#35) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://192.168.2.61/deployer3000/trust-module-frontend/pulls/35
2025-04-10 12:36:47 +03:00
DmitriyA 22c5fcf02c Converted time to constants
test-org/trust-module-frontend/pipeline/pr-redesign Build queued... Details
2025-04-10 05:33:15 -04:00
yuobrezkov 2c9813fbb9 auto versioning added 2025-04-10 11:03:22 +03:00
DmitriyA c1b0f10e62 Merge pull request 'added an environment variable' (#33) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/33
2025-04-09 17:19:13 +03:00
DmitriyA 858f6e2c4c added an environment variable 2025-04-09 10:17:45 -04:00
DmitriyA 4eed2f364c Merge pull request 'added the api prefix' (#31) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/31
2025-04-09 17:03:45 +03:00
DmitriyA 5b25876056 added the api prefix
test-org/trust-module-frontend/pipeline/pr-redesign Build queued... Details
2025-04-09 10:00:44 -04:00
DmitriyA 9ee15160e5 Merge pull request 'optimized chart loading' (#30) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/30
2025-04-09 16:17:23 +03:00
DmitriyA e56b82bb66 optimized chart loading
test-org/trust-module-frontend/pipeline/pr-redesign Build queued... Details
2025-04-09 09:15:25 -04:00
DmitriyA 46484efdea Merge pull request 'debugging' (#29) from debugging into redesign
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/29
2025-04-09 14:57:13 +03:00
DmitriyA b6b3b36f5a fixed data interpolation and range allocation
test-org/trust-module-frontend/pipeline/pr-redesign This commit looks good Details
2025-04-09 07:47:51 -04:00
DmitriyA 46da90fbb6 Fixed the date and time display 2025-04-08 01:42:47 -04:00
DmitriyA 64401cadbc fixed data transmission via a web socket and left data transmission via http requests for historical data 2025-04-07 10:27:08 -04:00
DmitriyA a24b89220c fixed bugs
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-04 15:31:20 -04:00
DmitriyA 32ece2f0ff refactoring, fixed bugs with the web socket
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-02 19:50:08 -04:00
DmitriyA 4405c693aa Established a connection to the back using a web socket
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-04-01 11:50:00 -04:00
DmitriyA 6a4640ba93 Deleted the trash files, added a button to minimize the side menu, and adjusted the styles
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-03-28 07:24:28 -04:00
DmitriyA bd96278895 redesign and fix graphics
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details
2025-03-27 10:09:58 -04:00
DmitriyA ed2e03e202 adjusting the chart legend 2025-03-26 06:53:41 -04:00
yuobrezkov cf38b25678 conflict
test-org/trust-module-frontend/pipeline/pr-main Build succeeded
2025-03-26 13:22:08 +03:00
DmitriyA 3ee0c3a02c environment variables
test-org/trust-module-frontend/pipeline/pr-rc There was a failure building this commit Details
2025-03-26 06:11:00 -04:00
yuobrezkov b6b8042d11 conflict 2025-03-26 12:28:33 +03:00
DmitriyA fc1db66288 redesign of graphs and visualizations
test-org/trust-module-frontend/pipeline/pr-rc There was a failure building this commit Details
2025-03-26 05:16:52 -04:00
DmitriyA c077449b2c redesign of graphs and visualizations 2025-03-26 05:16:43 -04:00
yuobrezkov 15a8a146fc login modal ip changed
test-org/trust-module-frontend/pipeline/pr-main Build succeeded
2025-03-26 10:04:52 +03:00
yuobrezkov b601dedc0e deleted stagged
test-org/trust-module-frontend/pipeline/pr-main Build succeeded
2025-03-26 09:19:48 +03:00
yuobrezkov 9399f544d2 added new domain 2025-03-26 09:18:04 +03:00
DmitriyA 5ed1b448e5 refactored the graph 2 2025-03-25 11:48:10 -04:00
DmitriyA 2d714b5985 added the logo, refactored the graph 2025-03-25 08:54:21 -04:00
DmitriyA 3fc7ee0ac3 Merge branch 'redesign' of http://git.enode/deployer3000/trust-module-frontend into redesign 2025-03-20 09:34:15 -04:00
DmitriyA 13101ac57c Rewrote the graph using react-flow 2025-03-20 09:31:54 -04:00
YurijO 67f776efe3 Merge pull request 'Обновить src/Charts/PrometheusChart.jsx' (#21) from refactoring into rc
test-org/trust-module-frontend/pipeline/pr-main Build succeeded
Reviewed-on: http://192.168.2.61/deployer3000/trust-module-frontend/pulls/21
2025-03-19 16:58:06 +03:00
54 changed files with 4196 additions and 3158 deletions

40
Jenkinsfile vendored
View File

@ -30,7 +30,23 @@ pipeline {
stage ('Initialize variables') { stage ('Initialize variables') {
steps { steps {
script { script {
env.IMAGE_TAG = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim() def hasTags = sh(script: "git tag -l | wc -l", returnStdout: true).trim().toInteger() > 0
echo "${hasTags}"
def lastVersion = "0.0.0"
if (hasTags) {
lastVersion = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim()
}
echo "Last version: ${lastVersion}"
def (major, minor, patch) = lastVersion.tokenize('.')
def newVersion = "${major}.${minor}.${patch.toInteger() + 1}"
echo "New version: ${newVersion}"
env.IMAGE_TAG = newVersion
env.NEW_VERSION = newVersion
} }
} }
} }
@ -71,17 +87,29 @@ pipeline {
echo "Attempting to merge PR ${env.CHANGE_ID} into master..." echo "Attempting to merge PR ${env.CHANGE_ID} into master..."
withCredentials([usernamePassword(credentialsId: 'gitea_creds', usernameVariable: 'GITEA_USER', passwordVariable: 'GITEA_PASS')]) { withCredentials([usernamePassword(credentialsId: 'gitea_creds', usernameVariable: 'GITEA_USER', passwordVariable: 'GITEA_PASS')]) {
def prId = env.CHANGE_ID def prId = env.CHANGE_ID
sh """ sh """
curl -X POST \ curl -X POST \
-u "${GITEA_USER}:${GITEA_PASS}" \ -u "${GITEA_USER}:${GITEA_PASS}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"do":"merge"}' \ -d '{"do":"merge"}' \
http://git.entcor/api/v1/repos/deployer3000/trust-module-frontend/pulls/${prId}/merge http://git.entcor/api/v1/repos/deployer3000/${env.IMAGE_NAME}/pulls/${prId}/merge
""" """
echo "PR ${prId} merged successfully into master!" def commitHash = sh(script: "git rev-parse HEAD~1", returnStdout: true).trim() // необходим для корректного отображения статусов
def context = "test-org/trust-module-frontend/pipeline/pr-${env.CHANGE_TARGET}" echo "PR ${prId} merged successfully into main!"
def commitHash = sh(script: "git rev-parse HEAD~1", returnStdout: true).trim() sleep(time: 15, unit: 'SECONDS')
notify(context, GITEA_USER, GITEA_PASS, env.GITEA_REPOSITORY_URL, "trust-module-frontend", commitHash, "success") sh "git checkout main && git pull origin main"
sh """
curl -v -X POST -u "${GITEA_USER}:${GITEA_PASS}" \
-H "Content-Type: application/json" \
-d '{"tag_name": "${env.NEW_VERSION}", "name": "Release ${env.NEW_VERSION}", "target_commitish": "main"}' \
"${env.GITEA_REPOSITORY_URL}deployer3000/${env.IMAGE_NAME}/releases"
"""
echo "New release succeeded!"
def context = "test-org/${env.IMAGE_NAME}/pipeline/pr-${env.CHANGE_TARGET}"
notify(context, GITEA_USER, GITEA_PASS, env.GITEA_REPOSITORY_URL, env.IMAGE_NAME, commitHash, "success")
} }
} }
} }

569
package-lock.json generated
View File

@ -8,6 +8,10 @@
"name": "trust-module", "name": "trust-module",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.8",
"@mui/material": "^6.4.7",
"axios": "^1.7.9", "axios": "^1.7.9",
"chart.js": "^4.0.0", "chart.js": "^4.0.0",
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
@ -50,7 +54,6 @@
"version": "7.26.2", "version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9",
@ -106,7 +109,6 @@
"version": "7.26.5", "version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.26.5", "@babel/parser": "^7.26.5",
@ -140,7 +142,6 @@
"version": "7.25.9", "version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/traverse": "^7.25.9", "@babel/traverse": "^7.25.9",
@ -182,7 +183,6 @@
"version": "7.25.9", "version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -192,7 +192,6 @@
"version": "7.25.9", "version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -226,7 +225,6 @@
"version": "7.26.7", "version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.26.7" "@babel/types": "^7.26.7"
@ -285,7 +283,6 @@
"version": "7.25.9", "version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.25.9", "@babel/code-frame": "^7.25.9",
@ -300,7 +297,6 @@
"version": "7.26.7", "version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.26.2", "@babel/code-frame": "^7.26.2",
@ -319,7 +315,6 @@
"version": "11.12.0", "version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@ -329,7 +324,6 @@
"version": "7.26.7", "version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.25.9", "@babel/helper-string-parser": "^7.25.9",
@ -339,6 +333,144 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
},
"node_modules/@emotion/styled": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2", "version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
@ -1023,7 +1155,6 @@
"version": "0.3.8", "version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.2.1", "@jridgewell/set-array": "^1.2.1",
@ -1038,7 +1169,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@ -1048,7 +1178,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@ -1058,14 +1187,12 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25", "version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@ -1077,6 +1204,247 @@
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
}, },
"node_modules/@mui/core-downloads-tracker": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.8.tgz",
"integrity": "sha512-vjP4+A1ybyCRhDZC7r5EPWu/gLseFZxaGyPdDl94vzVvk6Yj6gahdaqcjbhkaCrJjdZj90m3VioltWPAnWF/zw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.8.tgz",
"integrity": "sha512-LKGWiLWRyoOw3dWxZQ+lV//mK+4DVTTAiLd2ljmJdD6XV0rDB8JFKjRD9nyn9cJAU5XgWnii7ZR3c93ttUnMKg==",
"dependencies": {
"@babel/runtime": "^7.26.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^6.4.8",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.8.tgz",
"integrity": "sha512-5S9UTjKZZBd9GfbcYh/nYfD9cv6OXmj5Y7NgKYfk7JcSoshp8/pW5zP4wecRiroBSZX8wcrywSgogpVNO+5W0Q==",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.4.8",
"@mui/system": "^6.4.8",
"@mui/types": "~7.2.24",
"@mui/utils": "^6.4.8",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.4.8",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
},
"node_modules/@mui/private-theming": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.8.tgz",
"integrity": "sha512-sWwQoNSn6elsPTAtSqCf+w5aaGoh7AASURNmpy+QTTD/zwJ0Jgwt0ZaaP6mXq2IcgHxYnYloM/+vJgHPMkRKTQ==",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/utils": "^6.4.8",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.8.tgz",
"integrity": "sha512-oyjx1b1FvUCI85ZMO4trrjNxGm90eLN3Ohy0AP/SqK5gWvRQg1677UjNf7t6iETOKAleHctJjuq0B3aXO2gtmw==",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.8.tgz",
"integrity": "sha512-gV7iBHoqlsIenU2BP0wq14BefRoZcASZ/4LeyuQglayBl+DfLX5rEd3EYR3J409V2EZpR0NOM1LATAGlNk2cyA==",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/private-theming": "^6.4.8",
"@mui/styled-engine": "^6.4.8",
"@mui/types": "~7.2.24",
"@mui/utils": "^6.4.8",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.2.24",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
"integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "6.4.8",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.8.tgz",
"integrity": "sha512-C86gfiZ5BfZ51KqzqoHi1WuuM2QdSKoFhbkZeAfQRB+jCc4YNhhj11UXFVMMsqBgZ+Zy8IHNJW3M9Wj/LOwRXQ==",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/types": "~7.2.24",
"@types/prop-types": "^15.7.14",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils/node_modules/react-is": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
"integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g=="
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.32.1", "version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz",
@ -1461,18 +1829,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.14", "version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.18", "version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@ -1489,6 +1860,14 @@
"@types/react": "^18.0.0" "@types/react": "^18.0.0"
} }
}, },
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
@ -1749,6 +2128,39 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/babel-plugin-macros/node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -1853,7 +2265,6 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -2026,6 +2437,29 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2484,7 +2918,6 @@
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -2605,6 +3038,14 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.23.9", "version": "1.23.9",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
@ -2827,7 +3268,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -3093,6 +3533,11 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"node_modules/find-up": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -3446,6 +3891,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -3471,7 +3924,6 @@
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"parent-module": "^1.0.0", "parent-module": "^1.0.0",
@ -3535,6 +3987,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-async-function": { "node_modules/is-async-function": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@ -3605,7 +4062,6 @@
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"hasown": "^2.0.2" "hasown": "^2.0.2"
@ -3940,7 +4396,6 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"jsesc": "bin/jsesc" "jsesc": "bin/jsesc"
@ -3956,6 +4411,11 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -4023,6 +4483,11 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -4126,7 +4591,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
@ -4340,7 +4804,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"callsites": "^3.0.0" "callsites": "^3.0.0"
@ -4349,6 +4812,23 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4373,14 +4853,20 @@
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"engines": {
"node": ">=8"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
@ -4657,7 +5143,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@ -4939,6 +5424,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -5060,6 +5553,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -5077,7 +5575,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@ -5461,6 +5958,20 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -23,7 +23,12 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7", "@mui/material": "^6.4.7",
"@mui/icons-material": "^6.4.8" "@mui/icons-material": "^6.4.8",
"reactflow": "^11.11.4",
"vite-plugin-svgr": "^4.3.0",
"react-scripts": "^5.0.1",
"socket.io-client": "^4.8.1",
"antd": "^5.24.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",

26
public/system_monitor_icon.svg Executable file → Normal file
View File

@ -1,15 +1,11 @@
<svg <svg width="43" height="43" viewBox="0 0 43 43" fill="none" xmlns="http://www.w3.org/2000/svg">
width="100" height="100" <path d="M22.4391 0.0295059V0H21.5049H21.4951H20.5609V0.0295059C9.76424 0.48193 1.02264 8.95014 0.0884977 19.6116C0.0294994 20.2312 0 20.8607 0 21.5C0 22.1295 0.0294994 22.7589 0.0884977 23.3884C1.04231 34.3646 10.2756 43 21.4951 43H22.4391V39.2331H21.4951C12.37 39.2331 4.8182 32.3484 3.87423 23.3884H6.43083H14.6513C14.4349 22.7097 14.3169 21.9819 14.3169 21.2246C14.3169 20.6738 14.3858 20.1329 14.5038 19.6215H11.4752C12.37 14.8808 16.5884 11.3008 21.5049 11.3008C24.9367 11.3008 28.1226 13.0416 29.9909 15.8545H34.2584C32.0656 10.8484 27.0016 7.53385 21.5049 7.53385C14.5038 7.53385 8.58427 12.7761 7.65996 19.6215H6.2145H3.87423C4.8182 10.6615 12.37 3.77676 21.4951 3.77676H21.5049C30.63 3.77676 38.1818 10.6615 39.1258 19.6215H28.4962C28.6142 20.1427 28.6831 20.6738 28.6831 21.2246C28.6831 21.9819 28.5651 22.7097 28.3487 23.3884H28.919H31.5248H35.34H37.3067H43V21.5C43 9.95334 33.8552 0.511436 22.4391 0.0295059Z" fill="#428AC9"/>
viewBox="0 0 100 100" <path d="M22.7045 32.25C22.3112 32.2992 21.9081 32.3287 21.5049 32.3287C17.2472 32.3287 13.5205 29.6436 12.016 25.8472H8.06311C9.70523 31.7681 15.1528 36.0956 21.5049 36.0956C21.9081 36.0956 22.3112 36.0759 22.7045 36.0366V32.25Z" fill="#428AC9"/>
xmlns="http://www.w3.org/2000/svg" <path d="M25.2611 24.3817C23.383 26.457 20.1873 26.6242 18.1125 24.7457C16.0377 22.8769 15.8706 19.6706 17.7388 17.5954C19.617 15.5201 22.8127 15.3529 24.8875 17.2315C26.9623 19.1002 27.1294 22.3065 25.2611 24.3817Z" fill="url(#paint0_radial_2_3)"/>
fill="none" stroke="black" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"> <defs>
<!-- Окружность --> <radialGradient id="paint0_radial_2_3" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(19.8648 18.1752) scale(7.12571 7.12734)">
<circle cx="50" cy="50" r="45" stroke="#4CAF50" stroke-width="5" fill="none" /> <stop stop-color="#4A96D2"/>
<stop offset="1" stop-color="#1F2466"/>
<!-- График нагрузки --> </radialGradient>
<polyline points="20,70 35,40 50,60 65,30 80,50" stroke="#4CAF50" stroke-width="5" fill="none" /> </defs>
</svg>
<!-- Крестик в центре, символизирующий мониторинг -->
<line x1="45" y1="45" x2="55" y2="55" stroke="#4CAF50" stroke-width="4" />
<line x1="55" y1="45" x2="45" y2="55" stroke="#4CAF50" stroke-width="4" />
</svg>

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,39 +1,137 @@
import React, { useState, useMemo } from "react"; import React, { useState, useMemo, useEffect } from "react";
import { ThemeProvider, CssBaseline, Switch, Box } from "@mui/material"; import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress, Typography } from "@mui/material";
import Dashboard from "./Components/Layout/Dashboard"; import Dashboard from "./Components/Layout/Dashboard";
import LoginModal from "./Components/UI/LoginModal"; import LoginModal from "./Components/UI/LoginModal";
import { lightTheme, darkTheme } from "./Style/theme"; import { lightTheme, darkTheme } from "./Style/theme";
import "./Style/LoginModal.css"; import Logo from './assets/images/logo.svg?react';
import { checkAuth } from "./Components/UI/auth";
function App() { function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false); const [authState, setAuthState] = useState({
const [showLoginModal, setShowLoginModal] = useState(true); isAuthenticated: false,
isLoading: true,
user: null
});
const [showLoginModal, setShowLoginModal] = useState(false);
const [isDarkMode, setIsDarkMode] = useState( const [isDarkMode, setIsDarkMode] = useState(
window.matchMedia("(prefers-color-scheme: dark)").matches window.matchMedia("(prefers-color-scheme: dark)").matches
); );
const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]); const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]);
const handleLogin = () => { useEffect(() => {
setIsAuthenticated(true); const verifyAuth = async () => {
try {
const authStatus = await checkAuth();
setAuthState({
isAuthenticated: authStatus.isAuthenticated,
isLoading: false,
user: authStatus.user || null
});
setShowLoginModal(!authStatus.isAuthenticated);
} catch (error) {
console.error('Auth verification error:', error);
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
}
};
verifyAuth();
}, []);
const handleLogin = (userData) => {
setAuthState({
isAuthenticated: true,
isLoading: false,
user: userData
});
setShowLoginModal(false); setShowLoginModal(false);
}; };
const handleLogout = async () => {
try {
await fetch('http://192.168.2.39:3000/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
localStorage.removeItem('access_token');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
} catch (error) {
console.error('Logout failed:', error);
}
};
// Полноэкранный лоадер во время проверки авторизации
if (authState.isLoading) {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box sx={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
zIndex: 9999,
bgcolor: 'background.default'
}}>
<CircularProgress />
<Typography sx={{ mt: 2 }}>
Проверка авторизации...
</Typography>
</Box>
</ThemeProvider>
);
}
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
{!isAuthenticated && showLoginModal ? ( {!authState.isAuthenticated ? (
<LoginModal onLogin={handleLogin} onClose={() => setShowLoginModal(false)} /> <>
) : ( <Box sx={{
<Box sx={{ display: "flex", height: "100vh", overflow: "hidden", bgcolor: "background.default", color: "text.primary" }}> position: "fixed",
<Dashboard /> top: 24,
<Box sx={{ position: "absolute", top: 10, right: 10 }}> left: "50%",
<Switch checked={isDarkMode} onChange={() => setIsDarkMode((prev) => !prev)} /> transform: "translateX(-50%)",
zIndex: 1200,
'& svg': { width: 400, height: 'auto' }
}}>
<Logo />
</Box> </Box>
<LoginModal
open={showLoginModal}
onLogin={handleLogin}
onClose={() => setShowLoginModal(false)}
/>
</>
) : (
<Box sx={{
display: "flex",
height: "100vh",
overflow: "hidden",
bgcolor: "background.default"
}}>
<Dashboard user={authState.user} onLogout={handleLogout} />
<Switch
checked={isDarkMode}
onChange={() => setIsDarkMode(!isDarkMode)}
sx={{ position: "absolute", top: 10, right: 10 }}
/>
</Box> </Box>
)} )}
</ThemeProvider> </ThemeProvider>
); );
} }
export default App; export default App;

View File

@ -0,0 +1,20 @@
import React from 'react';
export const ConnectionStatusIndicator = ({ connectionStatus }) => {
return (
<div style={{
position: 'absolute',
top: '10px',
right: '10px',
padding: '5px 10px',
borderRadius: '4px',
backgroundColor: connectionStatus === 'connected' ? '#4CAF50' :
connectionStatus === 'error' ? '#F44336' : '#FFC107',
color: 'white',
fontSize: '12px'
}}>
{connectionStatus === 'connected' ? 'Online' :
connectionStatus === 'error' ? 'Connection Error' : 'Offline'}
</div>
);
};

View File

@ -0,0 +1,17 @@
import React from 'react';
export const CurrentRangeDisplay = ({ useCustomRange, startDate, endDate, selectedRange }) => {
return (
<div style={{
margin: '10px 0',
padding: '8px 12px',
backgroundColor: '#f0f7ff',
borderRadius: '4px',
borderLeft: '3px solid #4a6baf'
}}>
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
</div>
);
};

View File

@ -1,90 +1,229 @@
import React, { useState } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line, ResponsiveContainer } from 'recharts'; import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
import { HOUR, DAY } from './constants';
const TIME_FORMATS = {
LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов
MEDIUM: 'HH:mm', // Для диапазона > 1 часа
SHORT: 'HH:mm:ss' // Для коротких диапазонов
};
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => { const LineChartComponent = ({
const [selectionStart, setSelectionStart] = useState(null); chartData,
const [selectionEnd, setSelectionEnd] = useState(null); metricName,
colors,
onRangeSelect,
filteredData
}) => {
const [selectionArea, setSelectionArea] = useState(null);
const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null);
// Создаем массив уникальных временных меток
const allTimes = Object.values(chartData) const allTimestamps = Object.values(chartData)
.flat() .flat()
.map(point => point.time) .map(point => point.timestamp)
.filter((time, index, self) => self.indexOf(time) === index); .filter((timestamp, index, self) => self.indexOf(timestamp) === index)
.sort((a, b) => a - b);
const data = allTimestamps.map(timestamp => {
const point = { timestamp };
const firstPoint = Object.values(chartData)
.flat()
.find(p => p.timestamp === timestamp);
if (firstPoint) {
point.time = firstPoint.time;
point.fullTime = firstPoint.fullTime;
}
// Формируем данные для графика
const data = allTimes.map(time => {
const point = { time };
Object.keys(chartData).forEach(key => { Object.keys(chartData).forEach(key => {
const instanceData = chartData[key].find(p => p.time === time); const instanceData = chartData[key].find(p => p.timestamp === timestamp);
point[key] = instanceData ? instanceData.value : null; point[key] = instanceData ? instanceData.value : null;
}); });
return point; return point;
}); });
// Используем отфильтрованные данные, если они есть
const displayData = filteredData || data; const displayData = filteredData || data;
// Обработчик клика на графике const instanceKeys = displayData.length
const handleClick = (e) => { ? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
if (!e || !e.activeLabel) return; : [];
const clickedTime = e.activeLabel; // Функция для определения оптимального формата времени в зависимости от диапазона
const getTimeFormat = () => {
if (!data.length) return TIME_FORMATS.SHORT;
if (!selectionStart) { const range = data[data.length - 1].timestamp - data[0].timestamp;
setSelectionStart(clickedTime);
} else if (!selectionEnd) {
setSelectionEnd(clickedTime);
const startIndex = data.findIndex(point => point.time === selectionStart); if (range > DAY) return TIME_FORMATS.LONG;
const endIndex = data.findIndex(point => point.time === clickedTime); if (range > HOUR) return TIME_FORMATS.MEDIUM;
return TIME_FORMATS.SHORT;
onRangeSelect({ startIndex, endIndex }); };
setSelectionStart(null); useEffect(() => {
setSelectionEnd(null); const handleSelectStart = (e) => {
} if (isSelecting) {
e.preventDefault();
}
};
document.addEventListener('selectstart', handleSelectStart);
return () => document.removeEventListener('selectstart', handleSelectStart);
}, [isSelecting]);
const handleMouseDown = (e) => {
if (!e) return;
// Получаем индекс точки по координатам
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
setIsSelecting(true);
setSelectionArea({
start: data[activeIndex].timestamp,
end: null,
startIndex: activeIndex,
endIndex: null
});
};
const handleMouseMove = (e) => {
if (!isSelecting || !selectionArea?.start || !e) return;
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
setSelectionArea(prev => ({
...prev,
end: data[activeIndex].timestamp,
endIndex: activeIndex
}));
};
const handleMouseUp = () => {
if (!isSelecting || !selectionArea?.start || !selectionArea?.end) {
setIsSelecting(false);
setSelectionArea(null);
return;
}
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
// Нормализуем индексы к диапазону [0, 1] для родительского компонента
const normalizedStart = startIndex / (data.length - 1);
const normalizedEnd = endIndex / (data.length - 1);
onRangeSelect({
startIndex: normalizedStart,
endIndex: normalizedEnd
});
setIsSelecting(false);
setSelectionArea(null);
}; };
// Кастомный Tooltip для отображения значения
const CustomTooltip = ({ active, payload, label }) => { const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
const currentPoint = data.find(point => point.timestamp === label);
return ( return (
<div className="custom-tooltip" style={{ padding: '10px' }}> <div style={{
<p>{`Время: ${label}`}</p> backgroundColor: '#fff',
{payload.map((entry, index) => ( padding: '10px',
<p key={index} style={{}}> border: '1px solid #ccc',
{`Значение: ${entry.value}`} borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>
{currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')}
</p>
{payload.map((item, index) => (
<p key={index} style={{ color: item.color }}>
{`Значение: ${item.value}`}
</p> </p>
))} ))}
</div> </div>
); );
} }
return null; return null;
}; };
if (!data.length) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
}
return ( return (
<div> <div style={{ position: 'relative', height: '400px' }}>
<ResponsiveContainer width="100%" height={400}> <ResponsiveContainer width="100%" height="100%">
<LineChart data={displayData} onClick={handleClick}> <LineChart
<CartesianGrid strokeDasharray="3 3" /> data={displayData}
<XAxis dataKey="time" /> onMouseDown={handleMouseDown}
<YAxis /> onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
ref={chartRef}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="timestamp"
height={75}
tick={{ fontSize: 12, angle: -45, textAnchor: 'end' }}
interval={Math.max(1, Math.floor(data.length / 10))}
tickFormatter={(timestamp) => {
const date = new Date(timestamp);
const format = getTimeFormat();
if (format === 'dd.MM HH:mm') {
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} else if (format === 'HH:mm') {
return date.toLocaleString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
} else {
return date.toLocaleString('ru-RU', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
}}
/>
<YAxis tick={{ fontSize: 12 }} />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip />} />
<Legend /> {instanceKeys.map((instance, index) => (
{Object.keys(chartData).map((key, index) => (
<Line <Line
key={key} key={instance}
type="monotone" type=""
dataKey={key} dataKey={instance}
name={instance}
stroke={colors[index % colors.length]} stroke={colors[index % colors.length]}
name={key} strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
/> />
))} ))}
{selectionArea?.start && selectionArea?.end && (
<ReferenceArea
x1={selectionArea.start}
x2={selectionArea.end}
strokeOpacity={0.3}
fill="#4a6baf"
/>
)}
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
); );
}; };
export default LineChartComponent; export default React.memo(LineChartComponent);

View File

@ -0,0 +1,151 @@
import React from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { TIME_RANGES } from './constants';
export const TimeRangeSelector = ({
selectedRange,
handleRangeChange,
startDate,
setStartDate,
endDate,
setEndDate,
useCustomRange,
handleCustomRangeChange,
handleResetZoom
}) => {
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '15px',
alignItems: 'center',
marginBottom: '15px'
}}>
{/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px', display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
<div style={{ flex: '1' }}>
<label htmlFor="time-range" style={{
display: 'block',
marginBottom: '5px',
fontWeight: '500',
color: '#555'
}}>Стандартные диапазоны:</label>
<select
id="time-range"
value={selectedRange.value}
onChange={handleRangeChange}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd',
color: "#333",
backgroundColor: '#f9f9f9'
}}
>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
<button
onClick={handleResetZoom}
style={{
padding: '8px 16px',
backgroundColor: '#f0f0f0',
color: '#333',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s',
height: '36px',
whiteSpace: 'nowrap'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#e0e0e0'}
onMouseOut={(e) => e.target.style.backgroundColor = '#f0f0f0'}
>
Сбросить
</button>
</div>
{/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}>
<div style={{
marginBottom: '10px',
fontWeight: '500',
color: '#555'
}}>
Или укажите свой диапазон:
</div>
<div style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap'
}}>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Начальная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Конечная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<button
onClick={handleCustomRangeChange}
style={{
padding: '8px 16px',
backgroundColor: '#4a6baf',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
flex: '0 0 auto',
alignSelf: 'flex-end'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
>
Применить
</button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,35 @@
export const TIME_RANGES = [
{ label: '1 минута', value: 60, interval: 3000 },
{ label: '5 минут', value: 300, interval: 15000 },
{ label: '30 минут', value: 1800, interval: 90000 },
{ label: '1 час', value: 3600, interval: 180000 },
{ label: '3 часа', value: 10800, interval: 540000 },
{ label: '6 часов', value: 21600, interval: 1080000 },
{ label: '12 часов', value: 43200, interval: 2160000 },
{ label: '24 часа', value: 86400, interval: 4320000 },
{ label: '2 дня', value: 172800, interval: 8640000 },
{ label: '7 дней', value: 604800, interval: 30240000 },
{ label: '30 дней', value: 2592000, interval: 129600000 },
{ label: '90 дней', value: 7776000, interval: 388800000 },
{ label: '6 месяцев', value: 15552000, interval: 777600000 },
{ label: '9 месяцев', value: 23328000, interval: 1166400000 },
{ label: '1 год', value: 31536000, interval: 1576800000 },
];
export const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850'];
export const MAX_POINTS = 20;
// Для работы с временными интервалами (setTimeout и т.д.)
export const MS = 1;
export const SECOND_MS = 1000 * MS;
export const MINUTE_MS = 60 * SECOND_MS;
export const HOUR_MS = 60 * MINUTE_MS;
export const DAY_MS = 24 * HOUR_MS;
// Для работы с Unix-временем и API (Prometheus и т.д.)
export const SECOND = 1;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;

View File

@ -1,27 +0,0 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
const SystemStatusChart = ({ data }) => {
// Обрезаем массив, оставляя только последние 20 точек
const trimmedData = data.slice(-20);
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={trimmedData} // Используем обрезанный массив
margin={{
top: 5, right: 30, left: 20, bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>
</ResponsiveContainer>
);
};
export default SystemStatusChart;

View File

@ -1,267 +1,486 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import axios from 'axios'; import { webSocketManager } from './WebSocketManager';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import LineChartComponent from './Components/LineChartComponent'; import LineChartComponent from './Components/LineChartComponent';
import { TimeRangeSelector } from './Components/TimeRangeSelector';
import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator';
import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay';
import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants';
import axios from 'axios';
import Skeleton from '@mui/material/Skeleton';
import Box from '@mui/material/Box';
const MAX_POINTS = 20;
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; // Компонент Skeleton для графика
const TIME_RANGES = [ const ChartSkeleton = () => (
{ label: '1 минута', value: 60, interval: 3000 }, <Box sx={{
{ label: '5 минут', value: 300, interval: 15000 }, backgroundColor: '#fff',
{ label: '30 минут', value: 1800, interval: 90000 }, borderRadius: '8px',
{ label: '1 час', value: 3600, interval: 180000 }, padding: '20px',
{ label: '3 часа', value: 10800, interval: 540000 }, marginBottom: '20px',
{ label: '6 часов', value: 21600, interval: 1080000 }, position: 'relative'
{ label: '12 часов', value: 43200, interval: 2160000 }, }}>
{ label: '24 часа', value: 86400, interval: 4320000 }, <Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
{ label: '2 дня', value: 172800, interval: 8640000 }, <Skeleton variant="circular" width={16} height={16} />
{ label: '7 дней', value: 604800, interval: 30240000 }, </Box>
{ label: '30 дней', value: 2592000, interval: 129600000 },
{ label: '90 дней', value: 7776000, interval: 388800000 }, <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
{ label: '6 месяцев', value: 15552000, interval: 777600000 }, <Skeleton variant="text" width="40%" height={30} />
{ label: '9 месяцев', value: 23328000, interval: 1166400000 }, <Skeleton variant="text" width="30%" height={30} />
{ label: '1 год', value: 31536000, interval: 1576800000 }, </Box>
];
<Skeleton variant="rectangular" width="100%" height={300} />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((_, i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
))}
</Box>
</Box>
);
const PrometheusChart = ({ metricName }) => { const PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState({}); const [chartData, setChartData] = useState(null);
const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]);
const [startDate, setStartDate] = useState(new Date()); const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date());
const [useCustomRange, setUseCustomRange] = useState(false); const [useCustomRange, setUseCustomRange] = useState(false);
const [selectedGraphRange, setSelectedGraphRange] = useState(null); // Выбранный диапазон const [connectionStatus, setConnectionStatus] = useState('disconnected');
const [filteredData, setFilteredData] = useState(null); // Отфильтрованные данные const [selectedGraphRange, setSelectedGraphRange] = useState(null);
const [filteredData, setFilteredData] = useState(null);
const [isSelectingRange, setIsSelectingRange] = useState(false);
const [lastCustomRange, setLastCustomRange] = useState(null);
const intervalRef = useRef(null); const intervalRef = useRef(null);
const debounceRef = useRef(null);
// Функция для интерполяции данных const formatTime = useCallback((timestamp, rangeSeconds) => {
const interpolateData = (data, minPoints = 15) => { const ts = typeof timestamp === 'number' ? timestamp : Date.now();
if (data.length >= minPoints) return data; const date = new Date(ts);
const interpolatedData = []; // Определяем формат в зависимости от диапазона
for (let i = 0; i < data.length - 1; i++) { const showFullDate = rangeSeconds > 86400; // больше суток
interpolatedData.push(data[i]);
const currentPoint = data[i]; const timeOptions = {
const nextPoint = data[i + 1]; hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
};
// Вычисляем разницу во времени между точками const dateOptions = showFullDate ? {
const currentTime = new Date(currentPoint.time).getTime(); month: '2-digit',
const nextTime = new Date(nextPoint.time).getTime(); day: '2-digit',
const timeDiff = nextTime - currentTime; ...timeOptions
} : timeOptions;
// Добавляем промежуточные точки return {
const steps = Math.ceil((minPoints - data.length) / (data.length - 1)); display: date.toLocaleString('ru-RU', dateOptions),
for (let j = 1; j <= steps; j++) { fullDisplay: date.toLocaleString('ru-RU', {
const interpolatedTime = new Date(currentTime + (timeDiff * j) / (steps + 1)).toLocaleString(); year: 'numeric',
const interpolatedPoint = { time: interpolatedTime }; month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}),
timestamp: ts
};
}, []);
// Интерполируем значения для каждой метрики const calculateStep = useCallback((start, end) => {
Object.keys(currentPoint).forEach(key => { const range = end - start;
if (key !== 'time') { if (range <= MINUTE) return 1; // 1 мин
const currentValue = currentPoint[key]; if (range <= MINUTE * 5) return 5; // 5 мин
const nextValue = nextPoint[key]; if (range <= HOUR / 2) return 15; // 30 мин
interpolatedPoint[key] = currentValue + ((nextValue - currentValue) * j) / (steps + 1); if (range <= HOUR) return 30; // 1 час
} if (range <= HOUR * 3) return 60; // 3 часа
if (range <= HOUR * 6) return 120; // 6 часов
if (range <= DAY / 2) return 300; // 12 часов
if (range <= DAY) return 600; // 24 часа
return 1800; // > 24 часов
}, []);
const processMetricsData = useCallback((response) => {
console.log('Processing metrics data:', response);
if (response.metric !== metricName) return;
const dataArray = Array.isArray(response.data) ? response.data : [response.data];
if (!dataArray.length) return;
setChartData(prev => {
const newData = { ...(prev || {}) };
const rangeSeconds = useCustomRange
? (endDate.getTime() - startDate.getTime()) / 1000
: selectedRange.value;
dataArray.forEach(item => {
const instance = item.instance || 'default';
if (!newData[instance]) newData[instance] = [];
// Унифицированная конвертация timestamp
let timestamp;
if (typeof item.timestamp === 'number') {
// Определяем, в секундах или миллисекундах пришел timestamp
timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
} else {
timestamp = Date.now();
}
const value = parseFloat(item.value);
const formattedTime = formatTime(timestamp, rangeSeconds);
newData[instance].push({
time: formattedTime.display,
fullTime: formattedTime.fullDisplay,
value: value,
timestamp: timestamp
}); });
});
interpolatedData.push(interpolatedPoint); // Сортируем и ограничиваем данные
} Object.keys(newData).forEach(instance => {
newData[instance] = newData[instance]
.sort((a, b) => a.timestamp - b.timestamp)
.slice(-1000);
});
return newData;
});
}, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
const fetchData = useCallback(() => {
if (isSelectingRange) return;
const now = Math.floor(Date.now() / 1000);
const start = now - selectedRange.value;
const end = now;
const step = calculateStep(start, end);
webSocketManager.getMetricsRange(metricName, start, end, step)
.then(data => {
processMetricsData({ metric: metricName, data });
})
.catch(error => {
console.error('Error fetching metrics:', error);
});
}, [metricName, selectedRange.value, isSelectingRange, calculateStep, processMetricsData]);
const fetchCustomRangeData = useCallback(async () => {
// Добавляем проверку на валидность дат
if (!startDate || !endDate || startDate >= endDate) {
console.error('Invalid date range');
return;
} }
interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку const start = Math.floor(startDate.getTime() / 1000);
return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints const end = Math.ceil(endDate.getTime() / 1000); // Используем Math.ceil для конечной даты
}; const rangeSeconds = end - start;
const fetchData = async () => {
try { try {
let start, end; const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
params: {
if (useCustomRange) { metric: metricName,
start = Math.floor(startDate.getTime() / 1000); start,
end = Math.floor(endDate.getTime() / 1000); end,
} else { step: calculateStep(start, end)
end = Math.floor(Date.now() / 1000); }
start = end - selectedRange.value;
}
let step;
const range = end - start;
if (range <= 3600) step = 5;
else if (range <= 21600) step = 30;
else if (range <= 86400) step = 120;
else step = 300;
const response = await axios.get('http://192.168.2.39:3000/metrics', {
params: { metric: metricName, start, end, step },
}); });
/* if (response.data?.length) {
const response = await axios.get(`${process.env.REACT_APP_BACK_URL}/metrics`, { // Добавляем нормализацию timestamp
params: { metric: metricName, start, end, step }, const processedData = response.data.map(item => ({
}); */ ...item,
timestamp: item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000,
const result = response.data; value: parseFloat(item.value)
let metrics = Array.isArray(result) ? result : result.data || [];
if (!Array.isArray(metrics)) {
metrics = [];
}
const timePoints = [];
for (let t = start; t <= end; t += step) {
const date = new Date(t * 1000);
const formattedTime = range > 86400
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
timePoints.push(formattedTime);
}
const updatedData = {};
metrics.forEach(m => {
const date = new Date(m.timestamp);
const formattedTime = range > 86400
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const key = `${m.instance}-${m.device || m.scrape_job}`;
if (!updatedData[key]) updatedData[key] = {};
updatedData[key][formattedTime] = m.value;
});
const chartData = {};
Object.keys(updatedData).forEach(key => {
chartData[key] = timePoints.map(time => ({
time,
value: updatedData[key][time] ?? null,
})); }));
processMetricsData({
metric: metricName,
data: processedData
});
}
} catch (error) {
console.error('Ошибка при получении кастомных данных:', error);
}
}, [metricName, startDate, endDate, calculateStep, processMetricsData]);
const handleRangeChange = useCallback(async (event) => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
const selectedValue = event.target.value;
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
// Полный сброс состояния перед загрузкой новых данных
setChartData(null);
setSelectedRange(range);
setUseCustomRange(false);
setSelectedGraphRange(null);
setFilteredData(null);
const now = new Date();
setEndDate(now);
setStartDate(new Date(now.getTime() - range.value * 1000));
// Ждем завершения обновления состояния перед загрузкой
await new Promise(resolve => setTimeout(resolve, 0));
fetchData();
}, [fetchData]);
const handleCustomRangeChange = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setUseCustomRange(true);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
fetchCustomRangeData();
}, [fetchCustomRangeData]);
const interpolateData = useCallback((data, targetPointCount) => {
if (!data || data.length < 2) return data;
if (data.length >= targetPointCount) return data;
const interpolated = [];
const step = (data.length - 1) / (targetPointCount - 1);
for (let i = 0; i < targetPointCount; i++) {
const index = i * step;
const lowerIndex = Math.floor(index);
const upperIndex = Math.ceil(index);
if (lowerIndex === upperIndex) {
interpolated.push(data[lowerIndex]);
continue;
}
const fraction = index - lowerIndex;
const interpolatedPoint = {};
Object.keys(data[lowerIndex]).forEach(key => {
if (key === 'timestamp') {
interpolatedPoint[key] = data[lowerIndex][key] +
fraction * (data[upperIndex][key] - data[lowerIndex][key]);
// Добавляем отображаемое время
const { display, fullDisplay } = formatTime(interpolatedPoint[key],
(endDate - startDate) / 1000);
interpolatedPoint.time = display;
interpolatedPoint.fullTime = fullDisplay;
} else if (typeof data[lowerIndex][key] === 'number') {
interpolatedPoint[key] = data[lowerIndex][key] +
fraction * (data[upperIndex][key] - data[lowerIndex][key]);
} else {
interpolatedPoint[key] = data[lowerIndex][key];
}
}); });
setChartData(chartData); interpolated.push(interpolatedPoint);
} catch (error) {
console.error('Ошибка при загрузке метрик:', error);
} }
};
return interpolated;
}, []);
const handleRangeSelect = useCallback((range) => {
setLastCustomRange(range);
if (!range || !chartData) return;
setIsSelectingRange(true);
setSelectedGraphRange(range);
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
// Получаем все точки и сортируем по времени
const allPoints = Object.values(chartData).flat();
const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
// Вычисляем абсолютные индексы
const startIndex = Math.floor(range.startIndex * (sortedPoints.length - 1));
const endIndex = Math.floor(range.endIndex * (sortedPoints.length - 1));
// Фильтруем точки по выбранному диапазону
const filtered = sortedPoints.slice(startIndex, endIndex + 1);
// Применяем интерполяцию только если точек меньше 100
const interpolated = filtered.length < 100 ?
interpolateData(filtered, Math.min(100, filtered.length * 3)) :
filtered;
setFilteredData(interpolated);
setIsSelectingRange(false);
}, [chartData, interpolateData, formatTime]);
const handleResetZoom = useCallback(() => {
setSelectedGraphRange(null);
setFilteredData(null);
setIsSelectingRange(false);
if (useCustomRange) {
fetchCustomRangeData();
} else {
fetchData();
}
if (lastCustomRange) {
handleRangeSelect(lastCustomRange);
}
}, [fetchData, fetchCustomRangeData, useCustomRange, lastCustomRange, handleRangeSelect]);
useEffect(() => { useEffect(() => {
fetchData(); // Обработчик данных с сервера
const handleMetricsData = (data) => {
processMetricsData({ metric: metricName, data });
};
intervalRef.current = setInterval(() => { // Подписываемся на обновления метрики
fetchData(); const unsubscribe = webSocketManager.subscribe(metricName, handleMetricsData);
}, selectedRange.interval);
// Подписываемся на изменения статуса соединения
const unsubscribeStatus = webSocketManager.onConnectionStatusChange(setConnectionStatus);
return () => {
// Отписываемся при размонтировании компонента
unsubscribe();
unsubscribeStatus();
// Очищаем интервал обновления
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [metricName, processMetricsData]);
useEffect(() => {
if (useCustomRange && !isSelectingRange) {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
fetchCustomRangeData();
}, 500);
}
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, [useCustomRange, isSelectingRange, startDate, endDate, fetchCustomRangeData]);
useEffect(() => {
if (useCustomRange || isSelectingRange) return;
const fetchDataWrapper = () => {
try {
fetchData();
} catch (error) {
console.error('Error in interval fetch:', error);
}
};
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
fetchDataWrapper();
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
return () => { return () => {
if (intervalRef.current) { if (intervalRef.current) {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
} }
}; };
}, [metricName, selectedRange, useCustomRange, startDate, endDate]); }, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]);
const handleRangeChange = (event) => {
const selectedValue = event.target.value;
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
setSelectedRange(range);
setUseCustomRange(false);
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные
};
const handleCustomRangeChange = () => {
setUseCustomRange(true);
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные
};
useEffect(() => { useEffect(() => {
if (selectedGraphRange) { if (!selectedGraphRange || !chartData) {
const { startIndex, endIndex } = selectedGraphRange; setFilteredData(null);
const allTimes = Object.values(chartData) return;
.flat()
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index);
const data = allTimes.map(time => {
const point = { time };
Object.keys(chartData).forEach(key => {
const instanceData = chartData[key].find(p => p.time === time);
point[key] = instanceData ? instanceData.value : null;
});
return point;
});
const filtered = data.slice(startIndex, endIndex + 1);
// Интерполируем данные, если точек меньше 15
const interpolated = interpolateData(filtered, 15);
setFilteredData(interpolated); // Сохраняем интерполированные данные
} else {
setFilteredData(null); // Сбрасываем фильтрацию, если диапазон не выбран
} }
}, [selectedGraphRange, chartData]);
if (!Object.keys(chartData).length) return <p>Loading...</p>; const allPoints = Object.values(chartData).flat();
const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
const allTimes = Object.values(chartData) const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1));
.flat() const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1));
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index);
const data = allTimes.map(time => { const filtered = sortedPoints.slice(startIndex, endIndex + 1);
const point = { time }; const interpolated = filtered.length > 100 ?
Object.keys(chartData).forEach(key => { interpolateData(filtered, 100) :
const instanceData = chartData[key].find(p => p.time === time); filtered;
point[key] = instanceData ? instanceData.value : null;
}); setFilteredData(interpolated);
return point; }, [selectedGraphRange, chartData, interpolateData]);
});
if (chartData === null) {
return <ChartSkeleton />;
}
if (Object.keys(chartData).length === 0) {
return (
<Box sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
textAlign: 'center'
}}>
No data available
</Box>
);
}
return ( return (
<div> <div style={{
<div> backgroundColor: '#fff',
<label htmlFor="time-range">Выберите временной диапазон: </label> borderRadius: '8px',
<select id="time-range" value={selectedRange.value} onChange={handleRangeChange}> padding: '20px',
{TIME_RANGES.map(range => ( marginBottom: '20px',
<option key={range.value} value={range.value}>{range.label}</option> position: 'relative'
))} }}>
</select> <ConnectionStatusIndicator connectionStatus={connectionStatus} />
</div>
<div> <TimeRangeSelector
<label>Или выберите другой диапазон: </label> selectedRange={selectedRange}
<div> handleRangeChange={handleRangeChange}
<label>Начальная дата: </label> startDate={startDate}
<DatePicker setStartDate={setStartDate}
selected={startDate} endDate={endDate}
onChange={(date) => setStartDate(date)} setEndDate={setEndDate}
showTimeSelect useCustomRange={useCustomRange}
timeFormat="HH:mm" handleCustomRangeChange={handleCustomRangeChange}
timeIntervals={15} handleResetZoom={handleResetZoom}
dateFormat="yyyy-MM-dd HH:mm" />
/>
</div> <CurrentRangeDisplay
<div> useCustomRange={useCustomRange}
<label>Конечная дата: </label> startDate={startDate}
<DatePicker endDate={endDate}
selected={endDate} selectedRange={selectedRange}
onChange={(date) => setEndDate(date)} />
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
/>
</div>
<button onClick={handleCustomRangeChange}>Использовать кастомный диапазон</button>
</div>
<LineChartComponent <LineChartComponent
chartData={chartData} chartData={chartData}
metricName={metricName} metricName={metricName}
colors={COLORS} colors={COLORS}
description={metricName} onRangeSelect={handleRangeSelect}
onRangeSelect={setSelectedGraphRange}
filteredData={filteredData} filteredData={filteredData}
/> />
</div> </div>
); );
}; };
export default PrometheusChart; export default React.memo(PrometheusChart);

View File

@ -5,6 +5,22 @@ const SystemStatusChart = ({ data }) => {
// Обрезаем массив, оставляя только последние 20 точек // Обрезаем массив, оставляя только последние 20 точек
const trimmedData = data.slice(-20); const trimmedData = data.slice(-20);
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="custom-tooltip" style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc'
}}>
<p>{`Время: ${label}`}</p>
<p>{`Значение: ${payload[0].value}`}</p>
</div>
);
}
return null;
};
return ( return (
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
<LineChart <LineChart
@ -16,8 +32,7 @@ const SystemStatusChart = ({ data }) => {
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" /> <XAxis dataKey="time" />
<YAxis domain={[0, 100]} /> <YAxis domain={[0, 100]} />
<Tooltip /> <Tooltip content={<CustomTooltip />} />
<Legend />
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} /> <Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>

View File

@ -1,84 +0,0 @@
import React, { useState, useEffect } from "react";
import "../Style/SystemStatusTable.css";
import axios from "axios";
const SystemStatusTable = () => {
const [systemData, setSystemData] = useState([]);
const [expandedRow, setExpandedRow] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Загрузка данных с бэкенда
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get("/trust.json"); // Укажите ваш endpoint
setSystemData(response.data);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
fetchData();
}, []);
// Обработчик для кнопки "Подробнее"
const handleDetailsClick = (id) => {
setExpandedRow(expandedRow === id ? null : id);
};
if (loading) {
return <p>Загрузка данных...</p>;
}
if (error) {
return <p>Ошибка: {error}</p>;
}
return (
<table>
<caption>
<h2>Состояние системы</h2>
</caption>
<thead>
<tr>
<th>Метрика</th>
<th>Значение</th>
<th>Статус</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
{systemData.map((item) => (
<React.Fragment key={item.id}>
<tr>
<td>{item.name}</td>
<td>{item.value}%</td>
<td>
<span className={`status ${item.status}`}>{item.status}</span>
</td>
<td>
<button onClick={() => handleDetailsClick(item.id)}>
{expandedRow === item.id ? "Скрыть" : "Подробнее"}
</button>
</td>
</tr>
{expandedRow === item.id && (
<tr>
<td colSpan="4">
<div className="details">
<p>{item.details}</p>
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
);
};
export default SystemStatusTable;

View File

@ -1,84 +0,0 @@
import React, { useState, useEffect } from "react";
import "../Style/SystemStatusTable.css";
import axios from "axios";
const SystemStatusTableSoftware = () => {
const [systemData, setSystemData] = useState([]);
const [expandedRow, setExpandedRow] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Загрузка данных с бэкенда
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get("/TrustSoftware.json"); // Укажите ваш endpoint
setSystemData(response.data);
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
fetchData();
}, []);
// Обработчик для кнопки "Подробнее"
const handleDetailsClick = (id) => {
setExpandedRow(expandedRow === id ? null : id);
};
if (loading) {
return <p>Загрузка данных...</p>;
}
if (error) {
return <p>Ошибка: {error}</p>;
}
return (
<table>
<caption>
<h2>Состояние ПО</h2>
</caption>
<thead>
<tr>
<th>Метрика</th>
<th>Значение</th>
<th>Статус</th>
<th>Детали</th>
</tr>
</thead>
<tbody>
{systemData.map((item) => (
<React.Fragment key={item.id}>
<tr>
<td>{item.name}</td>
<td>{item.value}%</td>
<td>
<span className={`status ${item.status}`}>{item.status}</span>
</td>
<td>
<button onClick={() => handleDetailsClick(item.id)}>
{expandedRow === item.id ? "Скрыть" : "Подробнее"}
</button>
</td>
</tr>
{expandedRow === item.id && (
<tr>
<td colSpan="4">
<div className="details">
<p>{item.details}</p>
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
);
};
export default SystemStatusTableSoftware;

View File

@ -0,0 +1,115 @@
// src/services/WebSocketManager.js
import { io } from 'socket.io-client';
class WebSocketManager {
constructor() {
this.socket = null;
this.subscribers = new Map();
this.connectionStatus = 'disconnected';
this.connectionCallbacks = new Set();
}
connect() {
if (this.socket && (this.socket.connected || this.socket.reconnecting)) {
return this.socket;
}
this.socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
this.socket.on('connect', () => {
this.connectionStatus = 'connected';
this.notifyConnectionStatus();
});
this.socket.on('disconnect', (reason) => {
this.connectionStatus = 'disconnected';
this.notifyConnectionStatus();
if (reason === 'io server disconnect') this.socket.connect();
});
this.socket.on('connect_error', (error) => {
this.connectionStatus = 'error';
this.notifyConnectionStatus();
setTimeout(() => this.socket.connect(), 1000);
});
this.socket.on('metrics-data', (response) => {
const callbacks = this.subscribers.get(response.metric);
if (callbacks) {
callbacks.forEach(callback => callback(response.data));
}
});
return this.socket;
}
subscribe(metricName, callback) {
this.connect();
if (!this.subscribers.has(metricName)) {
this.subscribers.set(metricName, new Set());
this.socket.emit('subscribe-metric', {
metric: metricName,
isSubscription: true // Флаг для подписки
});
}
this.subscribers.get(metricName).add(callback);
return () => this.unsubscribe(metricName, callback);
}
unsubscribe(metricName, callback) {
const callbacks = this.subscribers.get(metricName);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
this.subscribers.delete(metricName);
this.socket.emit('unsubscribe-metric', { metric: metricName });
}
}
}
getMetricsRange(metricName, start, end, step) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Timeout while waiting for metrics data'));
}, 10000);
// Временный обработчик для разового запроса
const tempHandler = (data) => {
clearTimeout(timer);
this.socket.off(`metrics-range-${metricName}`, tempHandler);
resolve(data);
};
this.socket.on(`metrics-range-${metricName}`, tempHandler);
this.socket.emit('get-metrics', {
metric: metricName,
start,
end,
step,
isRangeQuery: true // Флаг для разового запроса
});
});
}
onConnectionStatusChange(callback) {
this.connectionCallbacks.add(callback);
callback(this.connectionStatus);
return () => this.connectionCallbacks.delete(callback);
}
notifyConnectionStatus() {
this.connectionCallbacks.forEach(callback => callback(this.connectionStatus));
}
}
export const webSocketManager = new WebSocketManager();

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Box, styled } from "@mui/material";
import SidebarMenu from "./SidebarMenu"; import SidebarMenu from "./SidebarMenu";
import "../../Style/Dashboard.css";
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
import generateTabContent from "../TreeChart/tabContent"; import generateTabContent from "../TreeChart/tabContent";
import CustomTabs from "../UI/MUItabs"; import CustomTabs from "../UI/MUItabs";
@ -9,24 +9,70 @@ import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent"; import TabContent from "../hooks/TabContent";
import menuData from "../TreeChart/menuData.json"; import menuData from "../TreeChart/menuData.json";
// Создаем стилизованные компоненты
const DashboardContainer = styled(Box)(({ theme }) => ({
display: 'flex',
height: '100vh',
width: '100vw',
overflow: 'hidden',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
const SidebarWrapper = styled(Box)(({ theme }) => ({
position: 'relative',
backgroundColor: theme.palette.custom.sidebar,
color: theme.palette.custom.sidebarText,
}));
const SidebarResizer = styled(Box)(({ theme }) => ({
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: '4px',
cursor: 'col-resize',
'&:hover': {
backgroundColor: theme.palette.primary.main,
},
}));
const MainContent = styled(Box)(({ theme }) => ({
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(2.5), // 20px
overflow: 'auto',
backgroundColor: theme.palette.background.default,
}));
const Content = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.custom.modalBackground,
//padding: theme.spacing(2.5),
borderRadius: '10px',
//boxShadow: theme.shadows[2],
maxWidth: '100%',
overflow: 'auto',
color: theme.palette.custom.modalText,
}));
const Dashboard = () => { const Dashboard = () => {
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная"); const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
const { sidebarWidth, startResizing } = useSidebarResize(250); const { sidebarWidth, startResizing } = useSidebarResize(290);
const [tabContent, setTabContent] = useState({}); const [tabContent, setTabContent] = useState({});
const [treeData1, setTreeData1] = useState(menuData); const [treeData1, setTreeData1] = useState(menuData);
const [treeData2, setTreeData2] = useState(menuData); const [treeData2, setTreeData2] = useState(menuData);
const [collapsed, setCollapsed] = useState(false);
const [statusHistories, setStatusHistories] = useState({ const [statusHistories, setStatusHistories] = useState({
history1: [], history1: [],
history2: [], history2: [],
}); });
// Генерация контента для вкладок
useEffect(() => { useEffect(() => {
const generatedTabContent = generateTabContent(menuData); const generatedTabContent = generateTabContent(menuData);
setTabContent(generatedTabContent); setTabContent(generatedTabContent);
}, []); }, []);
// Обновление статусов каждые 30 секунд
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
const updatedData1 = JSON.parse(JSON.stringify(treeData1)); const updatedData1 = JSON.parse(JSON.stringify(treeData1));
@ -56,15 +102,22 @@ const Dashboard = () => {
}, [treeData1, treeData2]); }, [treeData1, treeData2]);
return ( return (
<div className="dashboard-container"> <DashboardContainer>
{/* Сайдбар */} {/* Сайдбар */}
<div className="sidebar" style={{ width: sidebarWidth }}> <SidebarWrapper sx={{ width: collapsed ? 64 : sidebarWidth }}>
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} /> <SidebarMenu
<div className="sidebar-resizer" onMouseDown={startResizing} /> data={treeData1}
</div> onOpenTab={handleOpenTab}
sidebarWidth={sidebarWidth}
startResizing={startResizing}
collapsed={collapsed}
setCollapsed={setCollapsed}
/>
<SidebarResizer onMouseDown={startResizing} />
</SidebarWrapper>
{/* Основной контент */} {/* Основной контент */}
<div className="main-content"> <MainContent>
{/* Вкладки */} {/* Вкладки */}
<CustomTabs <CustomTabs
tabs={tabs} tabs={tabs}
@ -74,7 +127,7 @@ const Dashboard = () => {
/> />
{/* Контент вкладки */} {/* Контент вкладки */}
<div className="content"> <Content>
<TabContent <TabContent
activeTab={activeTab} activeTab={activeTab}
statusHistories={statusHistories} statusHistories={statusHistories}
@ -82,9 +135,9 @@ const Dashboard = () => {
tabContent={tabContent} tabContent={tabContent}
handleOpenTab={handleOpenTab} handleOpenTab={handleOpenTab}
/> />
</div> </Content>
</div> </MainContent>
</div> </DashboardContainer>
); );
}; };

View File

@ -1,49 +1,160 @@
import React from "react"; import React, { useState, useEffect } from "react";
import { Drawer, List } from "@mui/material"; import {
Drawer,
List,
Typography,
styled,
IconButton,
Tooltip,
Box
} from "@mui/material";
import {
ChevronLeft,
ChevronRight,
Menu as MenuIcon
} from "@mui/icons-material";
import MenuItem from "./SidebarMenuComponents/MenuItem"; import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter"; import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
import { statusManager1 } from "../TreeChart/dataUtils";
const SidebarResizer = styled('div')(({ theme }) => ({
width: "5px",
cursor: "ew-resize",
backgroundColor: 'transparent',
height: "100%",
position: "absolute",
right: 0,
top: 0,
transition: 'background-color 0.2s',
'&:hover': {
backgroundColor: theme.palette.primary.main,
},
zIndex: 2
}));
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing, collapsed, setCollapsed }) => {
const [hovered, setHovered] = useState(false);
const [menuData, setMenuData] = useState(data);
// Обновляем статусы при изменении данных
useEffect(() => {
if (data) {
// Создаем глубокую копию данных, чтобы не мутировать исходные
const dataCopy = JSON.parse(JSON.stringify(data));
statusManager1.updateStatuses(dataCopy);
setMenuData(dataCopy);
}
}, [data]);
const handleToggleCollapse = () => {
setCollapsed(!collapsed);
};
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
const handleSelectItem = (id, title, children) => { const handleSelectItem = (id, title, children) => {
onOpenTab(id, title, children); onOpenTab(id, title, children);
}; };
const drawerWidth = collapsed ? 64 : sidebarWidth;
return ( return (
<Drawer <Box
variant="permanent" onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
sx={{ sx={{
width: sidebarWidth, position: 'relative',
flexShrink: 0, width: drawerWidth,
"& .MuiDrawer-paper": { transition: 'width 0.3s ease',
width: sidebarWidth,
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
},
}} }}
> >
<List> <Drawer
<h2 style={{ padding: "16px", fontWeight: "bold" }}>Меню</h2> variant="permanent"
<MenuItem item={data} onSelectItem={handleSelectItem} /> sx={{
</List> width: drawerWidth,
flexShrink: 0,
{/* Ресайзер */} '& .MuiDrawer-paper': {
<div width: drawerWidth,
onMouseDown={startResizing} boxSizing: "border-box",
style={{ display: "flex",
width: "5px", flexDirection: "column",
cursor: "ew-resize", backgroundColor: 'custom.sidebar',
backgroundColor: "#ccc", color: 'custom.sidebarText',
height: "100%", transition: 'width 0.3s ease',
position: "absolute", overflowX: 'hidden',
right: 0, borderRight: 'none'
top: 0, },
}} }}
/> >
{/* Кнопка сворачивания/разворачивания */}
<Box sx={{
display: 'flex',
justifyContent: 'flex-end',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
backgroundColor: 'custom.sidebar'
}}>
<Tooltip title={collapsed ? "Развернуть меню" : "Свернуть меню"}>
<IconButton
onClick={handleToggleCollapse}
size="small"
sx={{
color: 'custom.sidebarText',
'&:hover': {
backgroundColor: 'custom.sidebarHover',
}
}}
>
{collapsed ? <ChevronRight /> : <ChevronLeft />}
</IconButton>
</Tooltip>
</Box>
<SidebarFooter sidebarWidth={sidebarWidth} /> {/* Содержимое меню */}
</Drawer> <Box sx={{
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}>
<List sx={{
overflowY: 'auto',
overflowX: 'hidden',
flex: '1 1 auto'
}}>
{!collapsed && (
<Typography
variant="h6"
sx={{
p: 2,
fontWeight: 'bold',
textAlign: 'center'
}}
>
Меню
</Typography>
)}
{menuData && (
<MenuItem
item={menuData}
onSelectItem={handleSelectItem}
collapsed={collapsed}
/>
)}
</List>
{/* Футер */}
{!collapsed && (
<SidebarFooter />
)}
</Box>
{/* Ресайзер */}
{!collapsed && (
<SidebarResizer onMouseDown={startResizing} />
)}
</Drawer>
</Box>
); );
}; };
export default SidebarMenu; export default SidebarMenu;

View File

@ -1,87 +1,108 @@
import React from "react"; import React from "react";
import { Drawer, List, ListItem, ListItemIcon, ListItemText, Collapse } from "@mui/material"; import {
ListItem,
ListItemIcon,
ListItemText,
Collapse,
List,
styled
} from "@mui/material";
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material"; import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
import { getStatusColor } from "../../TreeChart/dataUtils";
// Функция для сбора всех потомков
const getAllChildren = (node) => {
let children = [];
if (node.items && node.items.length > 0) {
node.items.forEach((child) => {
children.push(child); // Добавляем текущий элемент
children = children.concat(getAllChildren(child)); // Рекурсивно добавляем потомков
});
}
return children;
};
const MenuItem = ({ item, onSelectItem }) => {
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
cursor: "pointer",
paddingLeft: theme.spacing(2 + level * 2),
position: 'relative', // Добавляем для позиционирования индикатора
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
'&.Mui-selected': {
backgroundColor: theme.palette.custom.sidebarHover,
},
}));
const StatusIndicator = styled('div')(({ theme, status }) => ({
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: '4px',
backgroundColor: status ? getStatusColor(status) : 'transparent',
borderTopRightRadius: '4px',
borderBottomRightRadius: '4px',
}));
const IconWrapper = styled('div')(({ theme }) => ({
cursor: "pointer",
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(0.5),
'&:hover': {
backgroundColor: theme.palette.action.selected,
},
}));
const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const hasChildren = Array.isArray(item.items) && item.items.length > 0; const hasChildren = Array.isArray(item.items) && item.items.length > 0;
const handleToggle = () => { const handleToggle = (e) => {
e.stopPropagation();
setIsOpen(!isOpen); setIsOpen(!isOpen);
}; };
const handleOpenTab = (e) => { const handleOpenTab = (e) => {
e.stopPropagation(); // Останавливаем всплытие события e.stopPropagation();
const allChildren = getAllChildren(item); // Собираем всех потомков const allChildren = getAllChildren(item);
onSelectItem(item.id, item.title, allChildren); // Передаем данные в родительский компонент onSelectItem(item.id, item.title, allChildren);
}; };
return ( return (
<> <>
<ListItem <StyledListItem
component="div" component="div"
onClick={hasChildren ? handleToggle : handleOpenTab} onClick={hasChildren ? handleToggle : handleOpenTab}
level={level}
sx={{ sx={{
cursor: "pointer", // Курсор pointer везде pl: collapsed ? 2 : 2 + level * 2,
"&:hover": { justifyContent: collapsed ? 'center' : 'flex-start',
backgroundColor: "#f5f5f5", // Подсветка при наведении на весь элемент
},
}} }}
> >
<ListItemIcon> {/* Индикатор статуса */}
{hasChildren ? ( {!collapsed && <StatusIndicator status={item.status} />}
<div
onClick={handleOpenTab} <ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
style={{ <IconWrapper onClick={handleOpenTab}>
cursor: "pointer", {hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
borderRadius: "4px", // Скругление углов </IconWrapper>
padding: "4px", // Отступы для увеличения области hover
"&:hover": {
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
},
}}
>
{isOpen ? <FolderOpen /> : <Folder />}
</div>
) : (
<div
onClick={handleOpenTab}
style={{
cursor: "pointer",
borderRadius: "4px", // Скругление углов
padding: "4px", // Отступы для увеличения области hover
"&:hover": {
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
},
}}
>
{/* Здесь можно добавить другую иконку или оставить пустым */}
</div>
)}
</ListItemIcon> </ListItemIcon>
<ListItemText
primary={item.title} {!collapsed && (
sx={{ cursor: "pointer" }} // Курсор pointer для текста <>
/> <ListItemText
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)} primary={item.title}
</ListItem> primaryTypographyProps={{
{hasChildren && ( color: 'custom.sidebarText'
}}
/>
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
</>
)}
</StyledListItem>
{hasChildren && !collapsed && (
<Collapse in={isOpen} timeout="auto" unmountOnExit> <Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding> <List component="div" disablePadding>
{item.items.map((child, index) => ( {item.items.map((child, index) => (
<MenuItem key={index} item={child} onSelectItem={onSelectItem} /> <MenuItem
key={index}
item={child}
onSelectItem={onSelectItem}
level={level + 1}
collapsed={collapsed}
/>
))} ))}
</List> </List>
</Collapse> </Collapse>
@ -90,4 +111,15 @@ const MenuItem = ({ item, onSelectItem }) => {
); );
}; };
const getAllChildren = (node) => {
let children = [];
if (node.items && node.items.length > 0) {
node.items.forEach((child) => {
children.push(child);
children = children.concat(getAllChildren(child));
});
}
return children;
};
export default MenuItem; export default MenuItem;

View File

@ -1,16 +1,47 @@
import React from "react"; import React from "react";
import { List, ListItem, ListItemText } from "@mui/material"; import {
List,
ListItem,
ListItemText,
styled
} from "@mui/material";
const SidebarFooter = ({ sidebarWidth }) => { const FooterList = styled(List)(({ theme }) => ({
backgroundColor: theme.palette.custom.sidebar,
padding: theme.spacing(1, 0),
borderTop: `1px solid ${theme.palette.divider}`,
marginTop: 'auto'
}));
const FooterListItem = styled(ListItem)(({ theme }) => ({
'&:hover': {
backgroundColor: theme.palette.custom.sidebarHover,
},
padding: theme.spacing(1, 2),
}));
const SidebarFooter = () => {
return ( return (
<List sx={{ marginTop: "auto", backgroundColor: "#ffffff", padding: "10px 0" }}> <FooterList>
<ListItem button={true}> <FooterListItem button>
<ListItemText primary="Помощь" sx={{ color: "#000000" }} /> <ListItemText
</ListItem> primary="Помощь"
<ListItem button={true}> primaryTypographyProps={{
<ListItemText primary="Настройка" sx={{ color: "#000000" }} /> color: 'custom.sidebarText',
</ListItem> variant: 'body2'
</List> }}
/>
</FooterListItem>
<FooterListItem button>
<ListItemText
primary="Настройка"
primaryTypographyProps={{
color: 'custom.sidebarText',
variant: 'body2'
}}
/>
</FooterListItem>
</FooterList>
); );
}; };

View File

@ -0,0 +1,25 @@
import React from "react";
import { styled } from "@mui/material";
const StatusIndicator = styled('div')(({ theme, status }) => ({
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: '4px',
backgroundColor: getStatusColor(status),
borderRadius: '0 2px 2px 0',
transition: 'background-color 0.3s ease'
}));
const getStatusColor = (status) => {
switch (status) {
case 'red': return '#F44336';
case 'orange': return '#FF9800';
case 'yellow': return '#cebd21';
case 'green': return '#4CAF50';
default: return 'transparent';
}
};
export default StatusIndicator;

View File

@ -0,0 +1,101 @@
import React, { useEffect, useMemo, useRef } from 'react';
import ReactFlow, { Controls, Background } from 'reactflow';
import 'reactflow/dist/style.css';
import { debounce } from 'lodash';
import { useFlowChart } from './FlowChartComponents/useFlowChart';
import { useNodeHandlers } from './FlowChartComponents/useNodeHandlers';
import { useDataParser } from './FlowChartComponents/DataParser';
import NodeWrapper from './FlowChartComponents/NodeWrapper';
const nodeTypes = {
customNode: NodeWrapper
};
const FlowChart = ({ data }) => {
const {
nodes,
edges,
nodePositions,
setNodes,
setEdges,
onNodesChange,
onEdgesChange,
setNodePositions,
collapsedNodes,
toggleNodeCollapse
} = useFlowChart(data);
const { parseData } = useDataParser(nodePositions, collapsedNodes);
const initialized = useRef(false);
const debouncedSetNodePositions = useMemo(
() => debounce(setNodePositions, 100),
[setNodePositions]
);
const { onNodeDrag, onNodeDragStop } = useNodeHandlers(debouncedSetNodePositions);
useEffect(() => {
const { nodes: initialNodes, edges: initialEdges } = parseData(data);
setNodes(initialNodes);
setEdges(initialEdges);
// Автоматически сворачиваем узлы, которые являются родителями последнего уровня
if (!initialized.current && data) {
const findAndCollapseLastLevelParents = (items) => {
items.forEach(item => {
if (item.items && item.items.length > 0) {
// Проверяем, есть ли у детей свои дети
const hasGrandchildren = item.items.some(child =>
child.items && child.items.length > 0
);
// Если у детей нет своих детей - это родители последнего уровня
if (!hasGrandchildren) {
toggleNodeCollapse(item.id);
} else {
findAndCollapseLastLevelParents(item.items);
}
}
});
};
findAndCollapseLastLevelParents(data.items || []);
initialized.current = true;
}
}, [data, parseData, setNodes, setEdges, toggleNodeCollapse]);
const onNodeClick = (event, node) => {
if (node.data.hasChildren) {
toggleNodeCollapse(node.id);
}
};
useEffect(() => {
return () => {
debouncedSetNodePositions.cancel();
};
}, [debouncedSetNodePositions]);
return (
<div style={{ height: '85vh', width: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
nodeDragThreshold={1}
onNodeClick={onNodeClick}
fitView
>
<Background />
<Controls />
</ReactFlow>
</div>
);
};
export default React.memo(FlowChart);

View File

@ -0,0 +1,112 @@
import { useCallback } from 'react';
import { isLeafNode } from './nodeUtils';
import { getStatusColor } from '../dataUtils';
export const useDataParser = (nodePositions, collapsedNodes) => {
const getNodeStyle = useCallback((item, isLeaf) => ({
width: isLeaf ? 60 : 70,
height: isLeaf ? 60 : 70,
borderRadius: '50%',
backgroundColor: getStatusColor(item.status),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'black',
border: '2px solid #fff',
fontSize: isLeaf ? '0.8rem' : '1rem'
}), []);
const getCenterNodeStyle = useCallback((item) => ({
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: getStatusColor(item.status),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'black',
border: '2px solid #fff',
fontSize: '1.2rem'
}), []);
const parseData = useCallback((data) => {
if (!data) return { nodes: [], edges: [] };
const nodes = [];
const edges = [];
const centerX = 500;
const centerY = 400;
const baseLevelRadius = 150;
const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => {
if (!item || collapsedNodes[parentId]) return; // Пропускаем свёрнутые узлы
const nodeId = item.id;
const items = item.items || [];
const isLeaf = isLeafNode(item);
const savedPosition = nodePositions[nodeId];
let position = savedPosition || {
x: Math.round(centerX + Math.cos((angleStart + angleEnd) / 2) * (parentRadius + baseLevelRadius)),
y: Math.round(centerY + Math.sin((angleStart + angleEnd) / 2) * (parentRadius + baseLevelRadius))
};
const node = {
id: nodeId,
type: 'customNode',
position,
data: {
...item,
label: item.title,
style: getNodeStyle(item, isLeaf), // Переносим стили в data
hasChildren: items.length > 0,
collapsed: collapsedNodes[nodeId]
}
};
nodes.push(node);
if (parentId) {
edges.push({
id: `${parentId}-${nodeId}`,
source: parentId,
target: nodeId,
style: { stroke: isLeaf ? '#aaa' : '#666', strokeWidth: isLeaf ? 1 : 2 }
});
}
if (!collapsedNodes[nodeId] && items.length > 0) {
const spreadAngle = angleEnd - angleStart;
items.forEach((child, index) => {
if (!child) return;
const itemAngleStart = angleStart + (index / items.length) * spreadAngle;
const itemAngleEnd = angleStart + ((index + 1) / items.length) * spreadAngle;
traverse(child, nodeId, level + 1, itemAngleStart, itemAngleEnd, parentRadius + baseLevelRadius);
});
}
};
const centerNode = {
id: data.id,
type: 'customNode', // Добавляем тип узла
position: nodePositions[data.id] || { x: centerX, y: centerY },
style: getCenterNodeStyle(data),
data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] }
};
nodes.push(centerNode);
if (!collapsedNodes[data.id] && data.items.length > 0) {
const angleStep = (2 * Math.PI) / data.items.length;
data.items.forEach((child, index) => {
if (!child) return;
traverse(child, data.id, 1, index * angleStep, (index + 1) * angleStep, 0);
});
}
return { nodes, edges };
}, [nodePositions, collapsedNodes, getNodeStyle, getCenterNodeStyle]);
return { parseData };
};

View File

@ -0,0 +1,63 @@
import React, { memo } from 'react';
import { Handle } from 'reactflow';
const NodeWrapper = memo(({ id, data, selected }) => {
return (
<div
style={{
...data.style,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden', // Чтобы текст не выходил за границы
textOverflow: 'ellipsis', // Добавляем многоточие если текст не помещается
whiteSpace: 'nowrap', // Запрещаем перенос строк
padding: '0 8px', // Горизонтальный padding для текста
boxSizing: 'border-box' // Учитываем padding в общей ширине
}}
title={data.label} // Простой tooltip при наведении
>
{/* Хендл для входящих соединений */}
<Handle
type="target"
position="top"
style={{ visibility: 'hidden' }}
/>
{/* Обёртка для текста с ограничением ширины */}
<div style={{
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{data.label}
</div>
{data.hasChildren && (
<span style={{
position: 'absolute',
top: 5,
right: 5,
fontSize: '12px',
cursor: 'pointer',
background: '#fff',
padding: '2px 5px',
borderRadius: '3px',
border: '1px solid #aaa'
}}>
{data.collapsed ? '+' : '-'}
</span>
)}
{/* Хендл для исходящих соединений */}
<Handle
type="source"
position="bottom"
style={{ visibility: 'hidden' }}
/>
</div>
);
});
export default NodeWrapper;

View File

@ -0,0 +1,3 @@
export const isLeafNode = (item) => {
return !item.items || item.items.length === 0;
};

View File

@ -0,0 +1,46 @@
import { useState, useCallback, useEffect } from 'react';
import { useNodesState, useEdgesState } from 'reactflow';
import { statusManager1 } from '../dataUtils';
export const useFlowChart = (initialData) => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [nodePositions, setNodePositions] = useState({});
const [collapsedNodes, setCollapsedNodes] = useState({}); // Добавили
const toggleNodeCollapse = useCallback((nodeId) => {
setCollapsedNodes((prev) => ({
...prev,
[nodeId]: !prev[nodeId]
}));
}, []);
const initializeNodePositions = useCallback((nodes) => {
const positions = {};
nodes.forEach(node => {
positions[node.id] = node.position;
});
setNodePositions(positions);
}, []);
useEffect(() => {
const updateStatuses = (data) => {
statusManager1.updateStatuses(data);
};
updateStatuses(initialData);
}, [initialData]);
return {
nodes,
edges,
nodePositions,
setNodes,
setEdges,
onNodesChange,
onEdgesChange,
setNodePositions,
collapsedNodes,
toggleNodeCollapse,
initializeNodePositions
};
};

View File

@ -0,0 +1,24 @@
import { useCallback } from 'react';
export const useNodeHandlers = (debouncedSetNodePositions) => {
const onNodeDrag = useCallback((event, node) => {
// Фиксируем позицию сразу при перемещении
node.position = {
x: Math.round(node.position.x),
y: Math.round(node.position.y)
};
}, []);
const onNodeDragStop = useCallback((event, node) => {
node.position = {
x: Math.round(node.position.x),
y: Math.round(node.position.y)
};
debouncedSetNodePositions(prev => ({
...prev,
[node.id]: node.position
}));
}, [debouncedSetNodePositions]);
return { onNodeDrag, onNodeDragStop };
};

View File

@ -1,189 +0,0 @@
import React, { useRef, useEffect, useMemo } from "react";
import * as d3 from "d3";
import "../../Style/TreeChart.css";
import { getStatusColor } from "./dataUtils";
const TreeChart = ({ data, onNodeClick }) => {
const chartRef = useRef();
const nodePositions = useRef(new Map());
const { root, nodes, links } = useMemo(() => {
if (!data || !data.items) return { root: null, nodes: [], links: [] };
const root = d3.hierarchy(data, (d) => d.items);
const maxDepth = d3.max(root.descendants(), (d) => d.depth);
// Фильтруем узлы, исключая последний уровень
const nodes = root.descendants().filter((d) => d.depth < maxDepth);
// Фильтруем связи
const links = nodes.filter((d) => d.parent).map((d) => ({
source: d.parent,
target: d,
}));
// Размещаем узлы иерархически
const center = { x: 0, y: 0 }; // Центральная точка
const baseRadius = 150; // Базовый радиус для 1-го уровня
const branchOffset = 80; // Смещение узлов вдоль ветки
const angleOffset = Math.PI / 4; // Угол смещения для дочерних ветвей
const spreadFactor = 1.5; // Коэффициент растяжения для последних узлов
nodes.forEach((node) => {
const prev = nodePositions.current.get(node.data.id);
if (prev) {
node.x = prev.x;
node.y = prev.y;
} else {
if (node.depth === 0) {
// Центральный узел
node.x = center.x;
node.y = center.y;
} else if (node.depth === 1) {
// Первый уровень - равномерно по окружности
const parent = node.parent;
const index = parent.children.indexOf(node);
const totalSiblings = parent.children.length;
const radius = baseRadius * node.depth;
const sectorAngle = (Math.PI * 2) / totalSiblings;
const angle = index * sectorAngle;
node.x = parent.x + radius * Math.cos(angle);
node.y = parent.y + radius * Math.sin(angle);
node.angle = angle; // Запоминаем угол для веток
} else {
// Второй уровень и дальше - ветка растет в направлении родителя
const parent = node.parent;
const siblings = parent.children || [];
const index = siblings.indexOf(node);
const totalSiblings = siblings.length;
const direction = parent.angle || 0;
const offsetAngle = ((index - (totalSiblings - 1) / 2) * angleOffset) / totalSiblings;
let distance = branchOffset;
if (!node.children || node.children.length === 0) {
// Если это последний узел, увеличиваем расстояние
distance *= spreadFactor + node.depth * 0.2; // Чем глубже, тем больше разброс
}
node.x = parent.x + distance * Math.cos(direction + offsetAngle);
node.y = parent.y + distance * Math.sin(direction + offsetAngle);
node.angle = direction + offsetAngle;
}
}
nodePositions.current.set(node.data.id, { x: node.x, y: node.y });
});
return { root, nodes, links };
}, [data]);
useEffect(() => {
if (!chartRef.current) return;
const svg = d3.select(chartRef.current)
.attr("width", 2000)
.attr("height", 2000)
.attr("viewBox", [-500, -500, 1500, 1500])
.attr("style", "max-width: 100%; height: auto;");
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
svg.append("g").attr("class", "labels");
// Очищаем предыдущие элементы
svg.selectAll(".links line").remove();
svg.selectAll(".nodes circle").remove();
svg.selectAll(".labels text").remove();
// Рисуем связи
const linkGroup = svg.select(".links");
const link = linkGroup
.selectAll("line")
.data(links, (d) => `${d.source.data.id}-${d.target.data.id}`)
.join("line")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y);
// Рисуем узлы
const nodeGroup = svg.select(".nodes");
const node = nodeGroup
.selectAll("circle")
.data(nodes, (d) => d.data.id)
.join("circle")
.attr("fill", (d) => getStatusColor(d.data.status))
.attr("stroke", "#fff")
.attr("r", 7)
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.call(drag());
node.on("click", (event, d) => {
if (onNodeClick) {
onNodeClick(d.data.id, d.data.title);
}
});
// Рисуем текстовые метки
const labelGroup = svg.select(".labels");
const text = labelGroup
.selectAll("text")
.data(nodes, (d) => d.data.id)
.join("text")
.text((d) => (nodes.length > 50 ? "" : d.data.title)) // Скрываем текст, если узлов много
.attr("dx", 12)
.attr("dy", 4)
.style("user-select", "none") // Запрет выделения текста
.style("pointer-events", "none") // Запрет взаимодействия с текстом
.style("fill", "var(--TreeChart-text-color)") // Используем переменную для цвета текста
.attr("x", (d) => d.x + 12)
.attr("y", (d) => d.y + 4);
}, [root, links, nodes, onNodeClick]);
const drag = () => {
function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "#000");
}
function dragged(event, d) {
d.x = event.x;
d.y = event.y;
d3.select(this).attr("cx", d.x).attr("cy", d.y);
// Обновляем текстовую метку
d3.select(this.parentNode)
.select("text")
.attr("x", d.x + 12)
.attr("y", d.y + 4);
// Обновляем связи
d3.select(chartRef.current)
.selectAll(".links line")
.filter((link) => link.source === d || link.target === d)
.attr("x1", (link) => link.source.x)
.attr("y1", (link) => link.source.y)
.attr("x2", (link) => link.target.x)
.attr("y2", (link) => link.target.y);
}
function dragended(event, d) {
d3.select(this).attr("stroke", "#fff");
nodePositions.current.set(d.data.id, { x: d.x, y: d.y });
}
return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
};
return <svg ref={chartRef} />;
};
export default TreeChart;

File diff suppressed because it is too large Load Diff

View File

@ -1,714 +0,0 @@
{
"title": "Сервис ЗВКС",
"id": "1",
"items": [
{
"title": "Функциональные задачи",
"id": "functional_tasks",
"items": [
{
"id": "system_control",
"title": "Контроль системы"
},
{
"id": "system_management",
"title": "Система управления"
},
{
"id": "conference",
"title": "Проведение ВКС"
},
{
"id": "backup",
"title": "Резервное копирование"
},
{
"id": "relay_info",
"title": "Ретрансляция информации"
}
]
},
{
"id": "18",
"title": "Graviton S2082I (device$18)",
"items": [
{
"id": "4",
"title": "OS Linux (module$4) АО",
"items": [
{
"id": "190",
"title": "Загрузка процессора за 1 минуту"
},
{
"id": "191",
"title": "Загрузка процессора за 5 минут"
},
{
"id": "192",
"title": "Загрузка процессора за 15 минут"
},
{
"id": "197",
"title": "Общий объем SWAP-файла"
},
{
"id": "198",
"title": "Используемый объем SWAP-файла"
},
{
"id": "199",
"title": "Общий объем физической оперативной памяти"
},
{
"id": "200",
"title": "Доступный объем физической оперативной памяти"
},
{
"id": "201",
"title": "Свободный объем физической и виртуальной оперативной памяти"
},
{
"id": "202",
"title": "Буферизованный объем оперативной памяти"
},
{
"id": "203",
"title": "Кэшированый объем оперативной памяти"
},
{
"id": "274",
"title": "Используемый объем SWAP-файла"
},
{
"id": "275",
"title": "Время затраченное процессором на процессы с пониженным приоритетом"
},
{
"id": "276",
"title": "Время затраченное процессором на процессы ядра ОС"
},
{
"id": "277",
"title": "Время простоя процессора"
},
{
"id": "278",
"title": "Общая емкость жестких дисков"
},
{
"id": "279",
"title": "Доступная емкость жестких дисков"
}
]
},
{
"id": "5",
"title": "Vinteo (module$5) ПО",
"items": [
{
"id": "31",
"title": "Общее количество участников"
},
{
"id": "32",
"title": "Ожидание соединения"
},
{
"id": "33",
"title": "Зарегистрированные абоненты"
},
{
"id": "34",
"title": "Количество пользоватей HLS"
},
{
"id": "35",
"title": "Общее количество P2P комнат"
},
{
"id": "36",
"title": "Общее количество конференций"
},
{
"id": "37",
"title": "Общее количество активных конференций"
},
{
"id": "38",
"title": "Статус записи"
},
{
"id": "39",
"title": "Общее количество сохранённых записей"
}
]
},
{
"id": "280",
"title": "Сетевой адаптер №1 (port$261) Eth_1",
"items": [
{
"id": "207",
"title": "Скорость порта Eth_1"
},
{
"id": "209",
"title": "Административное состояние порта Eth_1"
},
{
"id": "210",
"title": "Оперативное состояние порта Eth_1"
},
{
"id": "211",
"title": "Общее количество отправленных октетов Eth_1"
},
{
"id": "212",
"title": "Количество входящих Multicast пакетов Eth_1"
},
{
"id": "213",
"title": "Количество иcходящих Multiicast пакетов Eth_1"
},
{
"id": "214",
"title": "Количество входящих Broadcast пакетов Eth_1"
},
{
"id": "215",
"title": "Количество иcходящих Broadcast пакетов Eth_1"
},
{
"id": "216",
"title": "Количество входящих Unicast пакетов Eth_1"
},
{
"id": "217",
"title": "Количество иcходящих Unicast пакетов Eth_1"
},
{
"id": "218",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_1"
},
{
"id": "219",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_1"
},
{
"id": "220",
"title": "Количество входящих пакетов с ошибкой Eth_1"
},
{
"id": "221",
"title": "Количество исходящих пакетов с ошибкой Eth_1"
},
{
"id": "222",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_1"
}
]
},
{
"id": "281",
"title": "Сетевой адаптер №2 (port$262) Eth_2",
"items": [
{
"id": "224",
"title": "Скорость порта Eth_2"
},
{
"id": "226",
"title": "Административное состояние порта Eth_2"
},
{
"id": "227",
"title": "Оперативное состояние порта Eth_2"
},
{
"id": "228",
"title": "Общее количество отправленных октетов Eth_2"
},
{
"id": "229",
"title": "Количество входящих Multicast пакетов Eth_2"
},
{
"id": "230",
"title": "Количество иcходящих Multiicast пакетов Eth_2"
},
{
"id": "231",
"title": "Количество входящих Broadcast пакетов Eth_2"
},
{
"id": "232",
"title": "Количество иcходящих Broadcast пакетов Eth_2"
},
{
"id": "233",
"title": "Количество входящих Unicast пакетов Eth_2"
},
{
"id": "234",
"title": "Количество иcходящих Unicast пакетов Eth_2"
},
{
"id": "235",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_2"
},
{
"id": "236",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_2"
},
{
"id": "237",
"title": "Количество входящих пакетов с ошибкой Eth_2"
},
{
"id": "238",
"title": "Количество исходящих пакетов с ошибкой Eth_2"
},
{
"id": "239",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_2"
}
]
},
{
"id": "282",
"title": "Сетевой адаптер №3 (port$263) Eth_3",
"items": [
{
"id": "241",
"title": "Скорость порта Eth_3"
},
{
"id": "243",
"title": "Административное состояние порта Eth_3"
},
{
"id": "244",
"title": "Оперативное состояние порта Eth_3"
},
{
"id": "245",
"title": "Общее количество отправленных октетов Eth_3"
},
{
"id": "246",
"title": "Количество входящих Multicast пакетов Eth_3"
},
{
"id": "247",
"title": "Количество иcходящих Multiicast пакетов Eth_3"
},
{
"id": "248",
"title": "Количество входящих Broadcast пакетов Eth_3"
},
{
"id": "249",
"title": "Количество иcходящих Broadcast пакетов Eth_3"
},
{
"id": "250",
"title": "Количество входящих Unicast пакетов Eth_3"
},
{
"id": "251",
"title": "Количество иcходящих Unicast пакетов Eth_3"
},
{
"id": "252",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_3"
},
{
"id": "253",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_3"
},
{
"id": "254",
"title": "Количество входящих пакетов с ошибкой Eth_3"
},
{
"id": "255",
"title": "Количество исходящих пакетов с ошибкой Eth_3"
},
{
"id": "256",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_3"
}
]
},
{
"id": "283",
"title": "Сетевой адаптер №4 (port$264) Eth_4",
"items": [
{
"id": "258",
"title": "Скорость порта Eth_4"
},
{
"id": "260",
"title": "Административное состояние порта Eth_4"
},
{
"id": "261",
"title": "Оперативное состояние порта Eth_4"
},
{
"id": "262",
"title": "Общее количество отправленных октетов Eth_4"
},
{
"id": "263",
"title": "Количество входящих Multicast пакетов Eth_4"
},
{
"id": "264",
"title": "Количество иcходящих Multiicast пакетов Eth_4"
},
{
"id": "265",
"title": "Количество входящих Broadcast пакетов Eth_4"
},
{
"id": "266",
"title": "Количество иcходящих Broadcast пакетов Eth_4"
},
{
"id": "267",
"title": "Количество входящих Unicast пакетов Eth_4"
},
{
"id": "268",
"title": "Количество иcходящих Unicast пакетов Eth_4"
},
{
"id": "269",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_4"
},
{
"id": "270",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_4"
},
{
"id": "271",
"title": "Количество входящих пакетов с ошибкой Eth_4"
},
{
"id": "272",
"title": "Количество исходящих пакетов с ошибкой Eth_4"
},
{
"id": "273",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_4"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_1",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_1",
"items": [
{
"id": "media_system_software_1_2",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_2",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_2",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_2",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_1",
"items": [
{
"id": "media_software_1_2",
"title": "ПО"
},
{
"id": "media_software_2_2",
"title": "ПО"
},
{
"id": "media_software_3_2",
"title": "ПО"
},
{
"id": "media_software_4_2",
"title": "ПО"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_2",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_2",
"items": [
{
"id": "media_system_software_1_3",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_3",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_3",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_3",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_2",
"items": [
{
"id": "media_software_1_3",
"title": "ПО"
},
{
"id": "media_software_2_3",
"title": "ПО"
},
{
"id": "media_software_3_3",
"title": "ПО"
},
{
"id": "media_software_4_3",
"title": "ПО"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_3",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_3",
"items": [
{
"id": "media_system_software_1_4",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_4",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_4",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_4",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_3",
"items": [
{
"id": "media_software_1_4",
"title": "ПО"
},
{
"id": "media_software_2_4",
"title": "ПО"
},
{
"id": "media_software_3_4",
"title": "ПО"
},
{
"id": "media_software_4_4",
"title": "ПО"
}
]
}
]
},
{
"title": "Медиа сервер",
"id": "media_server_4",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_4",
"items": [
{
"id": "media_system_software_1_5",
"title": "Центральный процессор"
},
{
"id": "media_system_software_2_5",
"title": "Оперативная память"
},
{
"id": "media_system_software_3_5",
"title": "Жесткий диск"
},
{
"id": "media_system_software_4_5",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_4",
"items": [
{
"id": "media_software_1_5",
"title": "ПО"
},
{
"id": "media_software_2_5",
"title": "ПО"
},
{
"id": "media_software_3_5",
"title": "ПО"
},
{
"id": "media_software_4_5",
"title": "ПО"
}
]
}
]
},
{
"title": "Сервер систем",
"id": "system_server_1",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_5",
"items": [
{
"id": "copy_system_software_1",
"title": "Центральный процессор"
},
{
"id": "copy_system_software_2",
"title": "Оперативная память"
},
{
"id": "copy_system_software_3",
"title": "Жесткий диск"
},
{
"id": "copy_system_software_4",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_5",
"items": [
{
"id": "copy_software_1",
"title": "ПО"
},
{
"id": "copy_software_2",
"title": "ПО"
},
{
"id": "copy_software_3",
"title": "ПО"
},
{
"id": "copy_software_4",
"title": "ПО"
}
]
}
]
},
{
"title": "Сервер систем",
"id": "system_server_2",
"items": [
{
"title": "Аппаратное обеспечение",
"id": "system_software_6",
"items": [
{
"id": "control_system_software_1",
"title": "Центральный процессор"
},
{
"id": "control_system_software_2",
"title": "Оперативная память"
},
{
"id": "control_system_software_3",
"title": "Жесткий диск"
},
{
"id": "control_system_software_4",
"title": "Сетевые адаптеры"
}
]
},
{
"title": "Программное обеспечение",
"id": "software_6",
"items": [
{
"id": "control_software_1",
"title": "ПО"
},
{
"id": "control_software_2",
"title": "ПО"
},
{
"id": "control_software_3",
"title": "ПО"
},
{
"id": "control_software_4",
"title": "ПО"
}
]
}
]
}
]
}

View File

@ -1,14 +1,15 @@
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense } from "react";
import Skeleton from '@mui/material/Skeleton';
import Box from '@mui/material/Box';
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart')); const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
// Функция для генерации названия метрики на основе id // Функция для генерации названия метрики на основе id
const getMetricName = (id) => { const getMetricName = (id) => {
return `zvks_apiforsnmp_measure_${id}`; return `zvks_apiforsnmp_measure_${id}`;
}; };
//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
// Функция для рекурсивного сбора всех id потомков // Функция для рекурсивного сбора всех id потомков
const getAllChildIds = (node) => { const getAllChildIds = (node) => {
let ids = []; let ids = [];
@ -23,9 +24,32 @@ const getAllChildIds = (node) => {
return ids; return ids;
}; };
// Компонент Skeleton для графика
const ChartSkeleton = () => (
<Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="60%" height={30} /> {/* Заголовок */}
<Skeleton variant="rectangular" width="100%" height={300} sx={{ mt: 2 }} /> {/* График */}
</Box>
);
// Компонент Skeleton для родительского контейнера
const ContainerSkeleton = () => (
<Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="40%" height={40} /> {/* Заголовок */}
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 1 }} /> {/* Описание */}
{/* Место для дочерних элементов */}
<Box sx={{ mt: 2 }}>
{[...Array(3)].map((_, i) => (
<ChartSkeleton key={i} />
))}
</Box>
</Box>
);
const tabContent = (data) => { const tabContent = (data) => {
const tabContent = {}; const tabContent = {};
// Функция для рекурсивного обхода и сбора данных
// Функция для рекурсивного обхода и сбора данных // Функция для рекурсивного обхода и сбора данных
const generateContent = (nodes) => { const generateContent = (nodes) => {
nodes.forEach((node) => { nodes.forEach((node) => {
@ -37,8 +61,11 @@ const tabContent = (data) => {
const content = ( const content = (
<div> <div>
<h2>{node.title}</h2> <h2>{node.title}</h2>
<Suspense fallback={<ContainerSkeleton />}>
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
</Suspense>
<p>Контент для {node.title}.</p> <p>Контент для {node.title}.</p>
{childrenContent} {/*childrenContent*/}
</div> </div>
); );
@ -53,7 +80,7 @@ const tabContent = (data) => {
const content = ( const content = (
<div key={node.id}> <div key={node.id}>
<h3>{node.title}</h3> {/* Используем title узла */} <h3>{node.title}</h3> {/* Используем title узла */}
<Suspense fallback={<div>Загрузка графика...</div>}> <Suspense fallback={<ChartSkeleton />}>
<PrometheusChart metricName={metricName} /> <PrometheusChart metricName={metricName} />
</Suspense> </Suspense>
</div> </div>

View File

@ -0,0 +1,34 @@
import React from 'react';
import Button from '@mui/material/Button';
import { styled } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
const StyledButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(1),
// Дополнительные стили
}));
const CustomButton = ({
children,
variant = 'contained',
color = 'primary',
loading = false,
startIcon,
endIcon,
...props
}) => {
return (
<StyledButton
variant={variant}
color={color}
startIcon={startIcon && !loading ? startIcon : undefined}
endIcon={endIcon && !loading ? endIcon : undefined}
disabled={loading}
{...props}
>
{loading ? <CircularProgress size={24} /> : children}
</StyledButton>
);
};
export default CustomButton;

View File

@ -1,24 +0,0 @@
import React from "react";
import criticalIcon from "../../assets/images/critical.png"; // Красный треугольник
import warningIcon from "../../assets/images/warning.png"; // Желтый треугольник
import "../../Style/ErrorIndicator.css"; // Подключаем стили
const ErrorIndicator = ({ criticalCount, warningCount }) => {
return (
<div className="error-indicator">
{/* Красный индикатор (критические ошибки) */}
<div className="error-item critical">
<img src={criticalIcon} alt="Критическая ошибка" />
<span>{criticalCount}</span>
</div>
{/* Желтый индикатор (предупреждения) */}
<div className="error-item warning">
<img src={warningIcon} alt="Предупреждение" />
<span>{warningCount}</span>
</div>
</div>
);
};
export default ErrorIndicator;

View File

@ -1,30 +0,0 @@
import React, { useState } from "react";
import "../Style/Expandable.css"
const ExpandableInfo = ({ details }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
<div className="expandable-info">
<button onClick={toggleExpand} className="expand-button">
{isExpanded ? "Скрыть" : "Подробнее"}
</button>
{isExpanded && (
<div className="details-menu">
{details.map((detail, index) => (
<div key={index} className="detail-item">
<span className="label">{detail.label}:</span>
<span className="value">{detail.value}</span>
</div>
))}
</div>
)}
</div>
);
};
export default ExpandableInfo;

View File

@ -1,7 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Modal from "./Modal"; import Modal from "./Modal";
import "../../Style/LoginModal.css"; import "../../Style/LoginModal.css";
import Logo from '../../assets/images/logo.svg?react';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import axios from 'axios';
const LoginModal = ({ onLogin, onClose }) => { const LoginModal = ({ onLogin, onClose }) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@ -15,10 +17,9 @@ const LoginModal = ({ onLogin, onClose }) => {
e.preventDefault(); e.preventDefault();
try { try {
// Отправляем данные на бэкенд const response = await fetch('http://192.168.2.39:3000/api/auth/login', {
console.log("Отправляем данные:", { username, password });
const response = await fetch('http://192.168.2.39:3000/auth/login', {
method: 'POST', method: 'POST',
credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -28,8 +29,9 @@ const LoginModal = ({ onLogin, onClose }) => {
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
onLogin(); // Успешная авторизация localStorage.setItem('access_token', data.access_token);
onClose(); // Закрыть модальное окно onLogin(data.user);
onClose();
} else { } else {
setError(data.message || "Неверный логин или пароль"); setError(data.message || "Неверный логин или пароль");
} }

View File

@ -1,10 +1,22 @@
import React from "react"; import React from "react";
import { Tabs, Tab, Box } from "@mui/material"; import { Tabs, Tab, Box, styled } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
const StyledTab = styled(Tab)(({ theme }) => ({
minHeight: 48,
'&.Mui-selected': {
color: theme.palette.primary.main,
fontWeight: theme.typography.fontWeightMedium,
},
'&:focus-visible': {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: '-2px',
},
}));
const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => { const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
const handleMouseDown = (e, id) => { const handleMouseDown = (e, id) => {
if (e.button === 1) { if (e.button === 1) { // Middle mouse button
e.preventDefault(); e.preventDefault();
onCloseTab(id); onCloseTab(id);
} }
@ -15,7 +27,13 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
}; };
return ( return (
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{
borderBottom: 1,
borderColor: 'divider',
'& .MuiTabs-indicator': {
backgroundColor: 'primary.main',
}
}}>
<Tabs <Tabs
value={activeTab} value={activeTab}
onChange={handleChange} onChange={handleChange}
@ -23,28 +41,34 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
scrollButtons="auto" scrollButtons="auto"
aria-label="tabs" aria-label="tabs"
> >
{/* Всегда отображаемые вкладки */} {/* Статические вкладки */}
<Tab <StyledTab
label="Главная" label="Главная"
value="Главная" value="Главная"
onMouseDown={(e) => handleMouseDown(e, "Главная")} onMouseDown={(e) => handleMouseDown(e, "Главная")}
/> />
<Tab <StyledTab
label="Визуализация" label="Визуализация"
value="Визуализация" value="Визуализация"
onMouseDown={(e) => handleMouseDown(e, "Визуализация")} onMouseDown={(e) => handleMouseDown(e, "Визуализация")}
/> />
{/* Динамически добавляемые вкладки */} {/* Динамические вкладки */}
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab <StyledTab
key={tab.id} key={tab.id}
label={ label={
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
<span>{tab.title}</span> <span>{tab.title}</span>
<CloseIcon <CloseIcon
fontSize="small" fontSize="small"
sx={{ ml: 1, cursor: "pointer" }} sx={{
ml: 1,
cursor: "pointer",
'&:hover': {
color: 'error.main'
}
}}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onCloseTab(tab.id); onCloseTab(tab.id);

View File

@ -1,56 +0,0 @@
import React from "react";
import "../../Style/common.css"; // Общие стили для табов
const Tabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
const handleMouseDown = (e, id) => {
// Проверяем, была ли нажата средняя кнопка мыши (button === 1)
if (e.button === 1) {
e.preventDefault(); // Предотвращаем стандартное поведение (например, прокрутку)
onCloseTab(id); // Закрываем вкладку
}
};
return (
<div className="tabs">
{/* Всегда отображаемые вкладки */}
<div
className={`tab ${activeTab === "Главная" ? "active" : ""}`}
onClick={() => onTabClick("Главная")}
onMouseDown={(e) => handleMouseDown(e, "Главная")} // Добавляем обработчик для СКМ
>
<span>Главная</span>
</div>
<div
className={`tab ${activeTab === "Визуализация" ? "active" : ""}`}
onClick={() => onTabClick("Визуализация")}
onMouseDown={(e) => handleMouseDown(e, "Визуализация")} // Добавляем обработчик для СКМ
>
<span>Визуализация</span>
</div>
{/* Динамически добавляемые вкладки */}
{tabs.map((tab) => (
<div
key={tab.id}
className={`tab ${activeTab === tab.id ? "active" : ""}`}
onClick={() => onTabClick(tab.id)}
onMouseDown={(e) => handleMouseDown(e, tab.id)} // Добавляем обработчик для СКМ
>
<span>{tab.title}</span>
<button
className="close-tab"
onClick={(e) => {
e.stopPropagation();
onCloseTab(tab.id);
}}
>
×
</button>
</div>
))}
</div>
);
};
export default Tabs;

View File

@ -1,12 +1,27 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import "../../Style/TreeTable.css"; import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Collapse,
Box,
Typography,
useTheme,
Tooltip
} from '@mui/material';
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
const TreeTable = ({ data }) => { const TreeTable = ({ data }) => {
const theme = useTheme();
const tableRef = useRef(null); const tableRef = useRef(null);
const [fontSize, setFontSize] = useState(16); const [fontSize, setFontSize] = useState(16);
const [log, setLog] = useState([]); const [log, setLog] = useState([]);
const [isLogVisible, setIsLogVisible] = useState(true); const [isLogVisible, setIsLogVisible] = useState(false);
const adjustFontSize = () => { const adjustFontSize = () => {
if (tableRef.current) { if (tableRef.current) {
@ -27,6 +42,13 @@ const TreeTable = ({ data }) => {
} }
}; };
useEffect(() => {
adjustFontSize();
window.addEventListener('resize', adjustFontSize);
return () => window.removeEventListener('resize', adjustFontSize);
}, [data]);
// Логирование статусов
useEffect(() => { useEffect(() => {
const newLog = []; const newLog = [];
const traverse = (items) => { const traverse = (items) => {
@ -35,7 +57,7 @@ const TreeTable = ({ data }) => {
newLog.push({ newLog.push({
title: item.title, title: item.title,
status: item.status, status: item.status,
time: new Date().toLocaleTimeString(), // Добавляем время time: new Date().toLocaleTimeString(),
}); });
} }
if (item.items) { if (item.items) {
@ -44,200 +66,285 @@ const TreeTable = ({ data }) => {
}); });
}; };
traverse(data.items); traverse(data.items);
setLog(prevLog => [...newLog, ...prevLog].slice(0, 50));
// Ограничиваем количество сообщений до 50
setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50));
}, [data]); }, [data]);
const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи"); const filteredData = data.items.filter(item => item.title !== "Функциональные задачи");
// Функция для отображения заголовков // Компонент индикаторов статуса
const renderHeaders = (items) => { const StatusIndicators = ({ status }) => (
<>
<Box
sx={{
width: '4px',
height: '20px',
display: 'inline-block',
backgroundColor: statusManager1.getStatusColor(status),
marginRight: '4px',
verticalAlign: 'middle'
}}
/>
<Box
sx={{
width: '4px',
height: '20px',
display: 'inline-block',
backgroundColor: statusManager2.getStatusColor(status),
marginRight: '8px',
verticalAlign: 'middle'
}}
/>
</>
);
// Ячейка с тултипом
const TableCellWithTooltip = ({ children, title, ...props }) => (
<Tooltip title={title} arrow>
<TableCell {...props}>
{children}
</TableCell>
</Tooltip>
);
// Рендер заголовков (первый уровень)
const renderMainHeaders = (items) => {
return items.map((item) => { return items.map((item) => {
const colSpan = item.items ? item.items.length : 1; const colSpan = item.items ? item.items.length : 1;
return ( return (
<th key={item.id} colSpan={colSpan} className="tree-table-header" title={item.title}> <TableCellWithTooltip
<div className="header-content"> key={item.id}
<div colSpan={colSpan}
className="status-indicator-bar" align="center"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }} title={item.title}
/> sx={{
<div backgroundColor: theme.palette.background.paper,
className="status-indicator-bar" border: `1px solid ${theme.palette.divider}`,
style={{ padding: '8px',
backgroundColor: statusManager2.getStatusColor(item.status), whiteSpace: 'nowrap',
marginLeft: "5px", overflow: 'hidden',
}} textOverflow: 'ellipsis'
/> }}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="subtitle2" noWrap>
{item.title} {item.title}
</div> </Typography>
</th> </TableCellWithTooltip>
); );
}); });
}; };
// Функция для отображения подзаголовков // Рендер подзаголовков (второй уровень)
const renderSubHeaders = (items) => { const renderSubHeaders = (items) => {
return items.map((item) => { return items.flatMap((item) => {
if (item.items) { if (item.items) {
return item.items.map((child) => ( return item.items.map((child) => (
<th key={child.id} className="tree-table-header" title={child.title}> <TableCellWithTooltip
<div className="header-content"> key={child.id}
<div align="center"
className="status-indicator-bar" title={child.title}
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }} sx={{
/> backgroundColor: theme.palette.background.paper,
<div border: `1px solid ${theme.palette.divider}`,
className="status-indicator-bar" padding: '8px',
style={{ whiteSpace: 'nowrap',
backgroundColor: statusManager2.getStatusColor(child.status), overflow: 'hidden',
marginLeft: "5px", textOverflow: 'ellipsis'
}} }}
/> >
<StatusIndicators status={child.status} />
<Typography component="span" variant="subtitle2" noWrap>
{child.title} {child.title}
</div> </Typography>
</th> </TableCellWithTooltip>
)); ));
} else {
return (
<th key={item.id} className="tree-table-header" title={item.title}>
<div className="header-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
}}
/>
{item.title}
</div>
</th>
);
} }
return (
<TableCellWithTooltip
key={item.id}
align="center"
title={item.title}
sx={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="subtitle2" noWrap>
{item.title}
</Typography>
</TableCellWithTooltip>
);
}); });
}; };
// Функция для отображения данных // Рендер данных (третий уровень)
const renderData = (items) => { const renderDataCells = (items) => {
return items.map((item) => { return items.flatMap((item) => {
if (item.items) { if (item.items) {
return item.items.map((child) => { return item.items.flatMap((child) => {
if (child.items) { if (child.items) {
return child.items.map((subChild) => ( return child.items.map((subChild) => (
<td key={subChild.id} className="tree-table-cell" title={subChild.title}> <TableCellWithTooltip
<div className="cell-content"> key={subChild.id}
<div title={subChild.title}
className="status-indicator-bar" sx={{
style={{ backgroundColor: statusManager1.getStatusColor(subChild.status) }} border: `1px solid ${theme.palette.divider}`,
/> padding: '8px',
<div whiteSpace: 'nowrap',
className="status-indicator-bar" overflow: 'hidden',
style={{ textOverflow: 'ellipsis'
backgroundColor: statusManager2.getStatusColor(subChild.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{subChild.title}</span>
</div>
</td>
));
} else {
return (
<td key={child.id} className="tree-table-cell" title={child.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(child.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(child.status),
marginLeft: "5px",
}}
/>
<span className="cell-text">{child.title}</span>
</div>
</td>
);
}
});
} else {
return (
<td key={item.id} className="tree-table-cell" title={item.title}>
<div className="cell-content">
<div
className="status-indicator-bar"
style={{ backgroundColor: statusManager1.getStatusColor(item.status) }}
/>
<div
className="status-indicator-bar"
style={{
backgroundColor: statusManager2.getStatusColor(item.status),
marginLeft: "5px",
}} }}
/> >
<span className="cell-text">{item.title}</span> <StatusIndicators status={subChild.status} />
</div> <Typography component="span" variant="body2" noWrap>
</td> {subChild.title}
); </Typography>
</TableCellWithTooltip>
));
}
return (
<TableCellWithTooltip
key={child.id}
title={child.title}
sx={{
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={child.status} />
<Typography component="span" variant="body2" noWrap>
{child.title}
</Typography>
</TableCellWithTooltip>
);
});
} }
return (
<TableCellWithTooltip
key={item.id}
title={item.title}
sx={{
border: `1px solid ${theme.palette.divider}`,
padding: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}
>
<StatusIndicators status={item.status} />
<Typography component="span" variant="body2" noWrap>
{item.title}
</Typography>
</TableCellWithTooltip>
);
}); });
}; };
return ( return (
<div className="tree-table-container"> <Box sx={{ width: '100%' }}>
<table ref={tableRef} className="tree-table" style={{ fontSize: `${fontSize}px` }}> <TableContainer
<thead> component={Paper}
<tr> ref={tableRef}
<th sx={{
colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)} fontSize: `${fontSize}px`,
className="tree-table-header" width: '100%',
title={data.title} '& .MuiTableCell-root': {
> py: 1,
<div className="header-content"> px: 2
<div }
className="status-indicator-bar" }}
style={{ backgroundColor: statusManager1.getStatusColor(data.status) }} >
/> <Table sx={{ width: '100%', tableLayout: 'fixed' }}>
<div <TableHead>
className="status-indicator-bar" {/* Основной заголовок таблицы */}
style={{ <TableRow>
backgroundColor: statusManager2.getStatusColor(data.status), <TableCellWithTooltip
marginLeft: "5px", colSpan={filteredData.reduce((acc, item) => acc + (item.items ? item.items.length : 1), 0)}
}} align="center"
/> title={data.title}
{data.title} sx={{
</div> backgroundColor: theme.palette.background.paper,
</th> border: `1px solid ${theme.palette.divider}`,
</tr> padding: '8px'
<tr>{renderHeaders(filteredData)}</tr> }}
<tr>{renderSubHeaders(filteredData)}</tr> >
</thead> <StatusIndicators status={data.status} />
<tbody> <Typography component="span" variant="subtitle1" fontWeight="bold" noWrap>
<tr className="tree-table-row">{renderData(filteredData)}</tr> {data.title}
</tbody> </Typography>
</table> </TableCellWithTooltip>
<button onClick={() => setIsLogVisible(!isLogVisible)} className="toggle-log-button"> </TableRow>
{isLogVisible ? "Скрыть лог" : "Показать лог"}
</button> {/* Строка с основными заголовками */}
{isLogVisible && ( <TableRow>
<div className="status-log"> {renderMainHeaders(filteredData)}
<h3>Лог статусов</h3> </TableRow>
<ul>
{/* Строка с подзаголовками (которая пропала в предыдущей версии) */}
<TableRow>
{renderSubHeaders(filteredData)}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
{renderDataCells(filteredData)}
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Button
variant="outlined"
onClick={() => setIsLogVisible(!isLogVisible)}
size="small"
sx={{ mt: 2 }}
>
{isLogVisible ? 'Скрыть историю изменения статусов' : 'Показать историю изменения статусов'}
</Button>
<Collapse in={isLogVisible}>
<Box sx={{
mt: 2,
p: 2,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 1,
backgroundColor: theme.palette.background.paper
}}>
<Typography variant="h6" gutterBottom>
История изменения статусов
</Typography>
<Box component="ul" sx={{
pl: 2,
maxHeight: 400,
overflow: 'auto',
listStyle: 'none'
}}>
{log.map((entry, index) => ( {log.map((entry, index) => (
<li key={index} style={{ color: statusManager1.getStatusColor(entry.status) }}> <Box
component="li"
key={index}
sx={{
py: 1,
borderBottom: `1px solid ${theme.palette.divider}`,
color: statusManager1.getStatusColor(entry.status)
}}
>
[{entry.time}] {entry.status}: {entry.title} [{entry.time}] {entry.status}: {entry.title}
</li> </Box>
))} ))}
</ul> </Box>
</div> </Box>
)} </Collapse>
</div> </Box>
); );
}; };

View File

@ -0,0 +1,22 @@
import axios from 'axios';
export const checkAuth = async () => {
try {
const response = await fetch('http://192.168.2.39:3000/api/auth/check', {
method: 'GET',
credentials: 'include', // Важно для отправки cookies
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token') || ''}`,
},
});
if (!response.ok) {
throw new Error('Not authenticated');
}
return await response.json();
} catch (err) {
console.error('Auth check failed:', err);
return { isAuthenticated: false };
}
};

View File

@ -0,0 +1,74 @@
import React, { useEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
const LazyChartBatchRenderer = ({ charts }) => {
const [visibleIndices, setVisibleIndices] = useState(new Set());
const placeholderRefs = useRef([]);
const ChartSkeleton = () => (
<Box sx={{ backgroundColor: '#fff', borderRadius: '8px', padding: '20px', marginBottom: '20px', position: 'relative', height: '500px' }}>
<Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
<Skeleton variant="circular" width={16} height={16} />
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width="40%" height={30} />
<Skeleton variant="text" width="30%" height={30} />
</Box>
<Skeleton variant="rectangular" width="100%" height={300} />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((_, i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
))}
</Box>
</Box>
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
setVisibleIndices((prev) => {
const updated = new Set(prev);
entries.forEach((entry) => {
const index = parseInt(entry.target.dataset.index, 10);
if (entry.isIntersecting) {
updated.add(index);
} else {
updated.delete(index);
}
});
return updated;
});
},
{
root: null,
rootMargin: '200px',
threshold: 0.1,
}
);
placeholderRefs.current.forEach((ref) => {
if (ref) observer.observe(ref);
});
return () => {
observer.disconnect();
};
}, [charts]);
return (
<div>
{charts.map((chart, index) => (
<div
key={index}
ref={(el) => (placeholderRefs.current[index] = el)}
data-index={index}
>
{visibleIndices.has(index) ? chart : <ChartSkeleton />}
</div>
))}
</div>
);
};
export default LazyChartBatchRenderer;

View File

@ -1,6 +1,6 @@
import SystemStatusChart from "../../Charts/SystemStatusChart"; import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable"; import TreeTable from "../UI/TreeTable";
import TreeChart from "../TreeChart/TreeChart"; import FlowChart from "../TreeChart/FlowChart";
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => { const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
if (activeTab === "Главная") { if (activeTab === "Главная") {
@ -22,7 +22,7 @@ const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleO
</div> </div>
); );
} else if (activeTab === "Визуализация") { } else if (activeTab === "Визуализация") {
return <TreeChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />; return <FlowChart data={treeData1} onNodeClick={(id, title) => handleOpenTab(id, title)} />;
} else { } else {
const tabData = tabContent[activeTab]; const tabData = tabContent[activeTab];
return tabData ? tabData.content : <p>Нет данных</p>; return tabData ? tabData.content : <p>Нет данных</p>;

View File

@ -1,52 +0,0 @@
/* Основной контейнер */
.dashboard-container {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
background-color: var(--background-color);
color: var(--text-color);
}
/* Сайдбар */
.sidebar {
flex-shrink: 0;
height: 100vh;
overflow-y: auto;
background-color: var(--sidebar-color);
color: var(--sidebar-text-color);
transition: width 0.2s ease;
}
/* Основной контент */
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow: auto;
background-color: var(--background-color);
color: var(--text-color);
}
/* Контент */
.content {
background-color: var(--modal-background);
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.521);
max-width: 100%;
overflow: auto;
color: var(--text-color);
}
/* Заголовки */
h2 {
color: var(--text-color);
text-align: center;
}
p {
color: var(--text-color);
}

View File

@ -1,38 +0,0 @@
.error-indicator {
display: flex;
align-items: center;
gap: 15px;
padding-bottom: 20px;
}
.error-item {
display: flex;
align-items: center;
gap: 5px;
}
.error-item img {
width: 30px;
height: 30px;
}
.error-item span {
font-size: 18px;
font-weight: bold;
}
.critical span {
color: red;
}
.warning span {
color: orange;
}
.indicator-container {
display: flex;
align-items: center;
gap: 15px;
justify-content: center;
}

View File

@ -1,39 +0,0 @@
.expandable-info {
margin-top: 10px;
}
.expand-button {
background-color: #444;
color: white;
border: none;
padding: 8px 16px;
cursor: pointer;
border-radius: 4px;
}
.expand-button:hover {
background-color: #333;
}
.details-menu {
margin-top: 10px;
padding: 10px;
border: 1px solid #333;
border-radius: 4px;
background-color: white;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.label {
font-weight: bold;
color: #333
}
.value {
color: #333;
}

View File

@ -1,143 +0,0 @@
/* Сайдбар */
.sidebar {
height: 100vh;
background-color: var(--sidebar-color);
color: var(--sidebar-text-color);
position: fixed;
left: 0;
top: 0;
z-index: 999;
overflow: hidden;
transition: width 0.2s ease;
display: flex;
flex-direction: column;
}
/* Контейнер для основного контента меню */
.sidebar-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 20px;
padding-right: 10px;
/* Отступ справа для скроллбара */
}
/* Заголовок меню */
.sidebar-title {
margin-bottom: 20px;
font-size: 1.5em;
font-weight: bold;
color: var(--sidebar-text-color);
padding: 10px;
text-align: center;
/* font-size: 2vh; */
}
/* Элементы меню */
.menu-item {
margin-bottom: 10px;
color: var(--sidebar-text-color);
width: 100%;
}
/* Элемент для перетаскивания */
.sidebar-resizer {
width: 5px;
height: 100%;
background-color: rgba(255, 255, 255, 0.1);
position: absolute;
right: 0;
top: 0;
cursor: ew-resize;
transition: background-color 0.2s ease;
z-index: 1000;
}
.sidebar-resizer:hover {
background-color: rgba(255, 255, 255, 0.3);
}
/* Стили для заголовка элемента меню */
.menu-item-header {
display: flex;
align-items: center;
justify-content: space-between;
/* Распределяем пространство между элементами */
padding: 10px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
width: 100%;
/* Занимаем всю доступную ширину */
box-sizing: border-box;
/* Учитываем padding в ширине */
}
/* Стили для текста элемента меню */
.menu-item-header span {
flex: 1;
/* Текст занимает все доступное пространство */
margin-right: 14px;
/* Отступ справа для текста */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие, если текст не помещается */
}
/* Стили для иконок */
.menu-item-header .open-parent-icon,
.menu-item-header .toggle-icon {
flex-shrink: 0;
/* Запрещаем сжатие иконок */
margin-left: 1px;
/* Отступ между иконками */
cursor: pointer;
}
.menu-item-header:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Круглый индикатор статуса */
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 10px;
flex-shrink: 0;
}
/* Подменю */
.submenu {
margin-left: 20px;
/* Отступ слева для вложенных элементов */
margin-top: 10px;
}
/* Стили для элементов нижнего уровня вложенности */
/* Дополнительные отступы для элементов без иконок */
.menu-item:not(.has-children) .menu-item-header {
padding-right: 25px;
/* Добавляем отступ справа для элементов без иконок */
}
/* Футер сайдбара */
.sidebar-footer {
padding: 10px;
background-color: var(--sidebar-color);
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
width: 100%;
}
.help,
.settings {
color: var(--sidebar-text-color);
margin: 5px 0;
overflow-x: hidden;
text-align: left;
}

View File

@ -1,62 +0,0 @@
.tree-table-container {
width: 100%;
overflow-x: hidden;
/* Убираем горизонтальный скролл */
}
.tree-table {
width: 100%;
border-collapse: collapse;
text-align: center;
table-layout: fixed;
/* Фиксированная ширина колонок */
background-color: var(--table-cell-background);
color: var(--table-text-color);
}
.tree-table-header {
padding: 10px;
border: 1px solid black;
font-weight: bold;
white-space: nowrap;
/* Текст не переносится */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие */
background-color: var(--table-header-background);
}
.tree-table-cell {
padding: 8px;
border: 1px solid black;
white-space: nowrap;
/* Текст не переносится */
overflow: hidden;
/* Скрываем текст, который не помещается */
text-overflow: ellipsis;
/* Добавляем многоточие */
}
.cell-content,
.header-content {
display: flex;
align-items: center;
gap: 2px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.cell-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.status-indicator-bar {
width: 6px;
height: 20px;
border-radius: 3px;
flex-shrink: 0;
}

View File

@ -1,53 +0,0 @@
/* Контейнер для вкладок */
.tabs {
display: flex;
gap: 5px;
padding: 5px;
background-color: var(--sidebar-color);
border-bottom: 2px solid var(--accent-color);
overflow-x: auto;
border-radius: 5px;
white-space: nowrap;
}
/* Стили для отдельной вкладки */
.tab {
display: flex;
align-items: center;
background-color: var(--sidebar-color);
color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
padding: 5px 15px;
border-radius: 5px 5px 0 0;
cursor: pointer;
flex-shrink: 0;
transition: background-color 0.3s ease;
}
/* Активная вкладка */
.tab.active {
background-color: var(--accent-color);
}
/* Кнопка закрытия вкладки */
.close-tab {
background: none;
border: none;
color: var(--sidebar-text-color);
/* Используем переменную для цвета текста */
cursor: pointer;
font-size: 16px;
margin-left: 10px;
padding: 0;
transition: color 0.3s ease;
}
/* Эффект при наведении на кнопку закрытия */
.close-tab:hover {
color: #ff6b6b;
}
/* Эффект при наведении на вкладку */
.tab:hover {
background-color: var(--accent-hover-color);
}

View File

@ -1,25 +0,0 @@
/* Темная тема, если пользователь предпочитает ее */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1E1E1E;
--text-color: #E0E0E0;
--header-color: #FFFFFF;
/* Основной цвет текста (светлый) */
--sidebar-color: #2d2d2d;
/* Темный цвет сайдбара */
--sidebar-text-color: #E0E0E0;
/* Светлый текст в сайдбаре */
--modal-background: #2d2d2d;
--modal--btn-background: #333333;
--modal-text: #FFFFFF;
--table-border: #444444;
--table-header-background: #2d2d2d;
--table-cell-background: #333333;
--table-text-color: #E0E0E0;
/* Светлый текст в таблице */
--TreeChart-text-color: #ffffff;
--scrollbar-track-color: #333;
/* hover for buttons */
--hover-button: #333d4d;
}
}

View File

@ -1,23 +0,0 @@
/* Светлая тема по умолчанию */
:root {
--background-color: #FFFFFF;
--text-color: #000000;
--header-color: #333333;
/* Основной цвет текста (черный) */
--sidebar-color: #3d74c7;
/* Синий цвет сайдбара */
--sidebar-text-color: #FFFFFF;
/* Белый текст в сайдбаре и вкладках */
--modal-background: #FFFFFF;
--modal--btn-background: #0f55bec2;
--modal-text: #333333;
--table-border: #ddd;
--table-header-background: #f9f9f9;
--table-cell-background: #FFFFFF;
--table-text-color: #000000;
/* Черный текст в таблице */
/* hover for buttons */
--hover-button: #2d62b1;
--hover-text-color: #FFFFFF
}

View File

@ -1,73 +1,191 @@
import { createTheme } from "@mui/material/styles"; import { createTheme } from "@mui/material/styles";
/**
* Общие настройки темы, применяемые для обеих тем (светлой и темной)
*/
const commonThemeSettings = {
// Настройки формы элементов
shape: {
borderRadius: 8, // Базовый радиус скругления углов для всех компонентов
},
// Переопределения стилей конкретных MUI компонентов
components: {
// Стили для компонента Drawer (боковое меню)
MuiDrawer: {
styleOverrides: {
paper: {
borderRight: 'none', // Убираем правую границу у бокового меню
}
},
MuiTab: {
styleOverrides: {
root: {
textTransform: 'none', // Убираем uppercase
minWidth: 'unset', // Убираем минимальную ширину
padding: '6px 16px',
'&:hover': {
color: 'primary.main',
opacity: 1,
},
'&.Mui-selected': {
color: 'primary.main',
},
'&.Mui-focusVisible': {
backgroundColor: 'action.selected',
},
},
},
},
MuiTabs: {
styleOverrides: {
indicator: {
height: 3, // Толщина индикатора
},
},
},
},
// Стили для кнопок-элементов списка
MuiListItemButton: {
styleOverrides: {
root: {
// Стиль для выбранного элемента
'&.Mui-selected': {
backgroundColor: 'rgba(255, 255, 255, 0.16)',
},
// Стиль при наведении на выбранный элемент
'&.Mui-selected:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.24)',
},
}
}
}
}
};
/**
* Светлая тема приложения
*/
export const lightTheme = createTheme({ export const lightTheme = createTheme({
...commonThemeSettings, // Распаковываем общие настройки
// Цветовая палитра для светлой темы
palette: { palette: {
mode: "light", mode: "light", // Режим светлой темы
// Фоновые цвета
background: { background: {
default: "#FFFFFF", default: "#FFFFFF", // Основной фон приложения
paper: "#FFFFFF", paper: "#FFFFFF", // Фон "бумажных" поверхностей (карточек, панелей)
}, },
// Текстовые цвета
text: { text: {
primary: "#000000", primary: "#000000", // Основной цвет текста
secondary: "#333333", // Вторичный цвет текста
}, },
// Основные цвета UI
primary: { primary: {
main: "#3d74c7", main: "#3d74c7", // Основной брендовый цвет
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
}, },
// Дополнительные цвета UI
secondary: { secondary: {
main: "#0f55bec2", main: "#0f55bec2", // Вторичный брендовый цвет
}, },
divider: "#e0e0e0", // Цвет разделителей
// Кастомные цвета для специфических элементов
custom: { custom: {
background: "#FFFFFF", background: "#D4EFFC", // Кастомный фоновый цвет
text: "#000000", text: "#000000", // Кастомный цвет текста
sidebar: "#3d74c7", sidebar: "#025EA1", // Фон боковой панели
sidebarText: "#FFFFFF", sidebarText: "#FFFFFF", // Текст в боковой панели
modalBackground: "#FFFFFF", sidebarHover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении в боковой панели
modalBtnBackground: "#0f55bec2", modalBackground: "#FFFFFF", // Фон модальных окон
modalText: "#333333", modalBtnBackground: "#0f55bec2", // Фон кнопок в модальных окнах
tableBorder: "#ddd", modalText: "#333333", // Текст в модальных окнах
tableHeaderBackground: "#f9f9f9", tableBorder: "#ddd", // Границы таблиц
tableCellBackground: "#FFFFFF", tableHeaderBackground: "#f9f9f9", // Фон заголовков таблиц
tableText: "#000000", tableCellBackground: "#FFFFFF", // Фон ячеек таблиц
treeChartText: "#000000", tableText: "#000000", // Текст в таблицах
scrollbarTrack: "#f1f1f1", treeChartText: "#000000", // Текст в древовидных диаграммах
hoverButton: "#2d62b1", scrollbarTrack: "#f1f1f1", // Цвет трека скроллбара
hoverText: "#FFFFFF", hoverButton: "#2d62b1", // Цвет кнопок при наведении
hoverText: "#FFFFFF", // Цвет текста при наведении
}, },
// Цвета для различных состояний
action: {
hover: "rgba(0, 0, 0, 0.04)", // Цвет при наведении на интерактивные элементы
selected: "rgba(0, 0, 0, 0.08)", // Цвет выбранных элементов
}
}, },
}); });
/**
* Темная тема приложения
*/
export const darkTheme = createTheme({ export const darkTheme = createTheme({
...commonThemeSettings, // Распаковываем общие настройки
// Цветовая палитра для темной темы
palette: { palette: {
mode: "dark", mode: "dark", // Режим темной темы
// Фоновые цвета
background: { background: {
default: "#1E1E1E", default: "#2d2d2d", // Основной фон приложения
paper: "#2d2d2d", paper: "#2d2d2d", // Фон "бумажных" поверхностей
}, },
// Текстовые цвета
text: { text: {
primary: "#E0E0E0", primary: "#E0E0E0", // Основной цвет текста
secondary: "#B0B0B0", // Вторичный цвет текста
}, },
// Основные цвета UI
primary: { primary: {
main: "#2d2d2d", main: "#3d74c7", // Основной брендовый цвет (может совпадать со светлой темой)
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
}, },
// Дополнительные цвета UI
secondary: { secondary: {
main: "#333333", main: "#0f55bec2", // Вторичный брендовый цвет
}, },
divider: "#444444", // Цвет разделителей
// Кастомные цвета для специфических элементов
custom: { custom: {
background: "#1E1E1E", background: "#1E1E1E", // Кастомный фоновый цвет
text: "#E0E0E0", text: "#E0E0E0", // Кастомный цвет текста
sidebar: "#2d2d2d", sidebar: "#2d2d2d", // Фон боковой панели
sidebarText: "#E0E0E0", sidebarText: "#E0E0E0", // Текст в боковой панели
modalBackground: "#2d2d2d", sidebarHover: "rgba(255, 255, 255, 0.16)", // Цвет при наведении в боковой панели
modalBtnBackground: "#333333", modalBackground: "#2d2d2d", // Фон модальных окон
modalText: "#FFFFFF", modalBtnBackground: "#333333", // Фон кнопок в модальных окнах
tableBorder: "#444444", modalText: "#FFFFFF", // Текст в модальных окнах
tableHeaderBackground: "#2d2d2d", tableBorder: "#444444", // Границы таблиц
tableCellBackground: "#333333", tableHeaderBackground: "#2d2d2d", // Фон заголовков таблиц
tableText: "#E0E0E0", tableCellBackground: "#333333", // Фон ячеек таблиц
treeChartText: "#FFFFFF", tableText: "#E0E0E0", // Текст в таблицах
scrollbarTrack: "#333", treeChartText: "#FFFFFF", // Текст в древовидных диаграммах
hoverButton: "#333d4d", scrollbarTrack: "#333", // Цвет трека скроллбара
hoverText: "#E0E0E0", hoverButton: "#333d4d", // Цвет кнопок при наведении
hoverText: "#E0E0E0", // Цвет текста при наведении
}, },
// Цвета для различных состояний
action: {
hover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении на интерактивные элементы
selected: "rgba(255, 255, 255, 0.16)", // Цвет выбранных элементов
}
}, },
}); });

319
src/assets/images/logo.svg Normal file
View File

@ -0,0 +1,319 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 273.81 45.36" style="enable-background:new 0 0 273.81 45.36;" xml:space="preserve">
<style type="text/css">
.st0{fill:#428AC9;}
.st1{fill:url(#SVGID_1_);}
</style>
<g>
<rect x="58.03" y="1.44" class="st0" width="1.62" height="40.83"/>
<path class="st0" d="M29.84,0.03V0h-0.95h-0.01h-0.95v0.03C16.95,0.49,8.06,9.1,7.11,19.94c-0.06,0.63-0.09,1.27-0.09,1.92
c0,0.64,0.03,1.28,0.09,1.92c0.97,11.16,10.36,19.94,21.77,19.94h0.96v-3.83h-0.96c-9.28,0-16.96-7-17.92-16.11h2.6h8.36
c-0.22-0.69-0.34-1.43-0.34-2.2c0-0.56,0.07-1.11,0.19-1.63h-3.08c0.91-4.82,5.2-8.46,10.2-8.46c3.49,0,6.73,1.77,8.63,4.63h4.34
c-2.23-5.09-7.38-8.46-12.97-8.46c-7.12,0-13.14,5.33-14.08,12.29h-1.47h-2.38c0.96-9.11,8.64-16.11,17.92-16.11h0.01
c9.28,0,16.96,7,17.92,16.11H36c0.12,0.53,0.19,1.07,0.19,1.63c0,0.77-0.12,1.51-0.34,2.2h0.58h2.65h3.88h2h5.79v-1.92
C50.75,10.12,41.45,0.52,29.84,0.03z"/>
<path class="st0" d="M30.11,32.79c-0.4,0.05-0.81,0.08-1.22,0.08c-4.33,0-8.12-2.73-9.65-6.59h-4.02
c1.67,6.02,7.21,10.42,13.67,10.42c0.41,0,0.82-0.02,1.22-0.06V32.79z"/>
<radialGradient id="SVGID_1_" cx="-5958.7173" cy="3785.8042" r="51.5778" gradientTransform="matrix(0.1405 0 0 0.1405 864.4218 -513.426)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#4A96D2"/>
<stop offset="1" style="stop-color:#1F2466"/>
</radialGradient>
<path class="st1" d="M32.71,24.79c-1.91,2.11-5.16,2.28-7.27,0.37c-2.11-1.9-2.28-5.16-0.38-7.27c1.91-2.11,5.16-2.28,7.27-0.37
C34.44,19.42,34.61,22.68,32.71,24.79z"/>
<g>
<path d="M77.34,11.48h-1.17V3.5h-4.29v7.98h-1.17V2.41h6.62V11.48z"/>
<path d="M80.63,14.2h-1.09V4.87h1.04v0.95h0.03c0.16-0.23,0.35-0.42,0.55-0.56c0.2-0.14,0.41-0.25,0.61-0.32
c0.2-0.07,0.4-0.11,0.58-0.14c0.18-0.02,0.33-0.03,0.45-0.03c0.52,0,0.96,0.08,1.33,0.24c0.37,0.16,0.67,0.38,0.9,0.67
c0.23,0.29,0.4,0.64,0.51,1.06c0.11,0.42,0.16,0.88,0.16,1.39c0,0.48-0.06,0.93-0.17,1.35s-0.29,0.78-0.52,1.09
c-0.23,0.31-0.53,0.56-0.89,0.74c-0.36,0.18-0.78,0.27-1.26,0.27c-0.21,0-0.41-0.02-0.62-0.05c-0.21-0.03-0.41-0.09-0.6-0.17
c-0.19-0.08-0.38-0.18-0.54-0.3c-0.17-0.12-0.31-0.27-0.43-0.45h-0.03V14.2z M82.61,10.6c0.68,0,1.17-0.22,1.47-0.65
c0.3-0.44,0.45-1.06,0.45-1.86c0-0.37-0.03-0.7-0.1-1c-0.06-0.29-0.17-0.54-0.32-0.73c-0.15-0.19-0.35-0.34-0.6-0.45
s-0.56-0.16-0.93-0.16c-0.64,0-1.12,0.22-1.46,0.67s-0.5,1.04-0.5,1.76c0,0.73,0.17,1.32,0.5,1.76S81.96,10.6,82.61,10.6z"/>
<path d="M90.1,11.58c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C91.03,11.5,90.59,11.58,90.1,11.58z M90.1,10.6
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C89.03,10.4,89.51,10.6,90.1,10.6z"/>
<path d="M95.98,11.48h-1.09V4.87h4.34v0.98h-3.25V11.48z"/>
<path d="M101.46,14.2h-1.09V4.87h1.04v0.95h0.03c0.16-0.23,0.35-0.42,0.55-0.56c0.2-0.14,0.41-0.25,0.61-0.32
c0.2-0.07,0.4-0.11,0.58-0.14c0.18-0.02,0.33-0.03,0.45-0.03c0.52,0,0.96,0.08,1.33,0.24c0.37,0.16,0.67,0.38,0.9,0.67
c0.23,0.29,0.4,0.64,0.51,1.06c0.11,0.42,0.16,0.88,0.16,1.39c0,0.48-0.06,0.93-0.17,1.35s-0.29,0.78-0.52,1.09
c-0.23,0.31-0.53,0.56-0.89,0.74c-0.36,0.18-0.78,0.27-1.26,0.27c-0.21,0-0.41-0.02-0.62-0.05c-0.21-0.03-0.41-0.09-0.6-0.17
c-0.19-0.08-0.38-0.18-0.54-0.3c-0.17-0.12-0.31-0.27-0.43-0.45h-0.03V14.2z M103.44,10.6c0.68,0,1.17-0.22,1.47-0.65
c0.3-0.44,0.45-1.06,0.45-1.86c0-0.37-0.03-0.7-0.1-1c-0.06-0.29-0.17-0.54-0.32-0.73c-0.15-0.19-0.35-0.34-0.6-0.45
s-0.56-0.16-0.93-0.16c-0.64,0-1.12,0.22-1.46,0.67s-0.5,1.04-0.5,1.76c0,0.73,0.17,1.32,0.5,1.76S102.79,10.6,103.44,10.6z"/>
<path d="M112.92,11.48h-0.96v-1.21h-0.03c-0.07,0.18-0.17,0.35-0.32,0.51c-0.14,0.16-0.31,0.3-0.5,0.42
c-0.19,0.12-0.4,0.21-0.63,0.28s-0.48,0.1-0.73,0.1c-0.3,0-0.58-0.05-0.83-0.14s-0.46-0.23-0.64-0.39
c-0.18-0.17-0.32-0.37-0.41-0.59c-0.1-0.22-0.15-0.47-0.15-0.73c0-0.44,0.12-0.8,0.35-1.08C108.3,8.38,108.61,8.16,109,8
c0.38-0.16,0.82-0.27,1.31-0.34s1-0.11,1.52-0.14V7.19c0-0.48-0.11-0.84-0.34-1.08s-0.63-0.36-1.2-0.36
c-0.32,0-0.66,0.04-1.02,0.12s-0.67,0.19-0.93,0.32l-0.19-0.89c0.27-0.15,0.6-0.27,1.01-0.38c0.41-0.1,0.82-0.16,1.26-0.16
c0.49,0,0.9,0.06,1.22,0.17c0.32,0.12,0.58,0.29,0.77,0.51s0.32,0.5,0.4,0.84c0.08,0.34,0.12,0.73,0.12,1.17V11.48z M111.83,8.37
c-0.37,0.02-0.73,0.04-1.08,0.08c-0.35,0.03-0.66,0.1-0.94,0.19s-0.5,0.22-0.67,0.38c-0.17,0.16-0.25,0.37-0.25,0.63
c0,0.27,0.09,0.5,0.28,0.68c0.19,0.19,0.48,0.28,0.89,0.28c0.52,0,0.94-0.16,1.28-0.48s0.5-0.79,0.5-1.41V8.37z"/>
<path d="M121.53,4.87l0.65,6.61h-1.04l-0.39-4.53V6.28h-0.03l-0.21,0.66l-1.66,4.53h-0.95l-1.66-4.53l-0.21-0.66h-0.03v0.65
l-0.39,4.55h-1.04l0.65-6.61h1.22l1.94,5.4h0.03l1.88-5.4H121.53z"/>
<path d="M130.54,4.87l0.65,6.61h-1.04l-0.39-4.53V6.28h-0.03l-0.21,0.66l-1.66,4.53h-0.95l-1.66-4.53l-0.21-0.66h-0.03v0.65
l-0.39,4.55h-1.04l0.65-6.61h1.22l1.94,5.4h0.03l1.88-5.4H130.54z"/>
<path d="M138.5,11.48h-1.09V8.54h-3.39v2.94h-1.09V4.87h1.09v2.68h3.39V4.87h1.09V11.48z"/>
<path d="M143.3,11.58c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C144.23,11.5,143.79,11.58,143.3,11.58z M143.3,10.6
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C142.23,10.4,142.7,10.6,143.3,10.6z"/>
<path d="M153.17,11.08c-0.16,0.14-0.41,0.26-0.76,0.36c-0.35,0.1-0.78,0.15-1.28,0.15c-1.17,0-2.03-0.3-2.58-0.91
c-0.56-0.6-0.84-1.45-0.84-2.53c0-0.51,0.07-0.97,0.2-1.39c0.13-0.42,0.33-0.77,0.59-1.07c0.26-0.3,0.57-0.53,0.94-0.69
c0.37-0.16,0.78-0.24,1.25-0.24c0.53,0,0.98,0.09,1.35,0.26c0.37,0.17,0.67,0.41,0.9,0.71c0.23,0.3,0.4,0.65,0.5,1.06
c0.1,0.41,0.16,0.84,0.16,1.31v0.35h-4.74c0.06,0.73,0.28,1.26,0.65,1.62c0.37,0.35,0.95,0.53,1.72,0.53
c0.42,0,0.77-0.04,1.04-0.11s0.51-0.17,0.72-0.29L153.17,11.08z M152.42,7.54c0.01-0.25-0.02-0.49-0.09-0.71
c-0.07-0.22-0.17-0.41-0.31-0.57c-0.14-0.16-0.32-0.29-0.53-0.38c-0.22-0.09-0.47-0.14-0.75-0.14c-0.56,0-1,0.15-1.31,0.45
c-0.31,0.3-0.5,0.75-0.57,1.33H152.42z"/>
<path d="M160.94,11.58c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C161.87,11.5,161.43,11.58,160.94,11.58z M160.94,10.6
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C159.87,10.4,160.35,10.6,160.94,10.6z"/>
<path d="M166.4,6.35c0.18-0.48,0.49-0.85,0.91-1.1c0.43-0.25,0.95-0.38,1.57-0.38c0.94,0,1.65,0.28,2.14,0.84s0.73,1.38,0.73,2.46
c0,1.07-0.26,1.91-0.79,2.51c-0.53,0.6-1.27,0.9-2.24,0.9c-0.6,0-1.1-0.09-1.52-0.29c-0.42-0.19-0.76-0.46-1.02-0.82
c-0.26-0.36-0.45-0.79-0.57-1.3c-0.12-0.5-0.17-1.08-0.17-1.72c0-1.04,0.07-1.91,0.22-2.6c0.15-0.69,0.39-1.25,0.72-1.67
c0.33-0.42,0.77-0.73,1.3-0.93s1.19-0.31,1.97-0.36c0.25-0.02,0.48-0.04,0.67-0.07s0.41-0.08,0.63-0.14v1.02
c-0.11,0.03-0.22,0.05-0.31,0.08c-0.1,0.03-0.2,0.05-0.3,0.07c-0.11,0.02-0.23,0.03-0.36,0.05s-0.29,0.03-0.47,0.05
c-0.57,0.05-1.05,0.13-1.43,0.24c-0.38,0.11-0.7,0.28-0.93,0.53c-0.24,0.24-0.42,0.57-0.54,0.98c-0.12,0.41-0.2,0.96-0.23,1.63
H166.4z M166.64,8.17c0,0.35,0.04,0.68,0.12,0.98c0.08,0.3,0.21,0.55,0.38,0.77c0.17,0.22,0.39,0.38,0.65,0.5
c0.26,0.12,0.57,0.17,0.93,0.17c0.62,0,1.09-0.22,1.39-0.65c0.31-0.43,0.46-1.02,0.46-1.78c0-0.76-0.16-1.34-0.47-1.73
c-0.32-0.39-0.81-0.59-1.48-0.59c-0.37,0-0.68,0.07-0.93,0.19c-0.25,0.13-0.45,0.3-0.61,0.51c-0.16,0.21-0.27,0.46-0.34,0.74
C166.68,7.58,166.64,7.87,166.64,8.17z"/>
<path d="M178.51,11.08c-0.16,0.14-0.41,0.26-0.76,0.36c-0.35,0.1-0.78,0.15-1.28,0.15c-1.17,0-2.03-0.3-2.58-0.91
c-0.56-0.6-0.84-1.45-0.84-2.53c0-0.51,0.07-0.97,0.2-1.39c0.13-0.42,0.33-0.77,0.59-1.07c0.26-0.3,0.57-0.53,0.94-0.69
c0.37-0.16,0.78-0.24,1.25-0.24c0.53,0,0.98,0.09,1.35,0.26c0.37,0.17,0.67,0.41,0.9,0.71c0.23,0.3,0.4,0.65,0.5,1.06
c0.1,0.41,0.16,0.84,0.16,1.31v0.35h-4.74c0.06,0.73,0.28,1.26,0.65,1.62c0.37,0.35,0.95,0.53,1.72,0.53
c0.42,0,0.77-0.04,1.04-0.11s0.51-0.17,0.72-0.29L178.51,11.08z M177.76,7.54c0.01-0.25-0.02-0.49-0.09-0.71
c-0.07-0.22-0.17-0.41-0.31-0.57c-0.14-0.16-0.32-0.29-0.53-0.38c-0.22-0.09-0.47-0.14-0.75-0.14c-0.56,0-1,0.15-1.31,0.45
c-0.31,0.3-0.5,0.75-0.57,1.33H177.76z"/>
<path d="M185.3,11.24c-0.15,0.07-0.36,0.14-0.65,0.22c-0.29,0.08-0.69,0.12-1.21,0.12c-0.56,0-1.05-0.08-1.46-0.24
c-0.41-0.16-0.75-0.39-1.02-0.69s-0.48-0.67-0.61-1.09c-0.13-0.42-0.2-0.9-0.2-1.44c0-1.06,0.29-1.89,0.86-2.47
c0.57-0.59,1.37-0.88,2.39-0.88c0.48,0,0.86,0.04,1.13,0.11c0.27,0.07,0.49,0.13,0.66,0.17l-0.21,0.93
c-0.16-0.05-0.36-0.1-0.6-0.16s-0.51-0.08-0.84-0.08c-0.69,0-1.23,0.18-1.63,0.55s-0.6,0.94-0.6,1.72c0,0.89,0.21,1.54,0.63,1.96
c0.42,0.42,1.03,0.62,1.81,0.62c0.26,0,0.51-0.03,0.75-0.08c0.24-0.05,0.43-0.11,0.59-0.18L185.3,11.24z"/>
<path d="M192.26,11.48h-1.09V5.85h-3.32v5.62h-1.09V4.87h5.49V11.48z"/>
<path d="M199.41,11.08c-0.16,0.14-0.41,0.26-0.76,0.36c-0.35,0.1-0.78,0.15-1.28,0.15c-1.17,0-2.03-0.3-2.58-0.91
c-0.56-0.6-0.84-1.45-0.84-2.53c0-0.51,0.07-0.97,0.2-1.39c0.13-0.42,0.33-0.77,0.59-1.07c0.26-0.3,0.57-0.53,0.94-0.69
c0.37-0.16,0.78-0.24,1.25-0.24c0.53,0,0.98,0.09,1.35,0.26c0.37,0.17,0.67,0.41,0.9,0.71c0.23,0.3,0.4,0.65,0.5,1.06
c0.1,0.41,0.16,0.84,0.16,1.31v0.35h-4.74c0.06,0.73,0.28,1.26,0.65,1.62c0.37,0.35,0.95,0.53,1.72,0.53
c0.42,0,0.77-0.04,1.04-0.11s0.51-0.17,0.72-0.29L199.41,11.08z M198.66,7.54c0.01-0.25-0.02-0.49-0.09-0.71
c-0.07-0.22-0.17-0.41-0.31-0.57c-0.14-0.16-0.32-0.29-0.53-0.38c-0.22-0.09-0.47-0.14-0.75-0.14c-0.56,0-1,0.15-1.31,0.45
c-0.31,0.3-0.5,0.75-0.57,1.33H198.66z"/>
<path d="M206.22,11.48h-1.09v-2.6c-0.04,0.01-0.12,0.03-0.22,0.05c-0.1,0.03-0.23,0.05-0.38,0.08s-0.31,0.06-0.5,0.08
c-0.19,0.02-0.38,0.03-0.58,0.03c-0.35,0-0.68-0.03-0.98-0.1c-0.3-0.07-0.56-0.18-0.77-0.34c-0.22-0.16-0.38-0.36-0.51-0.6
c-0.12-0.25-0.18-0.55-0.18-0.9V4.87h1.09v1.94c0,0.27,0.04,0.49,0.12,0.65c0.08,0.17,0.19,0.3,0.33,0.4
c0.14,0.1,0.31,0.17,0.52,0.21c0.2,0.04,0.43,0.06,0.67,0.06c0.33,0,0.61-0.03,0.85-0.08c0.24-0.05,0.42-0.1,0.54-0.14V4.87h1.09
V11.48z"/>
<path d="M213.37,11.08c-0.16,0.14-0.41,0.26-0.76,0.36c-0.35,0.1-0.78,0.15-1.28,0.15c-1.17,0-2.03-0.3-2.58-0.91
c-0.56-0.6-0.84-1.45-0.84-2.53c0-0.51,0.07-0.97,0.2-1.39c0.13-0.42,0.33-0.77,0.59-1.07c0.26-0.3,0.57-0.53,0.94-0.69
c0.37-0.16,0.78-0.24,1.25-0.24c0.53,0,0.98,0.09,1.35,0.26c0.37,0.17,0.67,0.41,0.9,0.71c0.23,0.3,0.4,0.65,0.5,1.06
c0.1,0.41,0.16,0.84,0.16,1.31v0.35h-4.74c0.06,0.73,0.28,1.26,0.65,1.62c0.37,0.35,0.95,0.53,1.72,0.53
c0.42,0,0.77-0.04,1.04-0.11s0.51-0.17,0.72-0.29L213.37,11.08z M212.62,7.54c0.01-0.25-0.02-0.49-0.09-0.71
c-0.07-0.22-0.17-0.41-0.31-0.57c-0.14-0.16-0.32-0.29-0.53-0.38c-0.22-0.09-0.47-0.14-0.75-0.14c-0.56,0-1,0.15-1.31,0.45
c-0.31,0.3-0.5,0.75-0.57,1.33H212.62z"/>
<path d="M220.98,11.48h-1.09V8.54h-3.39v2.94h-1.09V4.87h1.09v2.68h3.39V4.87h1.09V11.48z"/>
<path d="M223.05,11.48V4.87h1.09v4.21l-0.04,0.87h0.01l0.43-0.69l3.08-4.39h1.04v6.61h-1.09V7.16l0.04-0.76h-0.01l-0.4,0.65
l-3.11,4.43H223.05z"/>
<path d="M235.81,11.08c-0.16,0.14-0.41,0.26-0.76,0.36c-0.35,0.1-0.78,0.15-1.28,0.15c-1.17,0-2.03-0.3-2.58-0.91
c-0.56-0.6-0.84-1.45-0.84-2.53c0-0.51,0.07-0.97,0.2-1.39c0.13-0.42,0.33-0.77,0.59-1.07c0.26-0.3,0.57-0.53,0.94-0.69
c0.37-0.16,0.78-0.24,1.25-0.24c0.53,0,0.98,0.09,1.35,0.26c0.37,0.17,0.67,0.41,0.9,0.71c0.23,0.3,0.4,0.65,0.5,1.06
c0.1,0.41,0.16,0.84,0.16,1.31v0.35h-4.74c0.06,0.73,0.28,1.26,0.65,1.62c0.37,0.35,0.95,0.53,1.72,0.53
c0.42,0,0.77-0.04,1.04-0.11s0.51-0.17,0.72-0.29L235.81,11.08z M235.06,7.54c0.01-0.25-0.02-0.49-0.09-0.71
c-0.07-0.22-0.17-0.41-0.31-0.57c-0.14-0.16-0.32-0.29-0.53-0.38c-0.22-0.09-0.47-0.14-0.75-0.14c-0.56,0-1,0.15-1.31,0.45
c-0.31,0.3-0.5,0.75-0.57,1.33H235.06z"/>
<path d="M71.68,23.32h0.35l2.5-2.9h1.4l-2.81,3.1c0.1,0.05,0.19,0.11,0.27,0.19c0.07,0.07,0.15,0.16,0.24,0.27l2.63,3.06h-1.48
l-2-2.45c-0.13-0.16-0.24-0.28-0.32-0.34s-0.21-0.09-0.36-0.09h-0.42v2.88h-1.09v-6.61h1.09V23.32z"/>
<path d="M79.87,27.13c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C80.8,27.05,80.36,27.13,79.87,27.13z M79.87,26.14
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C78.8,25.95,79.27,26.14,79.87,26.14z"/>
<path d="M90.23,27.02h-1.09v-2.94h-3.39v2.94h-1.09v-6.61h1.09v2.68h3.39v-2.68h1.09V27.02z"/>
<path d="M94.73,27.02h-1.09V21.4h-2.29v-0.98h5.67v0.98h-2.29V27.02z"/>
<path d="M99.22,29.74h-1.09v-9.33h1.04v0.95h0.03c0.16-0.23,0.35-0.42,0.55-0.56c0.2-0.14,0.41-0.25,0.61-0.32
c0.2-0.07,0.4-0.11,0.58-0.14c0.18-0.02,0.33-0.03,0.45-0.03c0.52,0,0.96,0.08,1.33,0.24c0.37,0.16,0.67,0.38,0.9,0.67
c0.23,0.29,0.4,0.64,0.51,1.06c0.11,0.42,0.16,0.88,0.16,1.39c0,0.48-0.06,0.93-0.17,1.35s-0.29,0.78-0.52,1.09
c-0.23,0.31-0.53,0.56-0.89,0.74c-0.36,0.18-0.78,0.27-1.26,0.27c-0.21,0-0.41-0.02-0.62-0.05c-0.21-0.03-0.41-0.09-0.6-0.17
c-0.19-0.08-0.38-0.18-0.54-0.3c-0.17-0.12-0.31-0.27-0.43-0.45h-0.03V29.74z M101.2,26.14c0.68,0,1.17-0.22,1.47-0.65
c0.3-0.44,0.45-1.06,0.45-1.86c0-0.37-0.03-0.7-0.1-1c-0.06-0.29-0.17-0.54-0.32-0.73c-0.15-0.19-0.35-0.34-0.6-0.45
s-0.56-0.16-0.93-0.16c-0.64,0-1.12,0.22-1.46,0.67s-0.5,1.04-0.5,1.76c0,0.73,0.17,1.32,0.5,1.76S100.55,26.14,101.2,26.14z"/>
<path d="M108.69,27.13c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C109.63,27.05,109.18,27.13,108.69,27.13z M108.69,26.14
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C107.62,25.95,108.1,26.14,108.69,26.14z"/>
<path d="M113.32,27.13c-0.12,0-0.23-0.01-0.32-0.03c-0.09-0.02-0.17-0.04-0.23-0.06V26c0.07,0.03,0.12,0.04,0.16,0.05
s0.09,0.01,0.16,0.01c0.22,0,0.41-0.1,0.56-0.29c0.15-0.19,0.27-0.5,0.36-0.92c0.09-0.42,0.16-0.96,0.19-1.61
c0.04-0.66,0.06-1.45,0.06-2.38v-0.44h4.51v6.61h-1.09V21.4h-2.38v0.16c0,1.01-0.03,1.87-0.1,2.57c-0.07,0.7-0.18,1.28-0.34,1.72
c-0.16,0.45-0.36,0.77-0.62,0.97C113.99,27.03,113.68,27.13,113.32,27.13z"/>
<path d="M123.62,24.5c-0.22,0-0.39,0.04-0.52,0.12c-0.13,0.08-0.25,0.21-0.36,0.39l-1.1,2.02h-1.31l1.19-2.09
c0.1-0.18,0.21-0.32,0.32-0.41c0.11-0.1,0.22-0.16,0.31-0.21c-0.47-0.09-0.84-0.29-1.13-0.59c-0.29-0.3-0.43-0.71-0.43-1.25
c0-0.34,0.07-0.65,0.2-0.91c0.13-0.26,0.31-0.47,0.54-0.64s0.49-0.3,0.79-0.38c0.3-0.09,0.62-0.13,0.96-0.13h2.67v6.61h-1.09V24.5
H123.62z M124.67,23.51V21.4h-1.46c-0.48,0-0.84,0.08-1.08,0.25c-0.24,0.17-0.36,0.45-0.36,0.85c0,0.38,0.15,0.64,0.44,0.79
c0.29,0.15,0.67,0.22,1.11,0.22H124.67z"/>
<path d="M136.34,27.02h-1.09V21.4h-3.32v5.62h-1.09v-6.61h5.49V27.02z"/>
<path d="M143.11,27.02h-0.96v-1.21h-0.03c-0.07,0.18-0.17,0.35-0.32,0.51c-0.14,0.16-0.31,0.3-0.5,0.42
c-0.19,0.12-0.4,0.21-0.63,0.28s-0.48,0.1-0.73,0.1c-0.3,0-0.58-0.05-0.83-0.14s-0.46-0.23-0.64-0.39
c-0.18-0.17-0.32-0.37-0.41-0.59c-0.1-0.22-0.15-0.47-0.15-0.73c0-0.44,0.12-0.8,0.35-1.08c0.23-0.28,0.54-0.49,0.93-0.65
s0.82-0.27,1.31-0.34s1-0.11,1.52-0.14v-0.34c0-0.48-0.11-0.84-0.34-1.08s-0.63-0.36-1.2-0.36c-0.32,0-0.66,0.04-1.02,0.12
s-0.67,0.19-0.93,0.32l-0.19-0.89c0.27-0.15,0.6-0.27,1.01-0.38c0.41-0.1,0.82-0.16,1.26-0.16c0.49,0,0.9,0.06,1.22,0.17
c0.32,0.12,0.58,0.29,0.77,0.51s0.32,0.5,0.4,0.84c0.08,0.34,0.12,0.73,0.12,1.17V27.02z M142.03,23.91
c-0.37,0.02-0.73,0.04-1.08,0.08c-0.35,0.03-0.66,0.1-0.94,0.19s-0.5,0.22-0.67,0.38c-0.17,0.16-0.25,0.37-0.25,0.63
c0,0.27,0.09,0.5,0.28,0.68c0.19,0.19,0.48,0.28,0.89,0.28c0.52,0,0.94-0.16,1.28-0.48s0.5-0.79,0.5-1.41V23.91z"/>
<path d="M146.21,29.74h-1.09v-9.33h1.04v0.95h0.03c0.16-0.23,0.35-0.42,0.55-0.56c0.2-0.14,0.41-0.25,0.61-0.32
c0.2-0.07,0.4-0.11,0.58-0.14c0.18-0.02,0.33-0.03,0.45-0.03c0.52,0,0.96,0.08,1.33,0.24c0.37,0.16,0.67,0.38,0.9,0.67
c0.23,0.29,0.4,0.64,0.51,1.06c0.11,0.42,0.16,0.88,0.16,1.39c0,0.48-0.06,0.93-0.17,1.35s-0.29,0.78-0.52,1.09
c-0.23,0.31-0.53,0.56-0.89,0.74c-0.36,0.18-0.78,0.27-1.26,0.27c-0.21,0-0.41-0.02-0.62-0.05c-0.21-0.03-0.41-0.09-0.6-0.17
c-0.19-0.08-0.38-0.18-0.54-0.3c-0.17-0.12-0.31-0.27-0.43-0.45h-0.03V29.74z M148.19,26.14c0.68,0,1.17-0.22,1.47-0.65
c0.3-0.44,0.45-1.06,0.45-1.86c0-0.37-0.03-0.7-0.1-1c-0.06-0.29-0.17-0.54-0.32-0.73c-0.15-0.19-0.35-0.34-0.6-0.45
s-0.56-0.16-0.93-0.16c-0.64,0-1.12,0.22-1.46,0.67s-0.5,1.04-0.5,1.76c0,0.73,0.17,1.32,0.5,1.76S147.54,26.14,148.19,26.14z"/>
<path d="M157.66,27.02h-0.96v-1.21h-0.03c-0.07,0.18-0.17,0.35-0.32,0.51c-0.14,0.16-0.31,0.3-0.5,0.42
c-0.19,0.12-0.4,0.21-0.63,0.28s-0.48,0.1-0.73,0.1c-0.3,0-0.58-0.05-0.83-0.14s-0.46-0.23-0.64-0.39
c-0.18-0.17-0.32-0.37-0.41-0.59c-0.1-0.22-0.15-0.47-0.15-0.73c0-0.44,0.12-0.8,0.35-1.08c0.23-0.28,0.54-0.49,0.93-0.65
s0.82-0.27,1.31-0.34s1-0.11,1.52-0.14v-0.34c0-0.48-0.11-0.84-0.34-1.08s-0.63-0.36-1.2-0.36c-0.32,0-0.66,0.04-1.02,0.12
s-0.67,0.19-0.93,0.32l-0.19-0.89c0.27-0.15,0.6-0.27,1.01-0.38c0.41-0.1,0.82-0.16,1.26-0.16c0.49,0,0.9,0.06,1.22,0.17
c0.32,0.12,0.58,0.29,0.77,0.51s0.32,0.5,0.4,0.84c0.08,0.34,0.12,0.73,0.12,1.17V27.02z M156.58,23.91
c-0.37,0.02-0.73,0.04-1.08,0.08c-0.35,0.03-0.66,0.1-0.94,0.19s-0.5,0.22-0.67,0.38c-0.17,0.16-0.25,0.37-0.25,0.63
c0,0.27,0.09,0.5,0.28,0.68c0.19,0.19,0.48,0.28,0.89,0.28c0.52,0,0.94-0.16,1.28-0.48s0.5-0.79,0.5-1.41V23.91z"/>
<path d="M166.28,20.42l0.65,6.61h-1.04l-0.39-4.53v-0.66h-0.03l-0.21,0.66l-1.66,4.53h-0.95l-1.66-4.53l-0.21-0.66h-0.03v0.65
l-0.39,4.55h-1.04l0.65-6.61h1.22l1.94,5.4h0.03l1.88-5.4H166.28z"/>
<path d="M173.76,26.62c-0.16,0.14-0.41,0.26-0.76,0.36c-0.35,0.1-0.78,0.15-1.28,0.15c-1.17,0-2.03-0.3-2.58-0.91
c-0.56-0.6-0.84-1.45-0.84-2.53c0-0.51,0.07-0.97,0.2-1.39c0.13-0.42,0.33-0.77,0.59-1.07c0.26-0.3,0.57-0.53,0.94-0.69
c0.37-0.16,0.78-0.24,1.25-0.24c0.53,0,0.98,0.09,1.35,0.26c0.37,0.17,0.67,0.41,0.9,0.71c0.23,0.3,0.4,0.65,0.5,1.06
c0.1,0.41,0.16,0.84,0.16,1.31v0.35h-4.74c0.06,0.73,0.28,1.26,0.65,1.62c0.37,0.35,0.95,0.53,1.72,0.53
c0.42,0,0.77-0.04,1.04-0.11s0.51-0.17,0.72-0.29L173.76,26.62z M173,23.09c0.01-0.25-0.02-0.49-0.09-0.71
c-0.07-0.22-0.17-0.41-0.31-0.57c-0.14-0.16-0.32-0.29-0.53-0.38c-0.22-0.09-0.47-0.14-0.75-0.14c-0.56,0-1,0.15-1.31,0.45
c-0.31,0.3-0.5,0.75-0.57,1.33H173z"/>
<path d="M178.21,27.02h-1.09V21.4h-2.29v-0.98h5.67v0.98h-2.29V27.02z"/>
<path d="M182.71,29.74h-1.09v-9.33h1.04v0.95h0.03c0.16-0.23,0.35-0.42,0.55-0.56c0.2-0.14,0.41-0.25,0.61-0.32
c0.2-0.07,0.4-0.11,0.58-0.14c0.18-0.02,0.33-0.03,0.45-0.03c0.52,0,0.96,0.08,1.33,0.24c0.37,0.16,0.67,0.38,0.9,0.67
c0.23,0.29,0.4,0.64,0.51,1.06c0.11,0.42,0.16,0.88,0.16,1.39c0,0.48-0.06,0.93-0.17,1.35s-0.29,0.78-0.52,1.09
c-0.23,0.31-0.53,0.56-0.89,0.74c-0.36,0.18-0.78,0.27-1.26,0.27c-0.21,0-0.41-0.02-0.62-0.05c-0.21-0.03-0.41-0.09-0.6-0.17
c-0.19-0.08-0.38-0.18-0.54-0.3c-0.17-0.12-0.31-0.27-0.43-0.45h-0.03V29.74z M184.69,26.14c0.68,0,1.17-0.22,1.47-0.65
c0.3-0.44,0.45-1.06,0.45-1.86c0-0.37-0.03-0.7-0.1-1c-0.06-0.29-0.17-0.54-0.32-0.73c-0.15-0.19-0.35-0.34-0.6-0.45
s-0.56-0.16-0.93-0.16c-0.64,0-1.12,0.22-1.46,0.67s-0.5,1.04-0.5,1.76c0,0.73,0.17,1.32,0.5,1.76S184.03,26.14,184.69,26.14z"/>
<path d="M192.18,27.13c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C193.11,27.05,192.67,27.13,192.18,27.13z M192.18,26.14
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C191.11,25.95,191.58,26.14,192.18,26.14z"/>
<path d="M196.97,27.02v-6.61h2.47c0.81,0,1.43,0.12,1.85,0.36c0.42,0.24,0.63,0.68,0.63,1.31c0,0.36-0.11,0.68-0.32,0.95
c-0.22,0.27-0.5,0.46-0.86,0.55v0.04c0.42,0.07,0.77,0.23,1.04,0.49c0.27,0.25,0.41,0.61,0.41,1.06c0,0.36-0.06,0.66-0.18,0.9
c-0.12,0.24-0.29,0.43-0.51,0.57s-0.48,0.24-0.79,0.3c-0.31,0.06-0.64,0.08-1,0.08H196.97z M198.06,23.14h1.35
c0.16,0,0.32-0.01,0.49-0.02s0.32-0.05,0.46-0.11c0.14-0.06,0.25-0.15,0.34-0.27c0.09-0.12,0.14-0.28,0.14-0.49
c0-0.18-0.03-0.33-0.09-0.44c-0.06-0.11-0.15-0.2-0.27-0.26c-0.12-0.06-0.25-0.1-0.41-0.12c-0.16-0.02-0.33-0.03-0.52-0.03h-1.48
V23.14z M198.06,26.04h1.67c0.41,0,0.73-0.07,0.96-0.2c0.23-0.13,0.35-0.39,0.35-0.76c0-0.23-0.05-0.42-0.15-0.55
c-0.1-0.13-0.23-0.24-0.38-0.31c-0.16-0.07-0.33-0.12-0.51-0.14s-0.37-0.03-0.55-0.03h-1.39V26.04z"/>
<path d="M71.11,44.64c0.22-0.19,0.46-0.43,0.69-0.73c0.24-0.3,0.41-0.62,0.52-0.95l0.05-0.18l-2.71-6.81h1.18l2.1,5.44l1.76-5.44
h1.1l-2.42,7.01c-0.08,0.24-0.18,0.48-0.3,0.73c-0.12,0.24-0.25,0.47-0.38,0.68c-0.13,0.21-0.28,0.4-0.43,0.57
c-0.15,0.17-0.3,0.3-0.43,0.41L71.11,44.64z"/>
<path d="M81.73,42.34c-0.15,0.07-0.36,0.14-0.65,0.22c-0.29,0.08-0.69,0.12-1.21,0.12c-0.56,0-1.05-0.08-1.46-0.24
c-0.41-0.16-0.75-0.39-1.02-0.69s-0.48-0.67-0.61-1.09c-0.13-0.42-0.2-0.9-0.2-1.44c0-1.06,0.29-1.89,0.86-2.47
c0.57-0.59,1.37-0.88,2.39-0.88c0.48,0,0.86,0.04,1.13,0.11c0.27,0.07,0.49,0.13,0.66,0.17l-0.21,0.93
c-0.16-0.05-0.36-0.1-0.6-0.16s-0.51-0.08-0.84-0.08c-0.69,0-1.23,0.18-1.63,0.55s-0.6,0.94-0.6,1.72c0,0.89,0.21,1.54,0.63,1.96
c0.42,0.42,1.03,0.62,1.81,0.62c0.26,0,0.51-0.03,0.75-0.08c0.24-0.05,0.43-0.11,0.59-0.18L81.73,42.34z"/>
<path d="M85.81,42.57h-1.09v-5.62h-2.29v-0.98h5.67v0.98h-2.29V42.57z"/>
<path d="M91.94,42.67c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C92.87,42.6,92.43,42.67,91.94,42.67z M91.94,41.69
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C90.87,41.5,91.34,41.69,91.94,41.69z"/>
<path d="M96.73,42.57v-6.61h1.09v4.21l-0.04,0.87h0.01l0.43-0.69l3.08-4.39h1.04v6.61h-1.09v-4.31l0.04-0.76h-0.01l-0.4,0.65
l-3.11,4.43H96.73z M101.63,33.11c-0.02,0.29-0.08,0.54-0.19,0.75s-0.26,0.39-0.44,0.54c-0.18,0.14-0.4,0.25-0.64,0.32
c-0.25,0.07-0.5,0.1-0.77,0.1c-0.28,0-0.54-0.04-0.78-0.1c-0.24-0.07-0.45-0.17-0.63-0.32c-0.18-0.14-0.33-0.32-0.45-0.54
c-0.12-0.22-0.18-0.47-0.19-0.75l0.92-0.1c0.04,0.31,0.17,0.54,0.37,0.7c0.2,0.16,0.46,0.23,0.76,0.23s0.55-0.08,0.76-0.23
c0.2-0.16,0.33-0.39,0.37-0.7L101.63,33.11z"/>
<path d="M109.2,42.57h-1.09v-2.6c-0.04,0.01-0.12,0.03-0.22,0.05c-0.1,0.03-0.23,0.05-0.38,0.08s-0.31,0.06-0.5,0.08
c-0.19,0.02-0.38,0.03-0.58,0.03c-0.35,0-0.68-0.03-0.98-0.1c-0.3-0.07-0.56-0.18-0.77-0.34c-0.22-0.16-0.38-0.36-0.51-0.6
c-0.12-0.25-0.18-0.55-0.18-0.9v-2.31h1.09v1.94c0,0.27,0.04,0.49,0.12,0.65c0.08,0.17,0.19,0.3,0.33,0.4
c0.14,0.1,0.31,0.17,0.52,0.21c0.2,0.04,0.43,0.06,0.67,0.06c0.33,0,0.61-0.03,0.85-0.08c0.24-0.05,0.42-0.1,0.54-0.14v-3.04h1.09
V42.57z"/>
<path d="M111.27,42.57v-6.61h1.09v4.21l-0.04,0.87h0.01l0.43-0.69l3.08-4.39h1.04v6.61h-1.09v-4.31l0.04-0.76h-0.01l-0.4,0.65
l-3.11,4.43H111.27z"/>
<path d="M118.95,42.57v-6.61h2.47c0.81,0,1.43,0.12,1.85,0.36c0.42,0.24,0.63,0.68,0.63,1.31c0,0.36-0.11,0.68-0.32,0.95
c-0.22,0.27-0.5,0.46-0.86,0.55v0.04c0.42,0.07,0.77,0.23,1.04,0.49c0.27,0.25,0.41,0.61,0.41,1.06c0,0.36-0.06,0.66-0.18,0.9
c-0.12,0.24-0.29,0.43-0.51,0.57s-0.48,0.24-0.79,0.3c-0.31,0.06-0.64,0.08-1,0.08H118.95z M120.04,38.68h1.35
c0.16,0,0.32-0.01,0.49-0.02s0.32-0.05,0.46-0.11c0.14-0.06,0.25-0.15,0.34-0.27c0.09-0.12,0.14-0.28,0.14-0.49
c0-0.18-0.03-0.33-0.09-0.44c-0.06-0.11-0.15-0.2-0.27-0.26c-0.12-0.06-0.25-0.1-0.41-0.12c-0.16-0.02-0.33-0.03-0.52-0.03h-1.48
V38.68z M120.04,41.59h1.67c0.41,0,0.73-0.07,0.96-0.2c0.23-0.13,0.35-0.39,0.35-0.76c0-0.23-0.05-0.42-0.15-0.55
c-0.1-0.13-0.23-0.24-0.38-0.31c-0.16-0.07-0.33-0.12-0.51-0.14s-0.37-0.03-0.55-0.03h-1.39V41.59z"/>
<path d="M128.57,42.67c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C129.5,42.6,129.06,42.67,128.57,42.67z M128.57,41.69
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C127.5,41.5,127.97,41.69,128.57,41.69z"/>
<path d="M134.45,42.57h-1.09v-6.61h4.34v0.98h-3.25V42.57z"/>
<path d="M141.43,42.67c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C142.36,42.6,141.92,42.67,141.43,42.67z M141.43,41.69
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C140.36,41.5,140.83,41.69,141.43,41.69z"/>
<path d="M152.3,42.65c-0.09,0.02-0.17,0.03-0.25,0.03s-0.16,0-0.24,0c-0.99,0-1.73-0.29-2.22-0.86c-0.49-0.57-0.73-1.43-0.73-2.56
c0-1.08,0.25-1.92,0.75-2.51c0.5-0.59,1.24-0.89,2.22-0.89c0.08,0,0.16,0,0.24,0c0.08,0,0.16,0.01,0.24,0.03v-3.03h1.09v3.04
c0.16-0.03,0.33-0.04,0.49-0.04c1.01,0,1.76,0.28,2.23,0.83c0.48,0.55,0.72,1.39,0.72,2.53c0,0.54-0.06,1.02-0.19,1.44
c-0.13,0.43-0.31,0.79-0.56,1.09c-0.25,0.3-0.56,0.53-0.93,0.69c-0.37,0.16-0.8,0.24-1.28,0.24h-0.48v2.62h-1.09V42.65z
M152.3,36.83c-0.12-0.03-0.28-0.04-0.47-0.04c-0.28,0-0.54,0.04-0.76,0.14s-0.41,0.24-0.57,0.43c-0.16,0.2-0.27,0.46-0.36,0.77
c-0.08,0.32-0.12,0.7-0.12,1.16c0,0.85,0.16,1.48,0.47,1.87s0.8,0.58,1.44,0.58c0.06,0,0.12,0,0.17,0s0.12,0,0.19-0.01V36.83z
M153.39,41.74h0.47c0.57,0,1.01-0.2,1.33-0.6c0.32-0.4,0.48-1.05,0.48-1.97c0-0.85-0.15-1.47-0.46-1.83s-0.8-0.55-1.47-0.55
c-0.05,0-0.11,0-0.17,0.01c-0.06,0-0.12,0.01-0.18,0.02V41.74z"/>
<path d="M159.04,44.64c0.22-0.19,0.46-0.43,0.69-0.73c0.24-0.3,0.41-0.62,0.52-0.95l0.05-0.18l-2.71-6.81h1.18l2.1,5.44l1.76-5.44
h1.1l-2.42,7.01c-0.08,0.24-0.18,0.48-0.3,0.73c-0.12,0.24-0.25,0.47-0.38,0.68c-0.13,0.21-0.28,0.4-0.43,0.57
c-0.15,0.17-0.3,0.3-0.43,0.41L159.04,44.64z"/>
<path d="M170.48,42.57h-1.09v-2.94h-3.39v2.94h-1.09v-6.61h1.09v2.68h3.39v-2.68h1.09V42.57z"/>
<path d="M173.64,38.87h0.35l2.5-2.9h1.4l-2.81,3.1c0.1,0.05,0.19,0.11,0.27,0.19c0.07,0.07,0.15,0.16,0.24,0.27l2.63,3.06h-1.48
l-2-2.45c-0.13-0.16-0.24-0.28-0.32-0.34s-0.21-0.09-0.36-0.09h-0.42v2.88h-1.09v-6.61h1.09V38.87z"/>
<path d="M185.65,44.44h-1.04v-1.87h-5.31v-6.61h1.09v5.62h3.17v-5.62h1.09v5.62h1V44.44z"/>
<path d="M187.07,42.57v-6.61h1.09v4.21l-0.04,0.87h0.01l0.43-0.69l3.08-4.39h1.04v6.61h-1.09v-4.31l0.04-0.76h-0.01l-0.4,0.65
l-3.11,4.43H187.07z"/>
<path d="M197.48,42.67c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C198.41,42.6,197.97,42.67,197.48,42.67z M197.48,41.69
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C196.41,41.5,196.88,41.69,197.48,41.69z"/>
<path d="M207.84,42.57h-1.09v-2.94h-3.39v2.94h-1.09v-6.61h1.09v2.68h3.39v-2.68h1.09V42.57z"/>
<path d="M209.91,42.57v-6.61H211v4.21l-0.04,0.87h0.01l0.43-0.69l3.08-4.39h1.04v6.61h-1.09v-4.31l0.04-0.76h-0.01l-0.4,0.65
l-3.11,4.43H209.91z"/>
<path d="M218.68,45.29h-1.09v-9.33h1.04v0.95h0.03c0.16-0.23,0.35-0.42,0.55-0.56c0.2-0.14,0.41-0.25,0.61-0.32
c0.2-0.07,0.4-0.11,0.58-0.14c0.18-0.02,0.33-0.03,0.45-0.03c0.52,0,0.96,0.08,1.33,0.24c0.37,0.16,0.67,0.38,0.9,0.67
c0.23,0.29,0.4,0.64,0.51,1.06c0.11,0.42,0.16,0.88,0.16,1.39c0,0.48-0.06,0.93-0.17,1.35s-0.29,0.78-0.52,1.09
c-0.23,0.31-0.53,0.56-0.89,0.74c-0.36,0.18-0.78,0.27-1.26,0.27c-0.21,0-0.41-0.02-0.62-0.05c-0.21-0.03-0.41-0.09-0.6-0.17
c-0.19-0.08-0.38-0.18-0.54-0.3c-0.17-0.12-0.31-0.27-0.43-0.45h-0.03V45.29z M220.67,41.69c0.68,0,1.17-0.22,1.47-0.65
c0.3-0.44,0.45-1.06,0.45-1.86c0-0.37-0.03-0.7-0.1-1c-0.06-0.29-0.17-0.54-0.32-0.73c-0.15-0.19-0.35-0.34-0.6-0.45
s-0.56-0.16-0.93-0.16c-0.64,0-1.12,0.22-1.46,0.67s-0.5,1.04-0.5,1.76c0,0.73,0.17,1.32,0.5,1.76S220.01,41.69,220.67,41.69z"/>
<path d="M228.16,42.67c-1.04,0-1.81-0.29-2.33-0.87c-0.52-0.58-0.78-1.43-0.78-2.53c0-1.08,0.27-1.92,0.8-2.51
c0.53-0.6,1.3-0.89,2.31-0.89c1.04,0,1.81,0.29,2.33,0.87c0.52,0.58,0.78,1.42,0.78,2.54c0,0.53-0.07,1-0.21,1.42
c-0.14,0.42-0.34,0.78-0.6,1.08c-0.26,0.29-0.59,0.52-0.98,0.67C229.09,42.6,228.65,42.67,228.16,42.67z M228.16,41.69
c0.65,0,1.13-0.2,1.46-0.59c0.32-0.39,0.49-1,0.49-1.83c0-0.82-0.15-1.43-0.45-1.83c-0.3-0.4-0.8-0.6-1.5-0.6
c-0.62,0-1.1,0.2-1.44,0.6c-0.34,0.4-0.5,1.01-0.5,1.83c0,0.84,0.17,1.45,0.52,1.84C227.09,41.5,227.56,41.69,228.16,41.69z"/>
<path d="M232.95,42.57v-6.61h2.47c0.81,0,1.43,0.12,1.85,0.36c0.42,0.24,0.63,0.68,0.63,1.31c0,0.36-0.11,0.68-0.32,0.95
c-0.22,0.27-0.5,0.46-0.86,0.55v0.04c0.42,0.07,0.77,0.23,1.04,0.49c0.27,0.25,0.41,0.61,0.41,1.06c0,0.36-0.06,0.66-0.18,0.9
c-0.12,0.24-0.29,0.43-0.51,0.57s-0.48,0.24-0.79,0.3c-0.31,0.06-0.64,0.08-1,0.08H232.95z M234.04,38.68h1.35
c0.16,0,0.32-0.01,0.49-0.02s0.32-0.05,0.46-0.11c0.14-0.06,0.25-0.15,0.34-0.27c0.09-0.12,0.14-0.28,0.14-0.49
c0-0.18-0.03-0.33-0.09-0.44c-0.06-0.11-0.15-0.2-0.27-0.26c-0.12-0.06-0.25-0.1-0.41-0.12c-0.16-0.02-0.33-0.03-0.52-0.03h-1.48
V38.68z M234.04,41.59h1.67c0.41,0,0.73-0.07,0.96-0.2c0.23-0.13,0.35-0.39,0.35-0.76c0-0.23-0.05-0.42-0.15-0.55
c-0.1-0.13-0.23-0.24-0.38-0.31c-0.16-0.07-0.33-0.12-0.51-0.14s-0.37-0.03-0.55-0.03h-1.39V41.59z"/>
<path d="M244.54,42.57h-0.96v-1.21h-0.03c-0.07,0.18-0.17,0.35-0.32,0.51c-0.14,0.16-0.31,0.3-0.5,0.42
c-0.19,0.12-0.4,0.21-0.63,0.28s-0.48,0.1-0.73,0.1c-0.3,0-0.58-0.05-0.83-0.14s-0.46-0.23-0.64-0.39
c-0.18-0.17-0.32-0.37-0.41-0.59c-0.1-0.22-0.15-0.47-0.15-0.73c0-0.44,0.12-0.8,0.35-1.08c0.23-0.28,0.54-0.49,0.93-0.65
s0.82-0.27,1.31-0.34s1-0.11,1.52-0.14v-0.34c0-0.48-0.11-0.84-0.34-1.08s-0.63-0.36-1.2-0.36c-0.32,0-0.66,0.04-1.02,0.12
s-0.67,0.19-0.93,0.32l-0.19-0.89c0.27-0.15,0.6-0.27,1.01-0.38c0.41-0.1,0.82-0.16,1.26-0.16c0.49,0,0.9,0.06,1.22,0.17
c0.32,0.12,0.58,0.29,0.77,0.51s0.32,0.5,0.4,0.84c0.08,0.34,0.12,0.73,0.12,1.17V42.57z M243.46,39.46
c-0.37,0.02-0.73,0.04-1.08,0.08c-0.35,0.03-0.66,0.1-0.94,0.19s-0.5,0.22-0.67,0.38c-0.17,0.16-0.25,0.37-0.25,0.63
c0,0.27,0.09,0.5,0.28,0.68c0.19,0.19,0.48,0.28,0.89,0.28c0.52,0,0.94-0.16,1.28-0.48s0.5-0.79,0.5-1.41V39.46z"/>
<path d="M252.12,42.57h-1.09v-2.94h-3.39v2.94h-1.09v-6.61h1.09v2.68h3.39v-2.68h1.09V42.57z"/>
<path d="M254.2,42.57v-6.61h1.09v4.21l-0.04,0.87h0.01l0.43-0.69l3.08-4.39h1.04v6.61h-1.09v-4.31l0.04-0.76h-0.01l-0.4,0.65
l-3.11,4.43H254.2z"/>
<path d="M264.65,40.04c-0.22,0-0.39,0.04-0.52,0.12c-0.13,0.08-0.25,0.21-0.36,0.39l-1.1,2.02h-1.31l1.19-2.09
c0.1-0.18,0.21-0.32,0.32-0.41c0.11-0.1,0.22-0.16,0.31-0.21c-0.47-0.09-0.84-0.29-1.13-0.59c-0.29-0.3-0.43-0.71-0.43-1.25
c0-0.34,0.07-0.65,0.2-0.91c0.13-0.26,0.31-0.47,0.54-0.64s0.49-0.3,0.79-0.38c0.3-0.09,0.62-0.13,0.96-0.13h2.67v6.61h-1.09
v-2.53H264.65z M265.7,39.06v-2.11h-1.46c-0.48,0-0.84,0.08-1.08,0.25c-0.24,0.17-0.36,0.45-0.36,0.85c0,0.38,0.15,0.64,0.44,0.79
c0.29,0.15,0.67,0.22,1.11,0.22H265.7z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -83,7 +83,7 @@ button:focus-visible {
/* Фон скроллбара */ /* Фон скроллбара */
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--scrollbar-track-color, #f1f1f1); background: var(--scrollbar-track-color, #025EA1);
/* Цвет фона */ /* Цвет фона */
border-radius: 10px; border-radius: 10px;
/* Скругление углов */ /* Скругление углов */
@ -91,7 +91,7 @@ button:focus-visible {
/* Ползунок */ /* Ползунок */
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #3d74c7; background: #D4EFFC;
/* Основной цвет */ /* Основной цвет */
border-radius: 10px; border-radius: 10px;
/* Скругляем края */ /* Скругляем края */

View File

@ -1,11 +1,15 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
react(),
svgr()
],
server: { server: {
host: true, host: true,
allowedHosts: ['dev.msf.enode'] allowedHosts: ['dev.msf.enode', 'demo-msf.kis-npo.ru']
} }
}) })