Compare commits

..

No commits in common. "e47161acd1097f035c5e10084923f0269702c523" and "175b4f993d4deda47dba91df91d83e4d168a5dff" have entirely different histories.

54 changed files with 3157 additions and 4195 deletions

40
Jenkinsfile vendored
View File

@ -30,23 +30,7 @@ pipeline {
stage ('Initialize variables') {
steps {
script {
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
env.IMAGE_TAG = sh(script: "git describe --tags --abbrev=0", returnStdout: true).trim()
}
}
}
@ -87,29 +71,17 @@ pipeline {
echo "Attempting to merge PR ${env.CHANGE_ID} into master..."
withCredentials([usernamePassword(credentialsId: 'gitea_creds', usernameVariable: 'GITEA_USER', passwordVariable: 'GITEA_PASS')]) {
def prId = env.CHANGE_ID
sh """
curl -X POST \
-u "${GITEA_USER}:${GITEA_PASS}" \
-H "Content-Type: application/json" \
-d '{"do":"merge"}' \
http://git.entcor/api/v1/repos/deployer3000/${env.IMAGE_NAME}/pulls/${prId}/merge
http://git.entcor/api/v1/repos/deployer3000/trust-module-frontend/pulls/${prId}/merge
"""
def commitHash = sh(script: "git rev-parse HEAD~1", returnStdout: true).trim() // необходим для корректного отображения статусов
echo "PR ${prId} merged successfully into main!"
sleep(time: 15, unit: 'SECONDS')
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")
echo "PR ${prId} merged successfully into master!"
def context = "test-org/trust-module-frontend/pipeline/pr-${env.CHANGE_TARGET}"
def commitHash = sh(script: "git rev-parse HEAD~1", returnStdout: true).trim()
notify(context, GITEA_USER, GITEA_PASS, env.GITEA_REPOSITORY_URL, "trust-module-frontend", commitHash, "success")
}
}
}

569
package-lock.json generated
View File

