added full keycloak support. Partial Quantum backend support, Moved to

.env for config

- removed the globalVars variable for a lack of need
- changed the documentation page for test of multilevel navigation
- added machines page to route
- cleaned up dependencies for build
- changed config to split imports
This commit is contained in:
2026-04-26 16:25:02 +03:00
parent 8e78b8cb47
commit 8449da8373
33 changed files with 863 additions and 417 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.env
# Logs # Logs
logs logs
*.log *.log

326
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@mantine/notifications": "^8.3.1", "@mantine/notifications": "^8.3.1",
"@tabler/icons-react": "^3.34.1", "@tabler/icons-react": "^3.34.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"dotenv": "^17.4.2",
"immer": "^11.1.4", "immer": "^11.1.4",
"keycloak-js": "^26.2.3", "keycloak-js": "^26.2.3",
"miew-react": "^0.11.0", "miew-react": "^0.11.0",
@@ -26,6 +27,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@types/node": "^25.6.0",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
@@ -331,9 +333,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -347,9 +349,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -363,9 +365,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -379,9 +381,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -395,9 +397,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -411,9 +413,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -427,9 +429,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -443,9 +445,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -459,9 +461,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -475,9 +477,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -491,9 +493,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -507,9 +509,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -523,9 +525,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -539,9 +541,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -555,9 +557,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -571,9 +573,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -587,9 +589,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -603,9 +605,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -619,9 +621,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -635,9 +637,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -651,9 +653,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -667,9 +669,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -683,9 +685,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -699,9 +701,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -715,9 +717,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -731,9 +733,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1566,6 +1568,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.15", "version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -1795,9 +1807,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1966,14 +1978,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.5", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.15.11",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^2.1.0"
} }
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
@@ -1994,9 +2006,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2246,6 +2258,18 @@
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2313,9 +2337,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.12", "version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"devOptional": true, "devOptional": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -2326,32 +2350,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12", "@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.25.12", "@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.25.12", "@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.25.12", "@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.25.12", "@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.25.12", "@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.25.12", "@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.25.12", "@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.25.12", "@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.25.12", "@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.25.12", "@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.25.12" "@esbuild/win32-x64": "0.27.7"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@@ -2673,16 +2697,16 @@
} }
}, },
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.3.3", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.11", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -3097,9 +3121,9 @@
} }
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.23", "version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
@@ -3360,9 +3384,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -3373,9 +3397,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"devOptional": true, "devOptional": true,
"funding": [ "funding": [
{ {
@@ -3423,10 +3447,13 @@
} }
}, },
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT" "license": "MIT",
"engines": {
"node": ">=10"
}
}, },
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
@@ -3896,9 +3923,9 @@
} }
}, },
"node_modules/tinyglobby/node_modules/picomatch": { "node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -4023,6 +4050,13 @@
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
@@ -4153,13 +4187,13 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.2.4", "version": "7.3.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@@ -4265,9 +4299,9 @@
} }
}, },
"node_modules/vite/node_modules/picomatch": { "node_modules/vite/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View File

@@ -15,6 +15,7 @@
"@mantine/notifications": "^8.3.1", "@mantine/notifications": "^8.3.1",
"@tabler/icons-react": "^3.34.1", "@tabler/icons-react": "^3.34.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"dotenv": "^17.4.2",
"immer": "^11.1.4", "immer": "^11.1.4",
"keycloak-js": "^26.2.3", "keycloak-js": "^26.2.3",
"miew-react": "^0.11.0", "miew-react": "^0.11.0",
@@ -28,6 +29,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@types/node": "^25.6.0",
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0", "@types/react-dom": "^18.2.0",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",

View File

@@ -1,19 +1,20 @@
import axios, { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
import type { ConvertSchema } from "Types/ApiCalls/ConvertBackendCallsTypes"; import type { ConvertSchema } from "Types/ApiCalls/ConvertBackendCallsTypes";
const openbableBackendRoot = "http://localhost:1654";
export async function ConvertMoleculeToStandart( export async function ConvertMoleculeToStandart(
data: ConvertSchema, data: ConvertSchema,
): Promise<string | AxiosError> { ): Promise<string | AxiosError> {
try { try {
const response = await axios.post(openbableBackendRoot + "/convert", { const response = await axios.post(
text: data.inputText, import.meta.env.VITE_MOLECULAR_BACKEND_URL + "/convert",
format: data.inputFormat, {
convert_3d: data.make_3d, text: data.inputText,
add_hydrogen: data.add_h, format: data.inputFormat,
optimize_geometry: data.optimize, convert_3d: data.make_3d,
}); add_hydrogen: data.add_h,
optimize_geometry: data.optimize,
},
);
// Response from the FastAPI JSONResponse // Response from the FastAPI JSONResponse
return response.data.molfile; return response.data.molfile;
@@ -32,7 +33,9 @@ export async function GetInFormats(): Promise<
{ [key: string]: string } | AxiosError { [key: string]: string } | AxiosError
> { > {
try { try {
const response = await axios.get(openbableBackendRoot + "/informats"); const response = await axios.get(
import.meta.env.VITE_MOLECULAR_BACKEND_URL + "/informats",
);
return response.data; return response.data;
} catch (error) { } catch (error) {
//Error handling //Error handling

View File

@@ -1,28 +1,97 @@
import axios from "axios"; import axios from "axios";
import { keycloakURI } from "GlobalVars"; import Keycloak, { type KeycloakProfile } from "keycloak-js";
import Keycloak from "keycloak-js"; import { routes } from "Routes/Routes";
const keycloak = new Keycloak({ const keycloak = new Keycloak({
url: keycloakURI, url: import.meta.env.VITE_KEYCLOAK_URL
realm: "dev-realm", ? import.meta.env.VITE_KEYCLOAK_URL
: "",
realm: "quant_sim-realm",
clientId: "react-frontend", clientId: "react-frontend",
}); });
const api = axios.create({
baseURL: `${keycloak.authServerUrl}/realms/${keycloak.realm}`,
});
export default keycloak; export default keycloak;
export async function getProfile(keycloak: Keycloak) { export async function getProfile(keycloak: Keycloak) {
return keycloak.loadUserProfile(); return keycloak.loadUserProfile();
} }
export const handleSubmit = async (profile) => { export const updateUserData = async (
profile: KeycloakProfile,
): Promise<KeycloakProfile | undefined> => {
try { try {
await api.put("/account", profile); await keycloak.updateToken(30);
alert("Profile updated!"); const response = await axios.post(
`${keycloak.authServerUrl}/realms/${keycloak.realm}/account/`,
{
firstName: profile.firstName,
lastName: profile.lastName,
email: profile.email,
username: profile.username,
attributes: profile.attributes || {},
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${keycloak.token}`,
},
withCredentials: true, // Important: allows credentials in CORS
},
);
if (response.status === 204) {
// Refresh the local profile after successful update
const updatedProfile = await keycloak.loadUserProfile();
return updatedProfile;
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
}; };
export const updatePasswordWithRedirect = async (
successRedirectUrl?: string,
) => {
try {
await keycloak.updateToken(30);
// Trigger the UPDATE_PASSWORD required action
// This will redirect the user to Keycloak's password change page
return keycloak.login({
action: "UPDATE_PASSWORD",
redirectUri:
successRedirectUrl ||
window.location.origin +
"/" +
import.meta.env.VITE_BASE_PATH +
"/" +
routes.SettingsPage.path +
"?password_updated=true",
// Optional: Force re-authentication
prompt: "login",
});
} catch (error) {
console.error("Failed to initiate password update:", error);
throw error;
}
};
export const SendEmailVerification = async () => {
try {
await keycloak.updateToken(30);
// Trigger the UPDATE_PASSWORD required action
// This will redirect the user to Keycloak's password change page
return keycloak.login({
action: "VERIFY_EMAIL",
redirectUri:
window.location.origin +
import.meta.env.VITE_BASE_PATH +
"/" +
routes.SettingsPage.path +
"?email_sent=true",
});
} catch (error) {
console.error("Failed to initiate password update:", error);
throw error;
}
};

View File

@@ -0,0 +1,52 @@
import keycloak from "Api/Keycloak/Keycloak";
import axios from "axios";
import type { UserData } from "Types/User/User";
export const GetCurrentUserInfo = async (): Promise<UserData | undefined> => {
try {
// Trigger the UPDATE_PASSWORD required action
// This will redirect the user to Keycloak's password change page
const response = await axios.get(
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user`,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${keycloak.token}`,
},
withCredentials: true, // Important: allows credentials in CORS
},
);
if (response.status === 200) {
return response.data;
}
} catch (error) {
console.error("Error Getting User Data", error);
throw error;
}
};
export const UpdateCurrentUserInfo = async (
profile_picture_path: string,
): Promise<UserData | undefined> => {
try {
// Trigger the UPDATE_PASSWORD required action
// This will redirect the user to Keycloak's password change page
const response = await axios.put(
`${import.meta.env.VITE_QUANTUM_BACKEND_URL}/user`,
{ profile_picture_path: profile_picture_path },
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${keycloak.token}`,
},
withCredentials: true, // Important: allows credentials in CORS
},
);
if (response.status === 200) {
return response.data;
}
} catch (error) {
console.error("Error Getting User Data", error);
throw error;
}
};

View File

@@ -7,27 +7,17 @@ import "@mantine/notifications/styles.css";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import { useLayoutStore } from "Stores/LayoutStore"; import { useLayoutStore } from "Stores/LayoutStore";
import {
defaultAnimationDuration,
headerHeight,
sideMenuWidth,
} from "GlobalVars";
import Sidebar from "Components/Layout/Sidebar/Sidebar"; import Sidebar from "Components/Layout/Sidebar/Sidebar";
import { Outlet, useLocation } from "react-router"; import { Outlet, useLocation } from "react-router";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useEffect } from "react"; import { useEffect } from "react";
import { useMediaQuery } from "@mantine/hooks"; import { useMediaQuery } from "@mantine/hooks";
import BreadCrumbs from "Routes/Breadcrumbs/Breadcrumbs"; import BreadCrumbs from "Routes/Breadcrumbs/Breadcrumbs";
import keycloak, { getProfile } from "Api/Keycloak/Keycloak"; import keycloak from "Api/Keycloak/Keycloak";
import { AuthenticationStore } from "Stores/AuthenticationStore"; import { AuthenticationStore } from "Stores/AuthenticationStore";
import type { KeycloakProfile } from "keycloak-js";
import { useNavigate } from "react-router";
function App() { function App() {
const { is_navbar_open, set_navbar_open } = useLayoutStore(); const { is_navbar_open, set_navbar_open } = useLayoutStore();
const { set_token, set_profile } = AuthenticationStore(); const { set_token } = AuthenticationStore();
const navigate = useNavigate();
const theme = useMantineTheme(); const theme = useMantineTheme();
const location = useLocation(); const location = useLocation();
@@ -70,9 +60,9 @@ function App() {
<Notifications /> <Notifications />
<AppShell <AppShell
header={{ height: headerHeight }} header={{ height: 50 }}
aside={{ aside={{
width: sideMenuWidth, width: 280,
breakpoint: "sm", breakpoint: "sm",
collapsed: { collapsed: {
desktop: !is_navbar_open, desktop: !is_navbar_open,
@@ -80,7 +70,7 @@ function App() {
}, },
}} }}
padding="md" padding="md"
transitionDuration={defaultAnimationDuration} transitionDuration={100}
> >
<AppShell.Header id="Header" zIndex={1000}> <AppShell.Header id="Header" zIndex={1000}>
<Header /> <Header />

View File

@@ -1,4 +1,4 @@
import { Card, Text, Button, Title, Space } from "@mantine/core"; import { Card, Text, Space } from "@mantine/core";
import "./CardWithButton.css"; import "./CardWithButton.css";
import { Link } from "react-router"; import { Link } from "react-router";
import CustomButton from "Components/CustomButton/CustomButton"; import CustomButton from "Components/CustomButton/CustomButton";

View File

@@ -35,10 +35,11 @@
border-color: var(--color); border-color: var(--color);
background: transparent; background: transparent;
color: var(--color); color: var(--color);
--hover_color: color-mix(in srgb, var(--hovercolor) 20%, transparent);
} }
.outline:hover { .outline:hover {
background: color-mix(in srgb, var(--hovercolor) 20%, transparent); background: var(--hover_color);
} }
/* ###################### */ /* ###################### */
@@ -46,10 +47,11 @@
.color { .color {
background: var(--color); background: var(--color);
color: var(--textcolor); color: var(--textcolor);
--hover_color: var(--hovercolor);
} }
.color:hover { .color:hover {
background-color: var(--hovercolor); background-color: var(--hover_color);
} }
.primary { .primary {
@@ -98,8 +100,14 @@
.subtle { .subtle {
color: var(--color); color: var(--color);
--hover_color: color-mix(in srgb, var(--color) 20%, transparent);
} }
.subtle:hover { .subtle:hover {
background-color: color-mix(in srgb, var(--color) 20%, transparent); background-color: --hover_color;
}
.button-Disabled {
background-color: var(--hover_color);
cursor: inherit;
} }

View File

@@ -34,7 +34,10 @@ function CustomButton({
console.log(color); console.log(color);
return ( return (
<UnstyledButton <UnstyledButton
className={`colored ${style} ${color} textAlign-${textAlign}`} className={
`colored ${style} ${color} textAlign-${textAlign} ` +
(disabled ? "button-Disabled" : "")
}
style={ style={
color !== "primary" && color !== "primary" &&
color !== "secondary" && color !== "secondary" &&

View File

@@ -2,9 +2,11 @@ import { Burger, Image, Text } from "@mantine/core";
import "./Header.css"; import "./Header.css";
import { useLayoutStore } from "Stores/LayoutStore"; import { useLayoutStore } from "Stores/LayoutStore";
import { Link } from "react-router"; import { Link } from "react-router";
import { baseUrl } from "GlobalVars";
import { routes } from "Routes/Routes"; import { routes } from "Routes/Routes";
const logoUrl =
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH + "/bitmap.png";
function Header() { function Header() {
const { is_navbar_open, set_navbar_open } = useLayoutStore(); const { is_navbar_open, set_navbar_open } = useLayoutStore();
return ( return (
@@ -14,7 +16,7 @@ function Header() {
className="logo" className="logo"
w="auto" w="auto"
fit="contain" fit="contain"
src={baseUrl + "/bitmap.png"} src={logoUrl}
alt="image" alt="image"
/> />
<Text size="xl">QMolSim</Text> <Text size="xl">QMolSim</Text>

View File

@@ -1,6 +1,5 @@
import { import {
Avatar, Avatar,
Button,
Divider, Divider,
Modal, Modal,
Stack, Stack,
@@ -25,7 +24,6 @@ import { useState, type ForwardRefExoticComponent } from "react";
import { routes } from "Routes/Routes"; import { routes } from "Routes/Routes";
import keycloak from "Api/Keycloak/Keycloak"; import keycloak from "Api/Keycloak/Keycloak";
import { AuthenticationStore } from "Stores/AuthenticationStore"; import { AuthenticationStore } from "Stores/AuthenticationStore";
import { baseUri, baseUrl } from "GlobalVars";
import CustomButton from "Components/CustomButton/CustomButton"; import CustomButton from "Components/CustomButton/CustomButton";
interface SubtleLinkButtonProps { interface SubtleLinkButtonProps {
@@ -69,7 +67,12 @@ function Sidebar() {
<div className="sidebar_bottom_bottom"> <div className="sidebar_bottom_bottom">
<CustomButton <CustomButton
text="Подтвердить" text="Подтвердить"
onClick={() => keycloak.logout({ redirectUri: baseUri + baseUrl })} onClick={() =>
keycloak.logout({
redirectUri:
window.location.origin + "/" + import.meta.env.VITE_BASE_PATH,
})
}
textSize="lg" textSize="lg"
color="contrast" color="contrast"
></CustomButton> ></CustomButton>

View File

@@ -2,11 +2,17 @@ import { Pagination } from "@mantine/core";
import "./PaginationContainer.css"; import "./PaginationContainer.css";
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
export function PaginationContainer(a: PropsWithChildren) { interface PaginationContainerProps extends PropsWithChildren {
numberOfPages: number;
}
export function PaginationContainer(a: PaginationContainerProps) {
return ( return (
<div className="PaginationContainer"> <div className="PaginationContainer">
<div className="PaginationContents">{a.children}</div> <div className="PaginationContents">{a.children}</div>
<Pagination total={10} color="accent" /> {a.numberOfPages > 1 && (
<Pagination total={a.numberOfPages} color="accent" />
)}
</div> </div>
); );
} }

View File

@@ -1,11 +0,0 @@
export const baseUrl = "/quantum";
export const baseDomain = "localhost";
export const basePort = 8001;
export const keycloakURI = "http://auth.localhost";
export const baseUri = `http://${baseDomain}:${basePort}`;
//AppShell vars
export const headerHeight = 50;
export const sideMenuWidth = 280;
// Global Style vars
export const defaultAnimationDuration = 100;

View File

@@ -0,0 +1,4 @@
.MantineInput {
background-color: var(--mantine-color-secondary-filled);
color: var(--mantine-color-contrast-filled) !important;
}

View File

@@ -1,5 +1,4 @@
import { import {
Button,
Center, Center,
Modal, Modal,
Select, Select,
@@ -7,9 +6,8 @@ import {
Textarea, Textarea,
TextInput, TextInput,
Title, Title,
UnstyledButton,
} from "@mantine/core"; } from "@mantine/core";
import { useEffect, useState, type ChangeEvent } from "react"; import { useEffect, useState } from "react";
import "./NewExperiment.css"; import "./NewExperiment.css";
import { useExperimentStore } from "Stores/ExperimentStore"; import { useExperimentStore } from "Stores/ExperimentStore";
import CustomButton from "Components/CustomButton/CustomButton"; import CustomButton from "Components/CustomButton/CustomButton";
@@ -68,6 +66,7 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
label="Имя эксперимента" label="Имя эксперимента"
placeholder="Введите имя эксперимента" placeholder="Введите имя эксперимента"
required required
classNames={{ input: "MantineInput" }}
onChange={(event) => { onChange={(event) => {
setName(event.currentTarget.value); setName(event.currentTarget.value);
}} }}
@@ -80,6 +79,7 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
minRows={4} minRows={4}
maxRows={10} maxRows={10}
autosize autosize
classNames={{ input: "MantineInput" }}
onChange={(event) => { onChange={(event) => {
setDescription(event.currentTarget.value); setDescription(event.currentTarget.value);
}} }}
@@ -91,6 +91,7 @@ export function NewExperimentModal(props: NewExperimentModalProps) {
placeholder="Выберите команду эксперимента" placeholder="Выберите команду эксперимента"
searchable searchable
data={[]} data={[]}
classNames={{ input: "MantineInput" }}
onChange={() => {}} onChange={() => {}}
/> />
<Space h="md" /> <Space h="md" />

View File

View File

@@ -0,0 +1,31 @@
import { Helmet } from "react-helmet";
import "./DevicesPage.css";
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
import { Alert } from "@mantine/core";
import { Link } from "react-router";
function DevicesPage() {
return (
<>
<Helmet>
<title>Device Page | QMolSim</title>
<meta
name="description"
content="Get all the devices connected to your or your team account"
/>
</Helmet>
<div className="DevicesPage">
<PaginationContainer numberOfPages={1}>
<Alert title="Подключенные устройства не найдены" color="blue">
Для того, чтобы добавить устройство, простмотрите{" "}
<Link to={"documentaion"} className="invisible_link">
документацию
</Link>
</Alert>
</PaginationContainer>
</div>
</>
);
}
export default DevicesPage;

View File

@@ -1,26 +1,26 @@
.DocumentationPage { .DocumentationPage {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
justify-content: space-between; justify-content: space-between;
padding-top: 25px; padding-top: 25px;
} }
.contents { .contents {
flex-grow: 1; flex-grow: 1;
} }
.tableOfContents { .tableOfContents {
position: sticky; position: sticky;
top: 60px; top: 60px;
width: 150px; width: 190px;
height: min-content; height: min-content;
} }
.docTitle { .docTitle {
padding-bottom: 50px; padding-bottom: 50px;
} }
.hoverCard { .hoverCard {
position: fixed; position: fixed;
bottom: 10px; bottom: 10px;
} }

View File

@@ -1,13 +1,5 @@
import { import { Box, Divider, TableOfContents, Title } from "@mantine/core";
ActionIcon,
Box,
Divider,
Menu,
TableOfContents,
Title,
} from "@mantine/core";
import "./DocumentationPage.css"; import "./DocumentationPage.css";
import { IconMenu2 } from "@tabler/icons-react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
function DocumentationPage() { function DocumentationPage() {
@@ -31,9 +23,12 @@ function DocumentationPage() {
color="accent" color="accent"
size="sm" size="sm"
radius="sm" radius="sm"
minDepthToOffset={0}
depthOffset={20}
scrollSpyOptions={{ scrollSpyOptions={{
selector: "section h2", selector: "section h1, h2",
}} }}
className=""
getControlProps={({ data }) => ({ getControlProps={({ data }) => ({
onClick: () => onClick: () =>
data data
@@ -43,56 +38,12 @@ function DocumentationPage() {
})} })}
/> />
</Box> </Box>
<Box className="hoverCard" hiddenFrom="md">
<Menu
width={280}
shadow="md"
openDelay={100}
closeDelay={100}
closeOnClickOutside
closeOnItemClick
floatingStrategy="fixed"
>
<Menu.Target>
<ActionIcon
aria-label="navigation"
variant="gradient"
gradient={{ from: "blue", to: "cyan", deg: 90 }}
radius={"50%"}
size={40}
>
<IconMenu2 size={25} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<TableOfContents
variant="light"
color="accent"
size="sm"
radius="sm"
scrollSpyOptions={{
selector: "section h2",
}}
getControlProps={({ data }) => ({
onClick: () =>
data
.getNode()
.scrollIntoView({ behavior: "smooth", block: "center" }),
children: data.value,
})}
/>
</Menu.Dropdown>
</Menu>
</Box>
<div className="contents"> <div className="contents">
<section id="introduction" style={{ height: 1000 }}> <section id="introduction" style={{ height: 1000 }}>
<Title order={2}>Introduction</Title> <Title order={1}>1. Введение</Title>
</section> </section>
<section id="features" style={{ height: 1000 }}> <section id="quick-start" style={{ height: 1000 }}>
<Title order={2}>features</Title> <Title order={2}>1.1 Быстрое начало</Title>
</section>
<section id="conclusion" style={{ height: 1000 }}>
<Title order={2}>Conslusion</Title>
</section> </section>
</div> </div>
</div> </div>

View File

@@ -74,9 +74,9 @@ function ExperimentPage() {
</div> </div>
</div> </div>
{experiment.tasks_ids.length > 0 && ( {experiment.tasks_ids.length > 0 && (
<PaginationContainer> <PaginationContainer numberOfPages={1}>
{experiment.tasks_ids.map((task: ExperimentTask) => { {experiment.tasks_ids.map((task: number) => {
return <div>{task.data.name}</div>; return <div>{task}</div>;
})} })}
</PaginationContainer> </PaginationContainer>
)} )}
@@ -95,7 +95,7 @@ function ExperimentPage() {
<Alert color="red"> <Alert color="red">
<Center> <Center>
{" "} {" "}
<Text c="contrast" size={"xl"} size="md"> <Text c="contrast" size={"xl"}>
Ошибка. Эксперимент не найден Ошибка. Эксперимент не найден
</Text>{" "} </Text>{" "}
</Center> </Center>

View File

@@ -8,10 +8,11 @@ import ExperimentsListCard from "Components/ListCard/ExperimentsListCard";
import { useExperimentStore } from "Stores/ExperimentStore"; import { useExperimentStore } from "Stores/ExperimentStore";
import type { Experiment } from "Types/Experiment/Experiment"; import type { Experiment } from "Types/Experiment/Experiment";
import CustomButton from "Components/CustomButton/CustomButton"; import CustomButton from "Components/CustomButton/CustomButton";
import { Alert } from "@mantine/core";
function ExperimentsPage() { function ExperimentsPage() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { experiments, teams, tasks } = useExperimentStore(); const { experiments, teams } = useExperimentStore();
return ( return (
<> <>
@@ -32,18 +33,20 @@ function ExperimentsPage() {
marginBottom: "20px", marginBottom: "20px",
}} }}
> >
<div className="experimentsButtons"> {teams.length > 0 && (
<CustomButton <div className="experimentsButtons">
color="contrast" <CustomButton
onClick={() => { color="contrast"
setIsOpen(true); onClick={() => {
}} setIsOpen(true);
icon={<IconMicroscope />} }}
text="Создать эксперимент" icon={<IconMicroscope />}
/> text="Создать эксперимент"
</div> />
</div>
)}
</div> </div>
<PaginationContainer> <PaginationContainer numberOfPages={1}>
{experiments.map((exp: Experiment) => { {experiments.map((exp: Experiment) => {
return ( return (
<ExperimentsListCard <ExperimentsListCard
@@ -58,6 +61,12 @@ function ExperimentsPage() {
/> />
); );
})} })}
{teams.length == 0 && (
<Alert title="Команды не найдены" color="red">
Создайте или войдите в команду чтобы начать работу с
экспериментами
</Alert>
)}
</PaginationContainer> </PaginationContainer>
</div> </div>
</> </>

View File

@@ -1,4 +1,6 @@
.TeamsPage { .TeamsPage {
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column;
gap: 10px;
} }

View File

@@ -1,8 +1,14 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import "./TeamsPage.css"; import "./TeamsPage.css";
import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer"; import { PaginationContainer } from "Components/PaginationContainer/PaginationContainer";
import CustomButton from "Components/CustomButton/CustomButton";
import { IconUsersGroup } from "@tabler/icons-react";
import { useState } from "react";
import { Alert } from "@mantine/core";
function TeamsPage() { function TeamsPage() {
const [, setIsOpen] = useState(false);
return ( return (
<> <>
<Helmet> <Helmet>
@@ -13,7 +19,21 @@ function TeamsPage() {
/> />
</Helmet> </Helmet>
<div className="TeamsPage"> <div className="TeamsPage">
<PaginationContainer /> <div className="experimentsButtons">
<CustomButton
color="contrast"
onClick={() => {
setIsOpen(true);
}}
icon={<IconUsersGroup />}
text="Создать эксперимент"
/>
</div>
<PaginationContainer numberOfPages={1}>
<Alert title="Команды не найдены" color="blue">
Создайте или войдите в команду чтобы начать работу с экспериментами
</Alert>
</PaginationContainer>
</div> </div>
</> </>
); );

View File

@@ -1,13 +1,12 @@
import { import {
Button,
Title, Title,
Tabs, Tabs,
TextInput, TextInput,
SimpleGrid, SimpleGrid,
Chip,
Text,
Divider, Divider,
Switch, Switch,
LoadingOverlay,
Space,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconChartCandle, IconChartCandle,
@@ -17,20 +16,98 @@ import {
IconSun, IconSun,
IconMoonStars, IconMoonStars,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import keycloak from "Api/Keycloak/Keycloak"; import keycloak, {
import { useEffect } from "react"; SendEmailVerification,
updatePasswordWithRedirect,
updateUserData,
} from "Api/Keycloak/Keycloak";
import { useEffect, useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { AuthenticationStore } from "Stores/AuthenticationStore"; import { AuthenticationStore } from "Stores/AuthenticationStore";
import "./UserPage.css"; import "./UserPage.css";
import { useUserPreferencesStore } from "Stores/PreferencesStore"; import { useUserPreferencesStore } from "Stores/PreferencesStore";
import CustomButton from "Components/CustomButton/CustomButton"; import CustomButton from "Components/CustomButton/CustomButton";
import { notifications } from "@mantine/notifications";
import { useSearchParams } from "react-router";
import {
GetCurrentUserInfo,
UpdateCurrentUserInfo,
} from "Api/QuantumBackend/UserManagement";
import type { UserData } from "Types/User/User";
function UserPage() { function UserPage() {
const { profile, token } = AuthenticationStore(); const { profile, is_loading, profile_picture_path } = AuthenticationStore();
const { theme, set_theme } = useUserPreferencesStore(); const { theme, set_theme } = useUserPreferencesStore();
const [username, set_username] = useState<string>("");
const [email, set_email] = useState<string>("");
const [pfp_path, set_pfp_path] = useState<string>("");
const [is_editing_path, set_is_editing_path] = useState<boolean>(false);
const [searchParams] = useSearchParams();
const updateData = () => {
if (
profile &&
((profile.username && profile.username != username) ||
(profile.email && profile.email != email))
) {
console.log("A");
const prof_1 = profile;
prof_1.email = email;
prof_1.username = username;
AuthenticationStore.setState({
is_loading: true,
});
updateUserData(prof_1).then((data) => {
console.log(data);
if (data) {
notifications.show({
radius: "md",
title: "Данные успешно изменены",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
keycloak.loadUserProfile().then((profile) => {
AuthenticationStore.setState({
is_loading: false,
profile: profile,
});
});
}
});
}
};
const update_pfp = async () => {
UpdateCurrentUserInfo(pfp_path).then(() => {
GetCurrentUserInfo().then((info: UserData | undefined) => {
if (info && info.profile_picture_path)
AuthenticationStore.setState({
profile_picture_path: info.profile_picture_path,
});
});
});
};
useEffect(() => { useEffect(() => {
console.log(token); if (profile) {
}, []); if (profile.username) set_username(profile.username);
if (profile.email) set_email(profile.email);
if (searchParams.get("password_updated")) {
notifications.show({
radius: "md",
title: "Пароль изменен успешно",
message: "",
icon: <IconCheck />,
style: { paddingLeft: "5px" },
});
}
}
}, [profile, searchParams]);
useEffect(() => {
if (profile_picture_path) set_pfp_path(profile_picture_path);
}, [profile_picture_path]);
return ( return (
<> <>
<Helmet> <Helmet>
@@ -68,92 +145,223 @@ function UserPage() {
</Tabs.List> </Tabs.List>
<Tabs.Panel value="profile"> <Tabs.Panel value="profile">
<SimpleGrid verticalSpacing="lg" cols={2}> <div
<Title size="lg">Имя пользователя:</Title> style={{ position: "relative", padding: "5px", height: "100%" }}
<TextInput size="md" value={profile?.username} /> >
<Title size="lg">Почта:</Title> <LoadingOverlay
<TextInput size="md" value={profile?.email} /> visible={is_loading}
<CustomButton color="accent" text="Сохранить" /> zIndex={1000}
<CustomButton color="error" text="Отменить" /> overlayProps={{ radius: "sm", blur: 2 }}
</SimpleGrid> loaderProps={{ size: 50, type: "dots" }}
<Divider my="lg" /> />
<SimpleGrid verticalSpacing={"lg"} cols={2}> <SimpleGrid verticalSpacing="lg" cols={2}>
<Title <Title size="lg">Имя пользователя:</Title>
size="lg" <TextInput
style={{ display: "flex", alignItems: "center" }} size="md"
> value={username}
Почта верифицирована:{" "} onChange={(e) => set_username(e.currentTarget.value)}
<Chip mx="lg" size="auto" checked={profile?.emailVerified}> />
{profile?.emailVerified ? ( <Title size="lg">Почта:</Title>
<div <TextInput
style={{ size="md"
display: "flex", value={email}
justifyContent: "center", onChange={(e) => set_email(e.currentTarget.value)}
alignItems: "center", />
}}
> <CustomButton
<IconCheck color="accent"
size={25} text="Сохранить"
color={"green"} onClick={updateData}
style={{ verticalAlign: "center", margin: "5px" }} disabled={
/> !(
</div> profile != null &&
) : ( ((profile.username != undefined &&
<div profile.username != username) ||
style={{ (profile.email != undefined && profile.email != email))
display: "flex", )
justifyContent: "center", }
alignItems: "center", />
}} <CustomButton
> color="error"
<IconCancel text="Отменить"
size={25} disabled={
color="red" !(
profile != null &&
((profile.username != undefined &&
profile.username != username) ||
(profile.email != undefined && profile.email != email))
)
}
/>
</SimpleGrid>
<Divider my="lg" />
<SimpleGrid verticalSpacing={"lg"} cols={2}>
<Title
size="lg"
style={{ display: "flex", alignItems: "center" }}
>
Почта верифицирована:{" "}
<div>
{profile?.emailVerified ? (
<div
style={{ style={{
verticalAlign: "center", display: "flex",
margin: "5px", justifyContent: "center",
alignItems: "center",
}}
>
<IconCheck
size={25}
color={"white"}
style={{ verticalAlign: "center", margin: "5px" }}
/>
</div>
) : (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<IconCancel
size={25}
color="red"
style={{
verticalAlign: "center",
margin: "5px",
}}
/>
</div>
)}
</div>
</Title>
{!profile?.emailVerified && (
<div>
<CustomButton
style="outline"
color="contrast"
text="Подтвердить почту"
onClick={() => SendEmailVerification()}
/>
</div>
)}
{profile?.emailVerified && (
<div style={{ height: "40px", display: "flex" }}></div>
)}
<Title size="lg">Пароль:</Title>
<div>
<CustomButton
style="outline"
color="contrast"
text="Изменить пароль"
onClick={() => {
updatePasswordWithRedirect();
}}
/>
</div>
</SimpleGrid>
<Divider my="lg" />
<div style={{ display: "flex", flexDirection: "column" }}>
<Title size="lg">Фотография профиля</Title>
<Space my="sm" />
{!is_editing_path ? (
<img height={350} width={350} src={profile_picture_path} />
) : (
<TextInput
size="md"
value={pfp_path}
onChange={(e) => set_pfp_path(e.currentTarget.value)}
/>
)}
<Space my="sm" />
<div style={{ width: "15em" }}>
{!is_editing_path ? (
<CustomButton
style="outline"
color="contrast"
text="Изменить"
onClick={() => {
set_is_editing_path(true);
}}
/>
) : (
<>
<CustomButton
style="outline"
color="contrast"
text="Сохранить"
onClick={() => {
update_pfp();
set_is_editing_path(false);
}} }}
/> />
</div> <Space my="sm" />
<CustomButton
style="outline"
color="red"
text="Отменить"
onClick={() => {
set_is_editing_path(false);
set_pfp_path(profile_picture_path);
}}
/>
</>
)} )}
</Chip> </div>
</Title> </div>
{!profile?.emailVerified && ( </div>
<CustomButton
style="outline"
color="contrast"
text="Подтвердить почту"
/>
)}
{profile?.emailVerified && <div></div>}
<Title size="lg">Пароль:</Title>
<CustomButton
style="outline"
color="contrast"
text="Изменить пароль"
/>
</SimpleGrid>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="preference"> <Tabs.Panel value="preference">
<Switch <Switch
size="xl" size="xl"
defaultChecked={theme == "light"}
onChange={(event) => onChange={(event) =>
set_theme(!event.currentTarget.checked ? "dark" : "light") set_theme(!event.currentTarget.checked ? "dark" : "light")
} }
onLabel={ onLabel={
<IconSun <div
size={16} style={{
stroke={2.5} display: "flex",
color="var(--mantine-color-yellow-4)" flexDirection: "row",
/> flexWrap: "nowrap",
gap: "10px",
alignContent: "center",
justifyContent: "center",
alignItems: "center",
padding: "5px",
}}
>
<IconSun
size={30}
stroke={2.5}
color="var(--mantine-color-yellow-4)"
/>
<Title size={"md"}>Светлая</Title>
</div>
} }
offLabel={ offLabel={
<IconMoonStars <div
size={16} style={{
stroke={2.5} display: "flex",
color="var(--mantine-color-blue-6)" flexDirection: "row",
/> flexWrap: "nowrap",
gap: "10px",
alignContent: "center",
justifyContent: "center",
alignItems: "center",
padding: "5px",
}}
>
<Title size={"md"}>Темная</Title>
<IconMoonStars
size={30}
stroke={2.5}
color="var(--mantine-color-blue-6)"
/>
</div>
} }
/> />
</Tabs.Panel> </Tabs.Panel>

View File

@@ -1,6 +1,6 @@
import { Breadcrumbs } from "@mantine/core"; import { Breadcrumbs } from "@mantine/core";
import { type ReactElement } from "react"; import { type ReactElement } from "react";
import { Link, type UIMatch } from "react-router"; import { Link } from "react-router";
import "./Breadcrumbs.css"; import "./Breadcrumbs.css";
import { routes } from "Routes/Routes"; import { routes } from "Routes/Routes";
import { useLocation } from "react-router"; import { useLocation } from "react-router";

View File

@@ -10,7 +10,7 @@ export default function AuthGuard() {
loginStarted.current = true; loginStarted.current = true;
keycloak.login({ keycloak.login({
redirectUri: window.location.href, redirectUri: window.location.origin,
}); });
} }
}, []); }, []);

View File

@@ -1,6 +1,5 @@
import { createBrowserRouter } from "react-router"; import { createBrowserRouter } from "react-router";
import MainPage from "Pages/MainPage/MainPage"; import MainPage from "Pages/MainPage/MainPage";
import { baseUrl } from "../GlobalVars";
import App from "../App"; import App from "../App";
import ErrorPage from "./ErrorPage"; import ErrorPage from "./ErrorPage";
import DocumentationPage from "Pages/DocumentationPage/DocumentationPage"; import DocumentationPage from "Pages/DocumentationPage/DocumentationPage";
@@ -12,6 +11,7 @@ import UserPage from "Pages/UserPage/UserPage";
import ExperimentPage from "Pages/ExperimentsPage/ExperimentPage"; import ExperimentPage from "Pages/ExperimentsPage/ExperimentPage";
import ExperimentsPage from "Pages/ExperimentsPage/ExperimentsPage"; import ExperimentsPage from "Pages/ExperimentsPage/ExperimentsPage";
import AuthGuard from "./RouterAuhGuard"; import AuthGuard from "./RouterAuhGuard";
import DevicesPage from "Pages/DevicesPage/DevicesPage";
export const routes: { export const routes: {
[id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] }; [id: string]: { path: string; breadcrumbs: (path: string) => ReactElement[] };
@@ -66,7 +66,7 @@ const router = createBrowserRouter(
}, },
{ {
element: <AuthGuard />, // 🔒 everything below requires auth element: <AuthGuard />,
children: [ children: [
{ {
path: routes.ExperimentsPage.path, path: routes.ExperimentsPage.path,
@@ -84,6 +84,10 @@ const router = createBrowserRouter(
path: routes.TeamsPage.path, path: routes.TeamsPage.path,
Component: TeamsPage, Component: TeamsPage,
}, },
{
path: routes.MachinesPage.path,
Component: DevicesPage,
},
], ],
}, },
@@ -94,6 +98,6 @@ const router = createBrowserRouter(
], ],
}, },
], ],
{ basename: baseUrl }, { basename: import.meta.env.VITE_BASE_PATH },
); );
export default router; export default router;

View File

@@ -2,18 +2,24 @@ import type { KeycloakProfile } from "keycloak-js";
import { create } from "zustand"; import { create } from "zustand";
interface AuthernticationStoreState { interface AuthernticationStoreState {
token: string | undefined; is_loading: boolean;
profile: KeycloakProfile | null; profile: KeycloakProfile | null;
profile_picture_path: string;
set_profile: (profile: KeycloakProfile | null) => void; set_profile: (profile: KeycloakProfile | null) => void;
set_token: (token: string | undefined) => void; set_profile_picture_path: (path: string) => void;
set_is_loading: (is_loading: boolean) => void;
} }
export const AuthenticationStore = create<AuthernticationStoreState>()( export const AuthenticationStore = create<AuthernticationStoreState>()(
(set) => ({ (set) => ({
token: undefined, is_loading: true,
profile: null, profile: null,
profile_picture_path: "",
set_profile: (profile: KeycloakProfile | null) => set_profile: (profile: KeycloakProfile | null) =>
set(() => ({ profile: profile })), set(() => ({ profile: profile })),
set_token: (token: string | undefined) => set(() => ({ token: token })), set_is_loading: (is_loading: boolean) =>
set(() => ({ is_loading: is_loading })),
set_profile_picture_path: (path: string) =>
set(() => ({ profile_picture_path: path })),
}), }),
); );

View File

@@ -1,7 +1,3 @@
//export interface Experiment {}
import type { Team } from "Types/User/User";
export type ExperimentStatus = export type ExperimentStatus =
| "DRAFT" | "DRAFT"
| "QUEUE" | "QUEUE"

View File

@@ -18,3 +18,11 @@ export interface Permission {
id: number; id: number;
name: string; name: string;
} }
export interface UserData {
keycloak_id: string;
email: string;
username: string;
profile_picture_path: string | null;
created_at: Date;
}

View File

@@ -13,6 +13,9 @@ import {
import keycloak from "Api/Keycloak/Keycloak"; import keycloak from "Api/Keycloak/Keycloak";
import { AuthenticationStore } from "Stores/AuthenticationStore"; import { AuthenticationStore } from "Stores/AuthenticationStore";
import { useUserPreferencesStore } from "Stores/PreferencesStore"; import { useUserPreferencesStore } from "Stores/PreferencesStore";
import type { KeycloakProfile } from "keycloak-js";
import { GetCurrentUserInfo } from "Api/QuantumBackend/UserManagement";
import type { UserData } from "Types/User/User";
const colorSchemeManager = localStorageColorSchemeManager({ const colorSchemeManager = localStorageColorSchemeManager({
key: "my-app-color-scheme", key: "my-app-color-scheme",
@@ -60,18 +63,39 @@ const theme = createTheme({
}); });
async function bootstrap() { async function bootstrap() {
const authenticated = await keycloak.init({ AuthenticationStore.setState({ is_loading: true });
onLoad: "check-sso", keycloak
pkceMethod: "S256", .init({
silentCheckSsoRedirectUri: onLoad: "check-sso",
"http://localhost:8001/quantum/silent-check-sso.html", pkceMethod: "S256",
}); silentCheckSsoRedirectUri:
window.location.origin +
if (authenticated) { "/" +
const profile = await keycloak.loadUserProfile(); import.meta.env.VITE_BASE_PATH +
"/silent-check-sso.html",
AuthenticationStore.setState({ token: keycloak.token, profile: profile }); silentCheckSsoFallback: false,
} })
.then((authenticated: boolean) => {
if (authenticated) {
keycloak.loadUserProfile().then((profile: KeycloakProfile) => {
AuthenticationStore.setState({
is_loading: false,
profile: profile,
});
GetCurrentUserInfo().then((info: UserData | undefined) => {
if (info && info.profile_picture_path)
AuthenticationStore.setState({
profile_picture_path: info.profile_picture_path,
});
});
});
} else {
console.log("Keycloak authentication failed.");
AuthenticationStore.setState({
is_loading: false,
});
}
});
useUserPreferencesStore.setState({ useUserPreferencesStore.setState({
theme: checkIsDarkSchemePreferred() ? "dark" : "light", theme: checkIsDarkSchemePreferred() ? "dark" : "light",
@@ -80,7 +104,7 @@ async function bootstrap() {
createRoot(document.getElementById("root")!).render(<AppProviders />); createRoot(document.getElementById("root")!).render(<AppProviders />);
} }
function AppProviders() { export function AppProviders() {
const themePreference = useUserPreferencesStore((state) => state.theme); const themePreference = useUserPreferencesStore((state) => state.theme);
return ( return (

View File

@@ -1,14 +1,33 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { baseDomain, basePort, baseUrl } from "./src/GlobalVars"; import dotenv from "dotenv";
dotenv.config();
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tsconfigPaths()], plugins: [react(), tsconfigPaths()],
server: { server: {
host: baseDomain, host: process.env.VITE_HOSTNAME,
port: basePort, port: Number(process.env.VITE_PORT),
},
preview: {
port: Number(process.env.VITE_PORT),
},
base: "/" + process.env.VITE_BASE_PATH,
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes("node_modules")) {
return id
.toString()
.split("node_modules/")[1]
.split("/")[0]
.toString();
}
},
},
},
}, },
base: baseUrl,
}); });