@ -8,10 +8,6 @@
"name": "trust-module",
"version": "0.0.0",
"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",
"chart.js": "^4.0.0",
"chartjs-adapter-date-fns": "^3.0.0",
@ -54,6 +50,7 @@
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
@ -109,6 +106,7 @@
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz",
"integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.26.5",
@ -142,6 +140,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.25.9",
@ -183,6 +182,7 @@
"version": "7.25.9",
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -192,6 +192,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@ -225,6 +226,7 @@
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.7"
@ -283,6 +285,7 @@
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.25.9",
@ -297,6 +300,7 @@
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz",
"integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.26.2",
@ -315,6 +319,7 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -324,6 +329,7 @@
"version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.25.9",
@ -333,144 +339,6 @@
"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": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
@ -1155,6 +1023,7 @@
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
@ -1169,6 +1038,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -1178,6 +1048,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -1187,12 +1058,14 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -1204,247 +1077,6 @@
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"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": {
"version": "4.32.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz",
@ -1829,21 +1461,18 @@
"dev": true,
"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": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@ -1860,14 +1489,6 @@
"@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": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz",
@ -2128,39 +1749,6 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2265,6 +1853,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -2437,29 +2026,6 @@
"dev": true,
"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": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2918,6 +2484,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -3038,14 +2605,6 @@
"dev": true,
"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": {
"version": "1.23.9",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz",
@ -3268,6 +2827,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@ -3533,11 +3093,6 @@
"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": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@ -3891,14 +3446,6 @@
"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": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -3924,6 +3471,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@ -3987,11 +3535,6 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@ -4062,6 +3605,7 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@ -4396,6 +3940,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@ -4411,11 +3956,6 @@
"dev": true,
"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": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -4483,11 +4023,6 @@
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -4591,6 +4126,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@ -4804,6 +4340,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@ -4812,23 +4349,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": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4853,20 +4373,14 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/possible-typed-array-names": {
@ -5143,6 +4657,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@ -5424,14 +4939,6 @@
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -5553,11 +5060,6 @@
"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": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -5575,6 +5077,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -5958,20 +5461,6 @@
"dev": true,
"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": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -23,12 +23,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7",
"@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"
"@mui/icons-material": "^6.4.8"
},
"devDependencies": {
"@eslint/js": "^9.17.0",

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

@ -1,11 +1,15 @@
<svg width="43" height="43" viewBox="0 0 43 43" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
<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"/>
<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)"/>
<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)">
<stop stop-color="#4A96D2"/>
<stop offset="1" stop-color="#1F2466"/>
</radialGradient>
</defs>
</svg>
<svg
width="100" height="100"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
fill="none" stroke="black" stroke-width="5" stroke-linecap="round" stroke-linejoin="round">
<!-- Окружность -->
<circle cx="50" cy="50" r="45" stroke="#4CAF50" stroke-width="5" fill="none" />
<!-- График нагрузки -->
<polyline points="20,70 35,40 50,60 65,30 80,50" stroke="#4CAF50" stroke-width="5" fill="none" />
<!-- Крестик в центре, символизирующий мониторинг -->
<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: 1.8 KiB

After

Width:  |  Height:  |  Size: 729 B

View File

@ -1,137 +1,39 @@
import React, { useState, useMemo, useEffect } from "react";
import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress, Typography } from "@mui/material";
import React, { useState, useMemo } from "react";
import { ThemeProvider, CssBaseline, Switch, Box } from "@mui/material";
import Dashboard from "./Components/Layout/Dashboard";
import LoginModal from "./Components/UI/LoginModal";
import { lightTheme, darkTheme } from "./Style/theme";
import Logo from './assets/images/logo.svg?react';
import { checkAuth } from "./Components/UI/auth";
import "./Style/LoginModal.css";
function App() {
const [authState, setAuthState] = useState({
isAuthenticated: false,
isLoading: true,
user: null
});
const [showLoginModal, setShowLoginModal] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [showLoginModal, setShowLoginModal] = useState(true);
const [isDarkMode, setIsDarkMode] = useState(
window.matchMedia("(prefers-color-scheme: dark)").matches
);
const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]);
useEffect(() => {
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
});
const handleLogin = () => {
setIsAuthenticated(true);
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 (
<ThemeProvider theme={theme}>
<CssBaseline />
{!authState.isAuthenticated ? (
<>
<Box sx={{
position: "fixed",
top: 24,
left: "50%",
transform: "translateX(-50%)",
zIndex: 1200,
'& svg': { width: 400, height: 'auto' }
}}>
<Logo />
</Box>
<LoginModal
open={showLoginModal}
onLogin={handleLogin}
onClose={() => setShowLoginModal(false)}
/>
</>
{!isAuthenticated && showLoginModal ? (
<LoginModal 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 sx={{ display: "flex", height: "100vh", overflow: "hidden", bgcolor: "background.default", color: "text.primary" }}>
<Dashboard />
<Box sx={{ position: "absolute", top: 10, right: 10 }}>
<Switch checked={isDarkMode} onChange={() => setIsDarkMode((prev) => !prev)} />
</Box>
</Box>
)}
</ThemeProvider>
);
}
export default App;
export default App;

View File

@ -1,20 +0,0 @@
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

@ -1,17 +0,0 @@
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,229 +1,90 @@
import React, { useState, useRef, useEffect } from 'react';
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' // Для коротких диапазонов
};
import React, { useState } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line, ResponsiveContainer } from 'recharts';
const LineChartComponent = ({
chartData,
metricName,
colors,
onRangeSelect,
filteredData
}) => {
const [selectionArea, setSelectionArea] = useState(null);
const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null);
const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => {
const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null);
const allTimestamps = Object.values(chartData)
// Создаем массив уникальных временных меток
const allTimes = Object.values(chartData)
.flat()
.map(point => point.timestamp)
.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;
}
.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.timestamp === timestamp);
const instanceData = chartData[key].find(p => p.time === time);
point[key] = instanceData ? instanceData.value : null;
});
return point;
});
// Используем отфильтрованные данные, если они есть
const displayData = filteredData || data;
const instanceKeys = displayData.length
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
: [];
// Обработчик клика на графике
const handleClick = (e) => {
if (!e || !e.activeLabel) return;
// Функция для определения оптимального формата времени в зависимости от диапазона
const getTimeFormat = () => {
if (!data.length) return TIME_FORMATS.SHORT;
const clickedTime = e.activeLabel;
const range = data[data.length - 1].timestamp - data[0].timestamp;
if (!selectionStart) {
setSelectionStart(clickedTime);
} else if (!selectionEnd) {
setSelectionEnd(clickedTime);
if (range > DAY) return TIME_FORMATS.LONG;
if (range > HOUR) return TIME_FORMATS.MEDIUM;
return TIME_FORMATS.SHORT;
};
const startIndex = data.findIndex(point => point.time === selectionStart);
const endIndex = data.findIndex(point => point.time === clickedTime);
useEffect(() => {
const handleSelectStart = (e) => {
if (isSelecting) {
e.preventDefault();
}
};
onRangeSelect({ startIndex, endIndex });
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;
setSelectionStart(null);
setSelectionEnd(null);
}
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 }) => {
if (active && payload && payload.length) {
const currentPoint = data.find(point => point.timestamp === label);
return (
<div style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc',
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}`}
<div className="custom-tooltip" style={{ padding: '10px' }}>
<p>{`Время: ${label}`}</p>
{payload.map((entry, index) => (
<p key={index} style={{}}>
{`Значение: ${entry.value}`}
</p>
))}
</div>
);
}
return null;
};
if (!data.length) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
}
return (
<div style={{ position: 'relative', height: '400px' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={displayData}
onMouseDown={handleMouseDown}
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 }} />
<div>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={displayData} onClick={handleClick}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
{instanceKeys.map((instance, index) => (
<Legend />
{Object.keys(chartData).map((key, index) => (
<Line
key={instance}
type=""
dataKey={instance}
name={instance}
key={key}
type="monotone"
dataKey={key}
stroke={colors[index % colors.length]}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
name={key}
/>
))}
{selectionArea?.start && selectionArea?.end && (
<ReferenceArea
x1={selectionArea.start}
x2={selectionArea.end}
strokeOpacity={0.3}
fill="#4a6baf"
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default React.memo(LineChartComponent);
export default LineChartComponent;

View File

@ -1,151 +0,0 @@
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

@ -1,35 +0,0 @@
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

@ -0,0 +1,27 @@
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,486 +1,267 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { webSocketManager } from './WebSocketManager';
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 React, { useEffect, useState, useRef } from 'react';
import axios from 'axios';
import Skeleton from '@mui/material/Skeleton';
import Box from '@mui/material/Box';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import LineChartComponent from './Components/LineChartComponent';
// Компонент Skeleton для графика
const ChartSkeleton = () => (
<Box sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative'
}}>
<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>
);
const MAX_POINTS = 20;
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850'];
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 },
];
const PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState(null);
const [chartData, setChartData] = useState({});
const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]);
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date());
const [useCustomRange, setUseCustomRange] = useState(false);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const [selectedGraphRange, setSelectedGraphRange] = useState(null);
const [filteredData, setFilteredData] = useState(null);
const [isSelectingRange, setIsSelectingRange] = useState(false);
const [lastCustomRange, setLastCustomRange] = useState(null);
const [selectedGraphRange, setSelectedGraphRange] = useState(null); // Выбранный диапазон
const [filteredData, setFilteredData] = useState(null); // Отфильтрованные данные
const intervalRef = useRef(null);
const debounceRef = useRef(null);
const formatTime = useCallback((timestamp, rangeSeconds) => {
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
const date = new Date(ts);
// Функция для интерполяции данных
const interpolateData = (data, minPoints = 15) => {
if (data.length >= minPoints) return data;
// Определяем формат в зависимости от диапазона
const showFullDate = rangeSeconds > 86400; // больше суток
const interpolatedData = [];
for (let i = 0; i < data.length - 1; i++) {
interpolatedData.push(data[i]);
const timeOptions = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
};
const currentPoint = data[i];
const nextPoint = data[i + 1];
const dateOptions = showFullDate ? {
month: '2-digit',
day: '2-digit',
...timeOptions
} : timeOptions;
// Вычисляем разницу во времени между точками
const currentTime = new Date(currentPoint.time).getTime();
const nextTime = new Date(nextPoint.time).getTime();
const timeDiff = nextTime - currentTime;
return {
display: date.toLocaleString('ru-RU', dateOptions),
fullDisplay: date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}),
timestamp: ts
};
}, []);
// Добавляем промежуточные точки
const steps = Math.ceil((minPoints - data.length) / (data.length - 1));
for (let j = 1; j <= steps; j++) {
const interpolatedTime = new Date(currentTime + (timeDiff * j) / (steps + 1)).toLocaleString();
const interpolatedPoint = { time: interpolatedTime };
const calculateStep = useCallback((start, end) => {
const range = end - start;
if (range <= MINUTE) return 1; // 1 мин
if (range <= MINUTE * 5) return 5; // 5 мин
if (range <= HOUR / 2) return 15; // 30 мин
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
// Интерполируем значения для каждой метрики
Object.keys(currentPoint).forEach(key => {
if (key !== 'time') {
const currentValue = currentPoint[key];
const nextValue = nextPoint[key];
interpolatedPoint[key] = currentValue + ((nextValue - currentValue) * j) / (steps + 1);
}
});
});
// Сортируем и ограничиваем данные
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;
}
const start = Math.floor(startDate.getTime() / 1000);
const end = Math.ceil(endDate.getTime() / 1000); // Используем Math.ceil для конечной даты
const rangeSeconds = end - start;
try {
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
params: {
metric: metricName,
start,
end,
step: calculateStep(start, end)
}
});
if (response.data?.length) {
// Добавляем нормализацию timestamp
const processedData = response.data.map(item => ({
...item,
timestamp: item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000,
value: parseFloat(item.value)
}));
processMetricsData({
metric: metricName,
data: processedData
});
interpolatedData.push(interpolatedPoint);
}
}
interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку
return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints
};
const fetchData = async () => {
try {
let start, end;
if (useCustomRange) {
start = Math.floor(startDate.getTime() / 1000);
end = Math.floor(endDate.getTime() / 1000);
} else {
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 },
});
/*
const response = await axios.get(`${process.env.REACT_APP_BACK_URL}/metrics`, {
params: { metric: metricName, start, end, step },
}); */
const result = response.data;
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,
}));
});
setChartData(chartData);
} catch (error) {
console.error('Ошибка при получении кастомных данных:', error);
console.error('Ошибка при загрузке метрик:', error);
}
}, [metricName, startDate, endDate, calculateStep, processMetricsData]);
};
useEffect(() => {
fetchData();
const handleRangeChange = useCallback(async (event) => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
intervalRef.current = setInterval(() => {
fetchData();
}, selectedRange.interval);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [metricName, selectedRange, useCustomRange, startDate, endDate]);
const handleRangeChange = (event) => {
const selectedValue = event.target.value;
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
// Полный сброс состояния перед загрузкой новых данных
setChartData(null);
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
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;
}
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные
};
const handleCustomRangeChange = () => {
setUseCustomRange(true);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
fetchCustomRangeData();
}, [fetchCustomRangeData]);
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
setFilteredData(null); // Сбрасываем отфильтрованные данные
};
useEffect(() => {
if (selectedGraphRange) {
const { startIndex, endIndex } = selectedGraphRange;
const allTimes = Object.values(chartData)
.flat()
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index);
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];
}
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;
});
interpolated.push(interpolatedPoint);
}
const filtered = data.slice(startIndex, endIndex + 1);
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();
// Интерполируем данные, если точек меньше 15
const interpolated = interpolateData(filtered, 15);
setFilteredData(interpolated); // Сохраняем интерполированные данные
} else {
fetchData();
setFilteredData(null); // Сбрасываем фильтрацию, если диапазон не выбран
}
}, [selectedGraphRange, chartData]);
if (lastCustomRange) {
handleRangeSelect(lastCustomRange);
}
}, [fetchData, fetchCustomRangeData, useCustomRange, lastCustomRange, handleRangeSelect]);
if (!Object.keys(chartData).length) return <p>Loading...</p>;
useEffect(() => {
// Обработчик данных с сервера
const handleMetricsData = (data) => {
processMetricsData({ metric: metricName, data });
};
const allTimes = Object.values(chartData)
.flat()
.map(point => point.time)
.filter((time, index, self) => self.indexOf(time) === index);
// Подписываемся на обновления метрики
const unsubscribe = webSocketManager.subscribe(metricName, handleMetricsData);
// Подписываемся на изменения статуса соединения
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 () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]);
useEffect(() => {
if (!selectedGraphRange || !chartData) {
setFilteredData(null);
return;
}
const allPoints = Object.values(chartData).flat();
const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1));
const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1));
const filtered = sortedPoints.slice(startIndex, endIndex + 1);
const interpolated = filtered.length > 100 ?
interpolateData(filtered, 100) :
filtered;
setFilteredData(interpolated);
}, [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>
);
}
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;
});
return (
<div style={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative'
}}>
<ConnectionStatusIndicator connectionStatus={connectionStatus} />
<TimeRangeSelector
selectedRange={selectedRange}
handleRangeChange={handleRangeChange}
startDate={startDate}
setStartDate={setStartDate}
endDate={endDate}
setEndDate={setEndDate}
useCustomRange={useCustomRange}
handleCustomRangeChange={handleCustomRangeChange}
handleResetZoom={handleResetZoom}
/>
<CurrentRangeDisplay
useCustomRange={useCustomRange}
startDate={startDate}
endDate={endDate}
selectedRange={selectedRange}
/>
<div>
<div>
<label htmlFor="time-range">Выберите временной диапазон: </label>
<select id="time-range" value={selectedRange.value} onChange={handleRangeChange}>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
<div>
<label>Или выберите другой диапазон: </label>
<div>
<label>Начальная дата: </label>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
/>
</div>
<div>
<label>Конечная дата: </label>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
/>
</div>
<button onClick={handleCustomRangeChange}>Использовать кастомный диапазон</button>
</div>
<LineChartComponent
chartData={chartData}
metricName={metricName}
colors={COLORS}
onRangeSelect={handleRangeSelect}
description={metricName}
onRangeSelect={setSelectedGraphRange}
filteredData={filteredData}
/>
</div>
);
};
export default React.memo(PrometheusChart);
export default PrometheusChart;

View File

@ -5,22 +5,6 @@ const SystemStatusChart = ({ data }) => {
// Обрезаем массив, оставляя только последние 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 (
<ResponsiveContainer width="100%" height={300}>
<LineChart
@ -32,7 +16,8 @@ const SystemStatusChart = ({ data }) => {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis domain={[0, 100]} />
<Tooltip content={<CustomTooltip />} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>
</ResponsiveContainer>

View File

@ -0,0 +1,84 @@
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

@ -0,0 +1,84 @@
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

@ -1,115 +0,0 @@
// 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 { Box, styled } from "@mui/material";
import SidebarMenu from "./SidebarMenu";
import "../../Style/Dashboard.css";
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
import generateTabContent from "../TreeChart/tabContent";
import CustomTabs from "../UI/MUItabs";
@ -9,70 +9,24 @@ import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent";
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 { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
const { sidebarWidth, startResizing } = useSidebarResize(290);
const { sidebarWidth, startResizing } = useSidebarResize(250);
const [tabContent, setTabContent] = useState({});
const [treeData1, setTreeData1] = useState(menuData);
const [treeData2, setTreeData2] = useState(menuData);
const [collapsed, setCollapsed] = useState(false);
const [statusHistories, setStatusHistories] = useState({
history1: [],
history2: [],
});
// Генерация контента для вкладок
useEffect(() => {
const generatedTabContent = generateTabContent(menuData);
setTabContent(generatedTabContent);
}, []);
// Обновление статусов каждые 30 секунд
useEffect(() => {
const interval = setInterval(() => {
const updatedData1 = JSON.parse(JSON.stringify(treeData1));
@ -102,22 +56,15 @@ const Dashboard = () => {
}, [treeData1, treeData2]);
return (
<DashboardContainer>
<div className="dashboard-container">
{/* Сайдбар */}
<SidebarWrapper sx={{ width: collapsed ? 64 : sidebarWidth }}>
<SidebarMenu
data={treeData1}
onOpenTab={handleOpenTab}
sidebarWidth={sidebarWidth}
startResizing={startResizing}
collapsed={collapsed}
setCollapsed={setCollapsed}
/>
<SidebarResizer onMouseDown={startResizing} />
</SidebarWrapper>
<div className="sidebar" style={{ width: sidebarWidth }}>
<SidebarMenu data={treeData1} onOpenTab={handleOpenTab} sidebarWidth={sidebarWidth} startResizing={startResizing} />
<div className="sidebar-resizer" onMouseDown={startResizing} />
</div>
{/* Основной контент */}
<MainContent>
<div className="main-content">
{/* Вкладки */}
<CustomTabs
tabs={tabs}
@ -127,7 +74,7 @@ const Dashboard = () => {
/>
{/* Контент вкладки */}
<Content>
<div className="content">
<TabContent
activeTab={activeTab}
statusHistories={statusHistories}
@ -135,9 +82,9 @@ const Dashboard = () => {
tabContent={tabContent}
handleOpenTab={handleOpenTab}
/>
</Content>
</MainContent>
</DashboardContainer>
</div>
</div>
</div>
);
};

View File

@ -1,160 +1,49 @@
import React, { useState, useEffect } from "react";
import {
Drawer,
List,
Typography,
styled,
IconButton,
Tooltip,
Box
} from "@mui/material";
import {
ChevronLeft,
ChevronRight,
Menu as MenuIcon
} from "@mui/icons-material";
import React from "react";
import { Drawer, List } from "@mui/material";
import MenuItem from "./SidebarMenuComponents/MenuItem";
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) => {
onOpenTab(id, title, children);
};
const drawerWidth = collapsed ? 64 : sidebarWidth;
return (
<Box
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
<Drawer
variant="permanent"
sx={{
position: 'relative',
width: drawerWidth,
transition: 'width 0.3s ease',
width: sidebarWidth,
flexShrink: 0,
"& .MuiDrawer-paper": {
width: sidebarWidth,
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
},
}}
>
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: drawerWidth,
boxSizing: "border-box",
display: "flex",
flexDirection: "column",
backgroundColor: 'custom.sidebar',
color: 'custom.sidebarText',
transition: 'width 0.3s ease',
overflowX: 'hidden',
borderRight: 'none'
},
<List>
<h2 style={{ padding: "16px", fontWeight: "bold" }}>Меню</h2>
<MenuItem item={data} onSelectItem={handleSelectItem} />
</List>
{/* Ресайзер */}
<div
onMouseDown={startResizing}
style={{
width: "5px",
cursor: "ew-resize",
backgroundColor: "#ccc",
height: "100%",
position: "absolute",
right: 0,
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>
/>
{/* Содержимое меню */}
<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>
<SidebarFooter sidebarWidth={sidebarWidth} />
</Drawer>
);
};
export default SidebarMenu;
export default SidebarMenu;

View File

@ -1,108 +1,87 @@
import React from "react";
import {
ListItem,
ListItemIcon,
ListItemText,
Collapse,
List,
styled
} from "@mui/material";
import { Drawer, List, ListItem, ListItemIcon, ListItemText, Collapse } from "@mui/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 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 MenuItem = ({ item, onSelectItem }) => {
const [isOpen, setIsOpen] = React.useState(false);
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
const handleToggle = (e) => {
e.stopPropagation();
const handleToggle = () => {
setIsOpen(!isOpen);
};
const handleOpenTab = (e) => {
e.stopPropagation();
const allChildren = getAllChildren(item);
onSelectItem(item.id, item.title, allChildren);
e.stopPropagation(); // Останавливаем всплытие события
const allChildren = getAllChildren(item); // Собираем всех потомков
onSelectItem(item.id, item.title, allChildren); // Передаем данные в родительский компонент
};
return (
<>
<StyledListItem
component="div"
<ListItem
component="div"
onClick={hasChildren ? handleToggle : handleOpenTab}
level={level}
sx={{
pl: collapsed ? 2 : 2 + level * 2,
justifyContent: collapsed ? 'center' : 'flex-start',
cursor: "pointer", // Курсор pointer везде
"&:hover": {
backgroundColor: "#f5f5f5", // Подсветка при наведении на весь элемент
},
}}
>
{/* Индикатор статуса */}
{!collapsed && <StatusIndicator status={item.status} />}
<ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
<IconWrapper onClick={handleOpenTab}>
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
</IconWrapper>
</ListItemIcon>
{!collapsed && (
<>
<ListItemText
primary={item.title}
primaryTypographyProps={{
color: 'custom.sidebarText'
<ListItemIcon>
{hasChildren ? (
<div
onClick={handleOpenTab}
style={{
cursor: "pointer",
borderRadius: "4px", // Скругление углов
padding: "4px", // Отступы для увеличения области hover
"&:hover": {
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
},
}}
/>
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
</>
)}
</StyledListItem>
{hasChildren && !collapsed && (
>
{isOpen ? <FolderOpen /> : <Folder />}
</div>
) : (
<div
onClick={handleOpenTab}
style={{
cursor: "pointer",
borderRadius: "4px", // Скругление углов
padding: "4px", // Отступы для увеличения области hover
"&:hover": {
backgroundColor: "#e0e0e0", // Подсветка при наведении на иконку
},
}}
>
{/* Здесь можно добавить другую иконку или оставить пустым */}
</div>
)}
</ListItemIcon>
<ListItemText
primary={item.title}
sx={{ cursor: "pointer" }} // Курсор pointer для текста
/>
{hasChildren && (isOpen ? <ExpandLess /> : <ExpandMore />)}
</ListItem>
{hasChildren && (
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.items.map((child, index) => (
<MenuItem
key={index}
item={child}
onSelectItem={onSelectItem}
level={level + 1}
collapsed={collapsed}
/>
<MenuItem key={index} item={child} onSelectItem={onSelectItem} />
))}
</List>
</Collapse>
@ -111,15 +90,4 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
);
};
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;

View File

@ -1,47 +1,16 @@
import React from "react";
import {
List,
ListItem,
ListItemText,
styled
} from "@mui/material";
import { List, ListItem, ListItemText } from "@mui/material";
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 = () => {
const SidebarFooter = ({ sidebarWidth }) => {
return (
<FooterList>
<FooterListItem button>
<ListItemText
primary="Помощь"
primaryTypographyProps={{
color: 'custom.sidebarText',
variant: 'body2'
}}
/>
</FooterListItem>
<FooterListItem button>
<ListItemText
primary="Настройка"
primaryTypographyProps={{
color: 'custom.sidebarText',
variant: 'body2'
}}
/>
</FooterListItem>
</FooterList>
<List sx={{ marginTop: "auto", backgroundColor: "#ffffff", padding: "10px 0" }}>
<ListItem button={true}>
<ListItemText primary="Помощь" sx={{ color: "#000000" }} />
</ListItem>
<ListItem button={true}>
<ListItemText primary="Настройка" sx={{ color: "#000000" }} />
</ListItem>
</List>
);
};

View File

@ -1,25 +0,0 @@
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

@ -1,101 +0,0 @@
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

@ -1,112 +0,0 @@
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

@ -1,63 +0,0 @@
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

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

View File

@ -1,46 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -0,0 +1,189 @@
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

@ -0,0 +1,714 @@
{
"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,15 +1,14 @@
import React, { lazy, Suspense } from "react";
import Skeleton from '@mui/material/Skeleton';
import Box from '@mui/material/Box';
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
// Функция для генерации названия метрики на основе id
const getMetricName = (id) => {
return `zvks_apiforsnmp_measure_${id}`;
};
//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
// Функция для рекурсивного сбора всех id потомков
const getAllChildIds = (node) => {
let ids = [];
@ -24,32 +23,9 @@ const getAllChildIds = (node) => {
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 = {};
// Функция для рекурсивного обхода и сбора данных
// Функция для рекурсивного обхода и сбора данных
const generateContent = (nodes) => {
nodes.forEach((node) => {
@ -61,11 +37,8 @@ const tabContent = (data) => {
const content = (
<div>
<h2>{node.title}</h2>
<Suspense fallback={<ContainerSkeleton />}>
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
</Suspense>
<p>Контент для {node.title}.</p>
{/*childrenContent*/}
{childrenContent}
</div>
);
@ -80,7 +53,7 @@ const tabContent = (data) => {
const content = (
<div key={node.id}>
<h3>{node.title}</h3> {/* Используем title узла */}
<Suspense fallback={<ChartSkeleton />}>
<Suspense fallback={<div>Загрузка графика...</div>}>
<PrometheusChart metricName={metricName} />
</Suspense>
</div>

View File

@ -1,34 +0,0 @@
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

@ -0,0 +1,24 @@
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

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

View File

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

56
src/Components/UI/Tabs.jsx Executable file
View File

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

View File

@ -1,22 +0,0 @@
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

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

52
src/Style/Dashboard.css Executable file
View File

@ -0,0 +1,52 @@
/* Основной контейнер */
.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);
}

0
src/Style/DatePicker.css Executable file
View File

38
src/Style/ErrorIndicator.css Executable file
View File

@ -0,0 +1,38 @@
.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;
}

39
src/Style/Expandable.css Executable file
View File

@ -0,0 +1,39 @@
.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;
}

143
src/Style/SidebarMenu.css Executable file
View File

@ -0,0 +1,143 @@
/* Сайдбар */
.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;
}

62
src/Style/TreeTable.css Executable file
View File

@ -0,0 +1,62 @@
.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;
}

53
src/Style/common.css Executable file
View File

@ -0,0 +1,53 @@
/* Контейнер для вкладок */
.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);
}

25
src/Style/dark-theme.css Normal file
View File

@ -0,0 +1,25 @@
/* Темная тема, если пользователь предпочитает ее */
@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;
}
}

23
src/Style/light-theme.css Normal file
View File

@ -0,0 +1,23 @@
/* Светлая тема по умолчанию */
: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,191 +1,73 @@
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({
...commonThemeSettings, // Распаковываем общие настройки
// Цветовая палитра для светлой темы
palette: {
mode: "light", // Режим светлой темы
// Фоновые цвета
mode: "light",
background: {
default: "#FFFFFF", // Основной фон приложения
paper: "#FFFFFF", // Фон "бумажных" поверхностей (карточек, панелей)
default: "#FFFFFF",
paper: "#FFFFFF",
},
// Текстовые цвета
text: {
primary: "#000000", // Основной цвет текста
secondary: "#333333", // Вторичный цвет текста
primary: "#000000",
},
// Основные цвета UI
primary: {
main: "#3d74c7", // Основной брендовый цвет
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
main: "#3d74c7",
},
// Дополнительные цвета UI
secondary: {
main: "#0f55bec2", // Вторичный брендовый цвет
main: "#0f55bec2",
},
divider: "#e0e0e0", // Цвет разделителей
// Кастомные цвета для специфических элементов
custom: {
background: "#D4EFFC", // Кастомный фоновый цвет
text: "#000000", // Кастомный цвет текста
sidebar: "#025EA1", // Фон боковой панели
sidebarText: "#FFFFFF", // Текст в боковой панели
sidebarHover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении в боковой панели
modalBackground: "#FFFFFF", // Фон модальных окон
modalBtnBackground: "#0f55bec2", // Фон кнопок в модальных окнах
modalText: "#333333", // Текст в модальных окнах
tableBorder: "#ddd", // Границы таблиц
tableHeaderBackground: "#f9f9f9", // Фон заголовков таблиц
tableCellBackground: "#FFFFFF", // Фон ячеек таблиц
tableText: "#000000", // Текст в таблицах
treeChartText: "#000000", // Текст в древовидных диаграммах
scrollbarTrack: "#f1f1f1", // Цвет трека скроллбара
hoverButton: "#2d62b1", // Цвет кнопок при наведении
hoverText: "#FFFFFF", // Цвет текста при наведении
background: "#FFFFFF",
text: "#000000",
sidebar: "#3d74c7",
sidebarText: "#FFFFFF",
modalBackground: "#FFFFFF",
modalBtnBackground: "#0f55bec2",
modalText: "#333333",
tableBorder: "#ddd",
tableHeaderBackground: "#f9f9f9",
tableCellBackground: "#FFFFFF",
tableText: "#000000",
treeChartText: "#000000",
scrollbarTrack: "#f1f1f1",
hoverButton: "#2d62b1",
hoverText: "#FFFFFF",
},
// Цвета для различных состояний
action: {
hover: "rgba(0, 0, 0, 0.04)", // Цвет при наведении на интерактивные элементы
selected: "rgba(0, 0, 0, 0.08)", // Цвет выбранных элементов
}
},
});
/**
* Темная тема приложения
*/
export const darkTheme = createTheme({
...commonThemeSettings, // Распаковываем общие настройки
// Цветовая палитра для темной темы
palette: {
mode: "dark", // Режим темной темы
// Фоновые цвета
mode: "dark",
background: {
default: "#2d2d2d", // Основной фон приложения
paper: "#2d2d2d", // Фон "бумажных" поверхностей
default: "#1E1E1E",
paper: "#2d2d2d",
},
// Текстовые цвета
text: {
primary: "#E0E0E0", // Основной цвет текста
secondary: "#B0B0B0", // Вторичный цвет текста
primary: "#E0E0E0",
},
// Основные цвета UI
primary: {
main: "#3d74c7", // Основной брендовый цвет (может совпадать со светлой темой)
contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета
main: "#2d2d2d",
},
// Дополнительные цвета UI
secondary: {
main: "#0f55bec2", // Вторичный брендовый цвет
main: "#333333",
},
divider: "#444444", // Цвет разделителей
// Кастомные цвета для специфических элементов
custom: {
background: "#1E1E1E", // Кастомный фоновый цвет
text: "#E0E0E0", // Кастомный цвет текста
sidebar: "#2d2d2d", // Фон боковой панели
sidebarText: "#E0E0E0", // Текст в боковой панели
sidebarHover: "rgba(255, 255, 255, 0.16)", // Цвет при наведении в боковой панели
modalBackground: "#2d2d2d", // Фон модальных окон
modalBtnBackground: "#333333", // Фон кнопок в модальных окнах
modalText: "#FFFFFF", // Текст в модальных окнах
tableBorder: "#444444", // Границы таблиц
tableHeaderBackground: "#2d2d2d", // Фон заголовков таблиц
tableCellBackground: "#333333", // Фон ячеек таблиц
tableText: "#E0E0E0", // Текст в таблицах
treeChartText: "#FFFFFF", // Текст в древовидных диаграммах
scrollbarTrack: "#333", // Цвет трека скроллбара
hoverButton: "#333d4d", // Цвет кнопок при наведении
hoverText: "#E0E0E0", // Цвет текста при наведении
background: "#1E1E1E",
text: "#E0E0E0",
sidebar: "#2d2d2d",
sidebarText: "#E0E0E0",
modalBackground: "#2d2d2d",
modalBtnBackground: "#333333",
modalText: "#FFFFFF",
tableBorder: "#444444",
tableHeaderBackground: "#2d2d2d",
tableCellBackground: "#333333",
tableText: "#E0E0E0",
treeChartText: "#FFFFFF",
scrollbarTrack: "#333",
hoverButton: "#333d4d",
hoverText: "#E0E0E0",
},
// Цвета для различных состояний
action: {
hover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении на интерактивные элементы
selected: "rgba(255, 255, 255, 0.16)", // Цвет выбранных элементов
}
},
});
});

View File

@ -1,319 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 33 KiB

View File

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

View File

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